Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FAB-17992] Allow remove ledger data for a channel #1403

Closed
wants to merge 1 commit into from

Conversation

wenjianqiao
Copy link
Contributor

@wenjianqiao wenjianqiao commented Jun 15, 2020

Signed-off-by: Wenjian Qiao wenjianq@gmail.com

Type of change

  • New feature

Description

Add a Remove function to block storage provider in oder to
remove ledger data for a channel. It creates a temporary file
to indicate the channel is to be removed and start a goroutine
to remove channel ledger data in background. If remove fails or
orderer is stopped before remove is done, upon ledger restart,
it checks the existence of the temporary file and complete remove
as needed.

Additional details

Related issues

Story: https://jira.hyperledger.org/browse/FAB-17992
Epic: https://jira.hyperledger.org/browse/FAB-17712

@wenjianqiao wenjianqiao changed the title Allow remove ledger data for a channel [FAB-17992] Allow remove ledger data for a channel Jun 15, 2020
Add a Remove function to block storage provider in oder to
remove ledger data for a channel. It creates a temporary file
to indicate the channel is to be removed and start a goroutine
to remove channel ledger data in background. If remove fails or
orderer is stopped before remove is done, upon ledger restart,
it checks the existence of the temporary file and complete remove
as needed.

Signed-off-by: Wenjian Qiao <wenjianq@gmail.com>
@wenjianqiao wenjianqiao marked this pull request as ready for review June 15, 2020 18:04
@wenjianqiao wenjianqiao requested a review from a team as a code owner June 15, 2020 18:04
Copy link
Contributor

@cendhu cendhu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, Wenjian. I have a few suggestions on the approach.

}

// Remove block index and blocks for the given ledgerid (channelID).
// It creates a temporary file to indicate the channel is to be removed and deletes the ledger data in a separate goroutine.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about this. Given that the block store is common to both orderer and peer, isn't it good to handle the failures outside block store?

Let's assume we support deletion of channel from an orderer as well as a peer. In that case, for peer, we would use the idStore at kvledger pkg to manage the channel removal request (similar to what we do for join request). This is because at a peer, we need to remove data from many stores not just block store.

Similarly, I think the orderer needs to handle the failure (not at the block store). IMO, Orderer needs to have something like idStore to manage join/delete request. We shouldn't have this logic at the common block store. May be it is good to introduce idStore at

type fileLedgerFactory struct {

Copy link
Contributor Author

@wenjianqiao wenjianqiao Jun 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @Cendu. Since blockstore has only 1 provider, failure handling can be self-contained in blockstore even if it is called via peer. Having said so, agree that it is better to do it in the higher level above blockstore.

}
f.Close()

go p.removeLedgerData(ledgerid)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are multiple issues with this line.

  1. What would happen when the removeLedgerData() returns an error?
  2. There is a fundamental assumption that the channel removal request is non-blocking. While this may be true for orderer (need to verify), I am not sure whether it would be true for the peer (because the behaviour of peer channel delete might be similar to peer channel rollback, i.e., blocking call)

I would suggest leaving this blocking/non-blocking decision to the caller, i.e., either orderer or peer. If orderer wants to support a non-blocking delete, it can call Remove() within a goroutine. If peer wants to support a blocking delete, it can just call Remove().

Copy link
Contributor

@tock-ibm tock-ibm Jun 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plan in the channel participation API is to have a blocking call that returns only when resources are gone. Gone, as far as the API is concerned, means that after the call returns (API) List will not find that channel, and moreover, that we can create a new channel by the same name immediately after it was removed.

This doesn't mean that the underlying implementation cannot remove resources in a lazy fashion if it knows how to mark them as deleted and support the creation of a new channel before old resources were fully removed.

This is a rare call, and the performance impacts of it are negligible. I assume (hope) it is also relatively fast - a fraction of a second - such that a human user using an UI won't loose patience. Therefore the preference is for the REST call to return after all resources are removed.


// completePendingRemoves checks __toBeRemoved_xxx files and removes the corresponding channel ledger data
// if any temporary file(s) is found. This function should only be called upon ledger init.
func (p *BlockStoreProvider) completePendingRemoves() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is similar to

func (p *Provider) recoverUnderConstructionLedger() {

Recovery of creation or deletion request is better done at the higher layer using the idStore (i.e., peer or orderer).

Comment on lines 114 to 124
func (p *BlockStoreProvider) Exists(ledgerid string) (bool, error) {
exists, _, err := util.FileExists(p.conf.getLedgerBlockDir(ledgerid))
return exists, err
if !exists || err != nil {
return false, err
}
toBeRemoved, _, err := util.FileExists(p.conf.getToBeRemovedFilePath(ledgerid))
if err != nil {
return false, err
}
return !toBeRemoved, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this function used by the production code? I see it used only in the test. If it is not used in production, would it be better to remove it rather than complicating this method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Remove function calls this function to know if a channel exists. Before this PR, no production code uses it.

Comment on lines +127 to +144
// A channel is filtered out if it has a temporary __toBeRemoved_ file.
func (p *BlockStoreProvider) List() ([]string, error) {
return util.ListSubdirs(p.conf.getChainsDir())
subdirs, err := util.ListSubdirs(p.conf.getChainsDir())
if err != nil {
return nil, err
}
channelNames := []string{}
for _, subdir := range subdirs {
toBeRemoved, _, err := util.FileExists(p.conf.getToBeRemovedFilePath(subdir))
if err != nil {
return nil, err
}
if !toBeRemoved {
channelNames = append(channelNames, subdir)
}
}
return channelNames, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the peer, we only call Exists() and List() methods on the idStore at the kvledger pkg.

func (p *Provider) Exists(ledgerID string) (bool, error) {

The List() on idStore returns only the active ledger IDs.

func (p *Provider) List() ([]string, error) {

Would it be good to implement idStore at the factory.go which is used only by the orderer?

type fileLedgerFactory struct {

This idStore in fileLedgerFactory may take care of managing non-blocking channel removal request and failures. It can also implement the List() similar to what we have at the kvledger. As a result, we can remove this List() at the block store.

Comment on lines +178 to +182
batch.Delete(key)
numKeys++
if batch.Len() >= maxBatchSize {
if err := h.WriteBatch(batch, true); err != nil {
return err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that we limit the batch size to

  1. reduce the memory usage
  2. reduce huge disk reads (continuously)

If yes, instead of having a maxBatchSize, we need to have a limit on the batch memory limit. For example, we can have a memory limit of size ~10 MB. We can calculate the len(key) to measure the memory utilization of the batch.

Comment on lines +176 to +177
for iter.Next() {
key := iter.Key()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not much can be done here. It is unfortunate that we need to read the whole DB to delete the DB. When we call iter.Next(), it internally reads both the key and value.

Comment on lines +185 to +186
sleepTime := time.Duration(batchesInterval)
logger.Infof("Sleep for %d milliseconds between batches of deletion. Entries have been removed for channel %s: %d", sleepTime, h.dbName, numKeys)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a sleep of 1 second might increase the channel removal time. Moreover, the delete might set some flags or marker rather than writing huge amount of data to disk. Unless we do some benchmark, I am not sure about the sleep.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sleep is between batches so that the deletion does not throttle the other operations by causing a sudden burst of I/O. Sleep is added because Remove is a non-blocking method. Need to reconsider if Remove is a blocking method.

@wenjianqiao
Copy link
Contributor Author

wenjianqiao commented Jun 15, 2020

@Cendu Thank you for your comments. The approach taken in this PR was based on our discussion in scrum - a temporary file is used for simplicity. Asynchronous remove and sleep was based on discussion with @manish-sethi, the purpose is to prevent a sudden I/O burst. However, asynchronous remove prevents error propagation when removing a channel from peer. Agree it is better to let the caller decide how to call Remove (blocking vs. non-blocking). Will discuss offline with you and Manish regarding to idStore and how to limit batch size.

@manish-sethi
Copy link
Contributor

@Cendu Thank you for your comments. The approach taken in this PR was based on the discussion in scrum. Asynchronous remove and sleep was based on discussion with @manish-sethi. Having said so, asynchronous removal at blockstore prevents error propagation when removing a channel from peer. Therefore, it makes sense to let the caller decide how to call Remove (either blocking or non-blocking). Will discuss offline with you and Manish regarding to idStore and how to limit batch size.

In any case, we should still mark a channel 'under-deletion' state internally and

  • Don't return this channel in the list of available channels
  • Or rather, enhance the list API to include the status of the channel (e.g., active / under deletion) so consumer not necessarily has to maintain the state and can query blockstore to invoke the continuation of deletion.
    @tock-ibm - do you have an opinion on this?

@cendhu
Copy link
Contributor

cendhu commented Jun 16, 2020

@Cendu Thank you for your comments. The approach taken in this PR was based on the discussion in scrum. Asynchronous remove and sleep was based on discussion with @manish-sethi. Having said so, asynchronous removal at blockstore prevents error propagation when removing a channel from peer. Therefore, it makes sense to let the caller decide how to call Remove (either blocking or non-blocking). Will discuss offline with you and Manish regarding to idStore and how to limit batch size.

In any case, we should still mark a channel 'under-deletion' state internally and

  • Don't return this channel in the list of available channels
  • Or rather, enhance the list API to include the status of the channel (e.g., active / under deletion) so consumer not necessarily has to maintain the state and can query blockstore to invoke the continuation of deletion.
    @tock-ibm - do you have an opinion on this?

@manish-sethi why do you say so? IMO what you have done with idStore at the kvledger is the clean approach. The peer does not ask the block store to find out the list of active channels --

func (s *idStore) getActiveLedgerIDs() ([]string, error) {
If we have idStore at the orderer too, we can use it to store

  1. the last config block used for the join & under construction flag for a channel
  2. whether a channel is passed. (@tock-ibm at the peer, we can pause/resume a channel. See whether it is applicable/useful for the orderer too)
  3. whether a channel is being removed
  4. all active channels

I am not convinced that we need to add multiple internal files at the block store to manage these when it can be done cleanly using an idStore.

Moreover, ListDir() at the block store cannot be trusted in the current form. When a peer channel join command is issued, we first create all necessary DBs including a folder at the block store. However, if the peer fails before committing the genesis block, the folder at the block store is still present when the peer starts again. If the user does not issue another peer join channel, the ListDir() would return in-consistent result. Current, We have a no-op cleanup function

func (p *Provider) runCleanup(ledgerID string) error {
that can be enhanced to remove these empty folders and DBs created or we can store the genesis block itself in the idStore to manage failure but I still think that these things need to be managed at the consumer. I am not familiar with the orderer code and hence I am not sure whether orderer does a cleanup after a failure to rely on ListDir().

@manish-sethi
Copy link
Contributor

@manish-sethi why do you say so? IMO what you have done with idStore at the kvledger is the clean approach. The peer does not ask the block store to find out the list of active channels --

Yes, I agree and that's why I did that at the kvledger level at the first place. But somehow, orderer never maintained that status on it's own. I guess that it hit a corner case bug for not supporting atomic creation of a channel with genesis block commit. However, unfortunately, we made a mistake of supporting the list function which is not robust enough to recover from failures.

So, now the call we have to make is whether orderer maintains it's status on it's own or make the blockstore more robust in maintaining this. However, I did not want to drag that discussion over this PR, as this is beyond this PR.

@cendhu
Copy link
Contributor

cendhu commented Jun 16, 2020

Yes, I agree and that's why I did that at the kvledger level at the first place. But somehow, orderer never maintained that status on it's own. I guess that it hit a corner case bug for not supporting atomic creation of a channel with genesis block commit. However, unfortunately, we made a mistake of supporting the list function which is not robust enough to recover from failures.

So, now the call we have to make is whether orderer maintains it's status on it's own or make the blockstore more robust in maintaining this. However, I did not want to drag that discussion over this PR, as this is beyond this PR.

Sure. So we are in the same page. I will move this discussion offline. I think @wenjianqiao can limit the scope of this PR to just removal of the channel's data without handling the failure/recovery.

@tock-ibm
Copy link
Contributor

tock-ibm commented Jun 16, 2020

@Cendu Thank you for your comments. The approach taken in this PR was based on the discussion in scrum. Asynchronous remove and sleep was based on discussion with @manish-sethi. Having said so, asynchronous removal at blockstore prevents error propagation when removing a channel from peer. Therefore, it makes sense to let the caller decide how to call Remove (either blocking or non-blocking). Will discuss offline with you and Manish regarding to idStore and how to limit batch size.

In any case, we should still mark a channel 'under-deletion' state internally and

  • Don't return this channel in the list of available channels
  • Or rather, enhance the list API to include the status of the channel (e.g., active / under deletion) so consumer not necessarily has to maintain the state and can query blockstore to invoke the continuation of deletion.
    @tock-ibm - do you have an opinion on this?

@manish-sethi why do you say so? IMO what you have done with idStore at the kvledger is the clean approach. The peer does not ask the block store to find out the list of active channels --

func (s *idStore) getActiveLedgerIDs() ([]string, error) {

If we have idStore at the orderer too, we can use it to store

  1. the last config block used for the join & under construction flag for a channel
  2. whether a channel is passed. (@tock-ibm at the peer, we can pause/resume a channel. See whether it is applicable/useful for the orderer too)
  3. whether a channel is being removed
  4. all active channels

I am not convinced that we need to add multiple internal files at the block store to manage these when it can be done cleanly using an idStore.

Moreover, ListDir() at the block store cannot be trusted in the current form. When a peer channel join command is issued, we first create all necessary DBs including a folder at the block store. However, if the peer fails before committing the genesis block, the folder at the block store is still present when the peer starts again. If the user does not issue another peer join channel, the ListDir() would return in-consistent result. Current, We have a no-op cleanup function

func (p *Provider) runCleanup(ledgerID string) error {

that can be enhanced to remove these empty folders and DBs created or we can store the genesis block itself in the idStore to manage failure but I still think that these things need to be managed at the consumer. I am not familiar with the orderer code and hence I am not sure whether orderer does a cleanup after a failure to rely on ListDir().

The orderer responds with panic if it finds a folder that it got from r.ledgerFactory.ChannelIDs() but has no blocks in it:

func (r *Registrar) Initialize(consenters map[string]consensus.Consenter) {

It expects to get a configTX for every ChannelID that r.ledgerFactory.ChannelIDs() finds.

@tock-ibm
Copy link
Contributor

The orderer uses this API:

type Factory interface {
	// GetOrCreate gets an existing ledger (if it exists) or creates it if it does not
	GetOrCreate(channelID string) (ReadWriter, error)

	// ChannelIDs returns the channel IDs the Factory is aware of
	ChannelIDs() []string

	// Remove removes block indexes and blocks for the given channelID
	Remove(channelID string) error

	// Close releases all resources acquired by the factory
	Close()
}

So from that perspective, there are two options:

Blocking
Remove is a blocking call that returns when resources are removed or marked for removal and moved elsewhere, and lazily removed in the background. When remove returns, the ChannelIDs() should not return this channel, and GetOrCreate() should succeed in creating a new empty channel. Remove() should ideally complete fast - without a dependency on the amount of storage in the channel. The lazy cleanup can take longer time. From that perspective, it would be useful also to add a Get() and a Create() to this API. The orderer generally knows when a channel exists or not, and can select the correct call.

Non-Blocking
If Remove() can return and leave a channel in an intermediate state, i.e. "pending-removal", then this needs to be reflected in the API. That is, both ChannelIDs() and GetOrCreate() (or Get(), etc) should return a special code to reflect that.

I vote for the first approach, because it is simpler and easier to work with. The latter will trigger a lot of changes in every place in the code that uses this API.

As for crash tolerance, I think this API should handle it, i.e. the one created by:
func New(directory string, metricsProvider metrics.Provider) (blockledger.Factory, error)
called from here:

func createLedgerFactory(conf *config.TopLevel, metricsProvider metrics.Provider) (blockledger.Factory, string, error) {

Comment on lines +60 to +63
// Remove removes block indexes and blocks for the given channelID
func (flf *fileLedgerFactory) Remove(channelID string) error {
return flf.blkstorageProvider.Remove(channelID)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is a channelID removed from the flf.ledgers ?
Why is this not in sync with GetOrCreate()?
I don't see anything that will prevent a user from doing GetOrCreate() while this is going on, and getting stale results.

Comment on lines +184 to +189
func (p *BlockStoreProvider) removeLedgerData(ledgerid string) error {
logger.Infof("Removing block data for channel %s", ledgerid)
if err := p.leveldbProvider.Remove(ledgerid); err != nil {
logger.Errorf("Failed to remove block index for channel %s, error: %s", ledgerid, err)
return err
}
Copy link
Contributor

@tock-ibm tock-ibm Jun 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens to this if someone calls Open(ledgerid) while this method is being executed in a go-routine? The lock is already released, but some keys that belong to the old channel are still in the db, right?

Comment on lines +190 to +193
if err := os.RemoveAll(p.conf.getLedgerBlockDir(ledgerid)); err != nil {
logger.Errorf("Failed to remove blocks for channel %s, error: %s", ledgerid, err)
return err
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it safer to first move the folder to a different name in the same parent folder, with a prefix that indicates it is about to be removed say, "~my-channel", and only then remove it? The move can be made sync, whereas the RemoveAll async. This assumes that the move is to the same file system, and therefore will use the system call rename(), which will make it atomic.

@wenjianqiao
Copy link
Contributor Author

Close this PR due to design change. New PR is #1423

@wenjianqiao wenjianqiao deleted the removechannel branch July 15, 2020 21:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants