//go:build rocksdb // +build rocksdb package rocksdb import ( "errors" "fmt" "os" "os/signal" "path/filepath" "strconv" "strings" "syscall" "time" "github.com/cometbft/cometbft/libs/log" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/server" "github.com/linxGnu/grocksdb" "github.com/spf13/cobra" "golang.org/x/exp/slices" "github.com/Kava-Labs/opendb" ) const ( flagPrintStatsInterval = "print-stats-interval" ) var allowedDBs = []string{"application", "blockstore", "state"} func CompactRocksDBCmd() *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf( "compact <%s>", strings.Join(allowedDBs, "|"), ), Short: "force compacts RocksDB", Long: `This is a utility command that performs a force compaction on the state or blockstore. This should only be run once the node has stopped.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) statsIntervalStr, err := cmd.Flags().GetString(flagPrintStatsInterval) if err != nil { return err } statsInterval, err := time.ParseDuration(statsIntervalStr) if err != nil { return fmt.Errorf("failed to parse duration for --%s: %w", flagPrintStatsInterval, err) } clientCtx := client.GetClientContextFromCmd(cmd) ctx := server.GetServerContextFromCmd(cmd) if server.GetAppDBBackend(ctx.Viper) != "rocksdb" { return errors.New("compaction is currently only supported with rocksdb") } if !slices.Contains(allowedDBs, args[0]) { return fmt.Errorf( "invalid db name, must be one of the following: %s", strings.Join(allowedDBs, ", "), ) } return compactRocksDBs(clientCtx.HomeDir, logger, args[0], statsInterval) }, } cmd.Flags().String(flagPrintStatsInterval, "1m", "duration string for how often to print compaction stats") return cmd } // compactRocksDBs performs a manual compaction on the given db. func compactRocksDBs( rootDir string, logger log.Logger, dbName string, statsInterval time.Duration, ) error { dbPath := filepath.Join(rootDir, "data", dbName+".db") dbOpts, cfOpts, err := opendb.LoadLatestOptions(dbPath) if err != nil { return err } logger.Info("opening db", "path", dbPath) db, _, err := grocksdb.OpenDbColumnFamilies( dbOpts, dbPath, []string{opendb.DefaultColumnFamilyName}, []*grocksdb.Options{cfOpts}, ) if err != nil { return err } if err != nil { logger.Error("failed to initialize cometbft db", "path", dbPath, "err", err) return fmt.Errorf("failed to open db %s %w", dbPath, err) } defer db.Close() logColumnFamilyMetadata(db, logger) logger.Info("starting compaction...", "db", dbPath) done := make(chan bool) registerSignalHandler(db, logger, done) startCompactionStatsOutput(db, logger, done, statsInterval) // Actually run the compaction db.CompactRange(grocksdb.Range{Start: nil, Limit: nil}) logger.Info("done compaction", "db", dbPath) done <- true return nil } // bytesToMB converts bytes to megabytes. func bytesToMB(bytes uint64) float64 { return float64(bytes) / 1024 / 1024 } // logColumnFamilyMetadata outputs the column family and level metadata. func logColumnFamilyMetadata( db *grocksdb.DB, logger log.Logger, ) { metadata := db.GetColumnFamilyMetadata() logger.Info( "column family metadata", "name", metadata.Name(), "sizeMB", bytesToMB(metadata.Size()), "fileCount", metadata.FileCount(), "levels", len(metadata.LevelMetas()), ) for _, level := range metadata.LevelMetas() { logger.Info( fmt.Sprintf("level %d metadata", level.Level()), "sstMetas", strconv.Itoa(len(level.SstMetas())), "sizeMB", strconv.FormatFloat(bytesToMB(level.Size()), 'f', 2, 64), ) } } // startCompactionStatsOutput starts a goroutine that outputs compaction stats // every minute. func startCompactionStatsOutput( db *grocksdb.DB, logger log.Logger, done chan bool, statsInterval time.Duration, ) { go func() { ticker := time.NewTicker(statsInterval) isClosed := false for { select { // Make sure we don't try reading from the closed db. // We continue the loop so that we can make sure the done channel // does not stall indefinitely from repeated writes and no reader. case <-done: logger.Debug("stopping compaction stats output") isClosed = true case <-ticker.C: if !isClosed { compactionStats := db.GetProperty("rocksdb.stats") fmt.Printf("%s\n", compactionStats) } } } }() } // registerSignalHandler registers a signal handler that will cancel any running // compaction when the user presses Ctrl+C. func registerSignalHandler( db *grocksdb.DB, logger log.Logger, done chan bool, ) { // https://github.com/facebook/rocksdb/wiki/RocksDB-FAQ // Q: Can I close the DB when a manual compaction is in progress? // // A: No, it's not safe to do that. However, you call // CancelAllBackgroundWork(db, true) in another thread to abort the // running compactions, so that you can close the DB sooner. Since // 6.5, you can also speed it up using // DB::DisableManualCompaction(). c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { for sig := range c { logger.Info(fmt.Sprintf( "received %s signal, aborting running compaction... Do NOT kill me before compaction is cancelled. I will exit when compaction is cancelled.", sig, )) db.DisableManualCompaction() logger.Info("manual compaction disabled") // Stop the logging done <- true } }() }