Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 276
feat(store): support pruning store#2208
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
291aea5e5e80806b0cc417ca9b6782c76becd92e3e942094421c0b1345acc3a685dbe7d5b16c2350b21d7a04418a4d2fc129656c92422d008ed80b4b16db8c8f70c5132be97e1b3ffcfda2dfa1220f802ab0c7df7fcf6a84639bc934d79c299c5b3999dd5fecda2d678839aad9585715fe40d10b1b2e72561349ecca398e8eb65880c295a1a6541d0086904a680c1ba2a61d479429c9f6cc1f55File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package block | ||
| import ( | ||
| "context" | ||
| "time" | ||
| "cosmossdk.io/log" | ||
| "github.com/rollkit/rollkit/pkg/store" | ||
| ) | ||
| const DefaultFlushInterval = 10 * time.Second | ||
| // AsyncPruner is a service that periodically prunes block data in the background. | ||
| type AsyncPruner struct { | ||
| ps store.PruningStore | ||
| flushInterval time.Duration | ||
| logger log.Logger | ||
| } | ||
| func NewAsyncPruner(pruningStore store.PruningStore, flushInterval time.Duration, logger log.Logger) *AsyncPruner { | ||
| return &AsyncPruner{ | ||
| ps: pruningStore, | ||
| flushInterval: flushInterval, | ||
| logger: logger, | ||
| } | ||
| } | ||
| // Start starts the async pruner that periodically prunes block data. | ||
| func (s *AsyncPruner) Start(ctx context.Context) { | ||
| ticker := time.NewTicker(s.flushInterval) | ||
| defer ticker.Stop() | ||
| s.logger.Info("AsyncPruner started", "interval", s.flushInterval) | ||
| for { | ||
| select { | ||
| case <-ctx.Done(): | ||
| s.logger.Info("AsyncPruner stopped") | ||
| return | ||
| case <-ticker.C: | ||
| err := s.ps.PruneBlockData(ctx) | ||
| if err != nil { | ||
| s.logger.Error("Failed to prune block data", "error", err) | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -32,6 +32,11 @@ func ParseConfig(cmd *cobra.Command) (rollconf.Config, error) { | ||
| return rollconf.Config{}, fmt.Errorf("failed to load node config: %w", err) | ||
| } | ||
| if nodeConfig.Node.Pruning.Strategy != rollconf.PruningConfigStrategyCustom { | ||
Eoous marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| pruningConfig := rollconf.GetPruningConfigFromStrategy(nodeConfig.Node.Pruning.Strategy) | ||
| nodeConfig.Node.Pruning = pruningConfig | ||
| } | ||
| if err := nodeConfig.Validate(); err != nil { | ||
| return rollconf.Config{}, fmt.Errorf("failed to validate node config: %w", err) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -202,6 +202,9 @@ type NodeConfig struct { | ||
| // Header configuration | ||
| TrustedHash string `mapstructure:"trusted_hash" yaml:"trusted_hash" comment:"Initial trusted hash used to bootstrap the header exchange service. Allows nodes to start synchronizing from a specific trusted point in the chain instead of genesis. When provided, the node will fetch the corresponding header/block from peers using this hash and use it as a starting point for synchronization. If not provided, the node will attempt to fetch the genesis block instead."` | ||
| // Pruning management configuration | ||
| Pruning PruningConfig `mapstructure:"pruning" yaml:"pruning"` | ||
| } | ||
| // LogConfig contains all logging configuration parameters | ||
| @@ -243,6 +246,10 @@ func (c *Config) Validate() error { | ||
| return fmt.Errorf("could not create directory %q: %w", fullDir, err) | ||
| } | ||
| if err := c.Node.Pruning.Validate(); err != nil { | ||
| return fmt.Errorf("invalid pruning configuration: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
| @@ -286,6 +293,11 @@ func AddFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Uint64(FlagMaxPendingHeadersAndData, def.Node.MaxPendingHeadersAndData, "maximum headers or data pending DA confirmation before pausing block production (0 for no limit)") | ||
| cmd.Flags().Duration(FlagLazyBlockTime, def.Node.LazyBlockInterval.Duration, "maximum interval between blocks in lazy aggregation mode") | ||
| // Pruning configuration flags | ||
| cmd.Flags().String(FlagPruningStrategy, def.Node.Pruning.Strategy, "pruning strategy (none, default, everything, custom)") | ||
| cmd.Flags().Uint64(FlagPruningKeepRecent, def.Node.Pruning.KeepRecent, "number of recent blocks to keep") | ||
| cmd.Flags().Uint64(FlagPruningInterval, def.Node.Pruning.Interval, "frequency of pruning operations") | ||
Eoous marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| // Data Availability configuration flags | ||
| cmd.Flags().String(FlagDAAddress, def.DA.Address, "DA address (host:port)") | ||
| cmd.Flags().String(FlagDAAuthToken, def.DA.AuthToken, "DA auth token") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| package config | ||
| import ( | ||
| "errors" | ||
| ) | ||
| const ( | ||
| // Pruning configuration flags | ||
| // FlagPruningStrategy is a flag for specifying strategy for pruning block store | ||
| FlagPruningStrategy = "rollkit.node.pruning.strategy" | ||
| // FlagPruningKeepRecent is a flag for specifying how many blocks need to keep in store | ||
| FlagPruningKeepRecent = "rollkit.node.pruning.keep_recent" | ||
| // FlagPruningInterval is a flag for specifying how often prune blocks store | ||
| FlagPruningInterval = "rollkit.node.pruning.interval" | ||
| ) | ||
| const ( | ||
| PruningConfigStrategyNone = "none" | ||
| PruningConfigStrategyDefault = "default" | ||
| PruningConfigStrategyEverything = "everything" | ||
| PruningConfigStrategyCustom = "custom" | ||
| ) | ||
| var ( | ||
| PruningConfigNone = PruningConfig{ | ||
| Strategy: PruningConfigStrategyNone, | ||
| KeepRecent: 0, | ||
| Interval: 0, | ||
| } | ||
| PruningConfigDefault = PruningConfig{ | ||
| Strategy: PruningConfigStrategyDefault, | ||
| KeepRecent: 362880, | ||
| Interval: 10, | ||
| } | ||
| PruningConfigEverything = PruningConfig{ | ||
| Strategy: PruningConfigStrategyEverything, | ||
| KeepRecent: 2, | ||
| Interval: 10, | ||
| } | ||
| PruningConfigCustom = PruningConfig{ | ||
| Strategy: PruningConfigStrategyCustom, | ||
| KeepRecent: 100, | ||
| Interval: 100, | ||
| } | ||
| ) | ||
| // PruningConfig allows node operators to manage storage | ||
| type PruningConfig struct { | ||
| // todo: support volume-based strategy | ||
| Strategy string `mapstructure:"strategy" yaml:"strategy" comment:"Strategy determines the pruning approach (none, default, everything, custom)"` | ||
| KeepRecent uint64 `mapstructure:"keep_recent" yaml:"keep_recent" comment:"Number of recent blocks to keep, used in \"custom\" strategy, must be greater or equal than 2"` | ||
| Interval uint64 `mapstructure:"interval" yaml:"interval" comment:"How often the pruning process should run, used in \"custom\" strategy"` | ||
| // todo: support volume-based strategy | ||
| // VolumeConfig specifies configuration for volume-based storage | ||
| // VolumeConfig *VolumeStorageConfig `mapstructure:"volume_config" yaml:"volume_config"` | ||
| } | ||
| func (p PruningConfig) Validate() error { | ||
| // Only Custom strategy requires validation. | ||
| if p.Strategy != PruningConfigStrategyCustom { | ||
| return nil | ||
| } | ||
| if p.KeepRecent < 2 { | ||
| return errors.New("keep_recent must be greater or equal than 2 for custom pruning strategy") | ||
| } | ||
| return nil | ||
| } | ||
| func GetPruningConfigFromStrategy(strategy string) PruningConfig { | ||
| switch strategy { | ||
| case PruningConfigStrategyDefault: | ||
| return PruningConfigDefault | ||
| case PruningConfigStrategyEverything: | ||
| return PruningConfigEverything | ||
| default: | ||
| return PruningConfigNone | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,73 @@ | ||||||
| package store | ||||||
| import ( | ||||||
| "context" | ||||||
| "sync" | ||||||
| ds "github.com/ipfs/go-datastore" | ||||||
| "github.com/rollkit/rollkit/pkg/config" | ||||||
| "github.com/rollkit/rollkit/types" | ||||||
| ) | ||||||
| // DefaultPruningStore is for a store that supports pruning of block data. | ||||||
| type DefaultPruningStore struct { | ||||||
| Store | ||||||
| config config.PruningConfig | ||||||
| latestBlockDataHeight uint64 | ||||||
| mu sync.Mutex | ||||||
| } | ||||||
| var _ PruningStore = &DefaultPruningStore{} | ||||||
| func NewDefaultPruningStore(ds ds.Batching, config config.PruningConfig) PruningStore { | ||||||
| return &DefaultPruningStore{ | ||||||
| Store: &DefaultStore{db: ds}, | ||||||
| config: config, | ||||||
| } | ||||||
| } | ||||||
| // SaveBlockData saves the block data and updates the latest block data height. | ||||||
| func (s *DefaultPruningStore) SaveBlockData(ctx context.Context, header *types.SignedHeader, data *types.Data, signature *types.Signature) error { | ||||||
| err := s.Store.SaveBlockData(ctx, header, data, signature) | ||||||
| if err == nil { | ||||||
| s.mu.Lock() | ||||||
| s.latestBlockDataHeight = header.Height() | ||||||
Eoous marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||||||
| s.mu.Unlock() | ||||||
| } | ||||||
| return err | ||||||
| } | ||||||
| func (s *DefaultPruningStore) PruneBlockData(ctx context.Context) error { | ||||||
| // Skip if strategy is none. | ||||||
| if s.config.Strategy == config.PruningConfigStrategyNone { | ||||||
| return nil | ||||||
| } | ||||||
| // Read latest height after calling SaveBlockData. There is a delay between SetHeight and SaveBlockData. | ||||||
| s.mu.Lock() | ||||||
| height := s.latestBlockDataHeight | ||||||
| s.mu.Unlock() | ||||||
| // Skip if not the correct interval or latest height is less or equal than number of blocks need to keep. | ||||||
| if height%s.config.Interval != 0 || height < s.config.KeepRecent { | ||||||
| return nil | ||||||
| } | ||||||
| // Must keep at least 2 blocks(while strategy is everything). | ||||||
| endHeight := height - 1 - s.config.KeepRecent | ||||||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The calculation for If we want to keep With the current logic, To fix this,
Suggested change
| ||||||
| startHeight := uint64(0) | ||||||
| if endHeight > s.config.Interval { | ||||||
| startHeight = endHeight - s.config.Interval | ||||||
| } | ||||||
| for i := startHeight; i < endHeight; i++ { | ||||||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you need to protect against concurrent executions? Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As this is a public method, it can be called by other threads as well. It would not cost much to exit early when pruning is not completed. ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Other threads shouldn't call this method. It's only added for pruning thread Maybe currently is no need to add some protections? | ||||||
| // Could ignore for errors like not found. | ||||||
| _ = s.DeleteBlockData(ctx, i) | ||||||
| } | ||||||
Comment on lines
+67
to
+70
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Errors from Ignoring these errors can hide serious problems and leave the pruning process in an incomplete state without any notification. The error should be propagated up to the With the recommended changes to iferr:=s.DeleteBlockData(ctx, i); err!=nil {
returnerr
} | ||||||
| return nil | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -82,7 +82,7 @@ func (s *DefaultStore) SaveBlockData(ctx context.Context, header *types.SignedHe | ||
| batch, err := s.db.Batch(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to create a new batch: %w", err) | ||
| return fmt.Errorf("failed to create a new batch for saving block data: %w", err) | ||
| } | ||
| if err := batch.Put(ctx, ds.NewKey(getHeaderKey(height)), headerBlob); err != nil { | ||
| @@ -104,6 +104,31 @@ func (s *DefaultStore) SaveBlockData(ctx context.Context, header *types.SignedHe | ||
| return nil | ||
| } | ||
| // DeleteBlockData deletes block at given height. | ||
| func (s *DefaultStore) DeleteBlockData(ctx context.Context, height uint64) error { | ||
| batch, err := s.db.Batch(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to create a new batch for deleting block data: %w", err) | ||
| } | ||
| if err := batch.Delete(ctx, ds.NewKey(getHeaderKey(height))); err != nil { | ||
| return fmt.Errorf("failed to delete header blob in batch: %w", err) | ||
| } | ||
| if err := batch.Delete(ctx, ds.NewKey(getDataKey(height))); err != nil { | ||
| return fmt.Errorf("failed to delete data blob in batch: %w", err) | ||
| } | ||
| if err := batch.Delete(ctx, ds.NewKey(getSignatureKey(height))); err != nil { | ||
| return fmt.Errorf("failed to delete signature of block blob in batch: %w", err) | ||
| } | ||
| if err := batch.Commit(ctx); err != nil { | ||
| return fmt.Errorf("failed to commit batch: %w", err) | ||
| } | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how do we ensure that we delete everything? there might be more prefix stores in the future? ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Structures like block and header may need to refactor in future I think currently it's enough to be same with | ||
| return nil | ||
| } | ||
Eoous marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page.
Comment on lines
+108
to
+128
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current implementation of To fix this, I've also added handling for func (s*DefaultStore) DeleteBlockData(ctx context.Context, heightuint64) error {
// Get the header to retrieve the block hash for index deletion.header, err:=s.GetHeader(ctx, height)
iferr!=nil {
// If block not found, it might have been already pruned.iferrors.Is(err, ds.ErrNotFound) {
returnnil
}
returnfmt.Errorf("failed to get header for block %d: %w", height, err)
}
hash:=header.Hash()
batch, err:=s.db.Batch(ctx)
iferr!=nil {
returnfmt.Errorf("failed to create a new batch for deleting block data: %w", err)
}
iferr:=batch.Delete(ctx, ds.NewKey(getHeaderKey(height))); err!=nil {
returnfmt.Errorf("failed to delete header blob in batch: %w", err)
}
iferr:=batch.Delete(ctx, ds.NewKey(getDataKey(height))); err!=nil {
returnfmt.Errorf("failed to delete data blob in batch: %w", err)
}
iferr:=batch.Delete(ctx, ds.NewKey(getSignatureKey(height))); err!=nil {
returnfmt.Errorf("failed to delete signature of block blob in batch: %w", err)
}
iferr:=batch.Delete(ctx, ds.NewKey(getIndexKey(hash))); err!=nil {
returnfmt.Errorf("failed to delete index key in batch: %w", err)
}
iferr:=batch.Commit(ctx); err!=nil {
returnfmt.Errorf("failed to commit batch: %w", err)
}
returnnil
} | ||
| // TODO: We unmarshal the header and data here, but then we re-marshal them to proto to hash or send them to DA, we should not unmarshal them here and allow the caller to handle them as needed. | ||
| // GetBlockData returns block header and data at given height, or error if it's not found in Store. | ||
| func (s *DefaultStore) GetBlockData(ctx context.Context, height uint64) (*types.SignedHeader, *types.Data, error) { | ||
| header, err := s.GetHeader(ctx, height) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -16,6 +16,8 @@ type Store interface { | ||
| // SaveBlockData saves block along with its seen signature (which will be included in the next block). | ||
| SaveBlockData(ctx context.Context, header *types.SignedHeader, data *types.Data, signature *types.Signature) error | ||
| // DeleteBlockData deletes block at given height. | ||
| DeleteBlockData(ctx context.Context, height uint64) error | ||
| // GetBlockData returns block at given height, or error if it's not found in Store. | ||
| GetBlockData(ctx context.Context, height uint64) (*types.SignedHeader, *types.Data, error) | ||
| @@ -52,3 +54,9 @@ type Store interface { | ||
| // Close safely closes underlying data storage, to ensure that data is actually saved. | ||
| Close() error | ||
| } | ||
| type PruningStore interface { | ||
Eoous marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| Store | ||
| PruneBlockData(ctx context.Context) error | ||
alpe marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.