Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -964,13 +964,8 @@ func (app *App) HandleClose() error {
}
}

// Close state store (SeiDB) - critical for cleaning up background goroutines
if app.stateStore != nil {
if err := app.stateStore.Close(); err != nil {
app.Logger().Error("failed to close state store", "error", err)
errs = append(errs, fmt.Errorf("failed to close state store: %w", err))
}
}
// Note: stateStore (ssStore) is already closed by cms.Close() in BaseApp.Close()
// No need to close it again here.

if len(errs) > 0 {
return fmt.Errorf("errors during close: %v", errs)
Expand Down
13 changes: 13 additions & 0 deletions sei-db/ss/pebbledb/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ var (

type Database struct {
storage *pebble.DB
closed atomic.Bool // Set to true when Close() is called, checked by Prune()
asyncWriteWG sync.WaitGroup
config config.StateStoreConfig
// Earliest version for db after pruning
Expand Down Expand Up @@ -187,6 +188,12 @@ func New(dataDir string, config config.StateStoreConfig) (*Database, error) {
}

func (db *Database) Close() error {
// Mark as closed first to signal pruning to skip/return early
// Use CompareAndSwap to ensure Close is idempotent
if !db.closed.CompareAndSwap(false, true) {
return nil
}

// Stop background metrics collection
if db.metricsCancel != nil {
db.metricsCancel()
Expand All @@ -201,6 +208,7 @@ func (db *Database) Close() error {
_ = db.streamHandler.Close()
db.streamHandler = nil
}

var err error
if db.storage != nil {
err = db.storage.Close()
Expand Down Expand Up @@ -634,6 +642,11 @@ func (db *Database) writeAsyncInBackground() {
// it has been updated. This occurs when that module's keys are updated in between pruning runs, the node after is restarted.
// This is not a large issue given the next time that module is updated, it will be properly pruned thereafter.
func (db *Database) Prune(version int64) (_err error) {
// Check if database is closed
if db.closed.Load() {
return errors.New("pebbledb: database is closed")
}

startTime := time.Now()
defer func() {
otelMetrics.pruneLatency.Record(
Expand Down
26 changes: 26 additions & 0 deletions sei-db/ss/pebbledb/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/sei-protocol/sei-db/config"
sstest "github.com/sei-protocol/sei-db/ss/test"
"github.com/sei-protocol/sei-db/ss/types"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)

Expand All @@ -22,3 +23,28 @@ func TestStorageTestSuite(t *testing.T) {

suite.Run(t, s)
}

// TestPruneAfterClose verifies that calling Prune() after Close() returns an error
// instead of causing a panic due to nil pointer dereference.
// This is a regression test for the nil pointer panic during node shutdown.
func TestPruneAfterClose(t *testing.T) {
dir := t.TempDir()
cfg := config.DefaultStateStoreConfig()
cfg.Backend = "pebbledb"

db, err := New(dir, cfg)
require.NoError(t, err)

// Write some data
err = db.SetLatestVersion(1)
require.NoError(t, err)

// Close the database
err = db.Close()
require.NoError(t, err)

// Prune should return error, not panic
err = db.Prune(1)
require.Error(t, err)
require.Contains(t, err.Error(), "database is closed")
}
13 changes: 13 additions & 0 deletions sei-db/ss/rocksdb/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type VersionedChangesets struct {

type Database struct {
storage *grocksdb.DB
closed atomic.Bool // Set to true when Close() is called, checked by Prune()
config config.StateStoreConfig
cfHandle *grocksdb.ColumnFamilyHandle

Expand Down Expand Up @@ -293,6 +294,11 @@ func (db *Database) writeAsyncInBackground() {
// lazy prune. Future compactions will honor the increased full_history_ts_low
// and trim history when possible.
func (db *Database) Prune(version int64) error {
// Check if database is closed
if db.closed.Load() {
return fmt.Errorf("rocksdb: database is closed")
}

tsLow := version + 1 // we increment by 1 to include the provided version

var ts [TimestampSize]byte
Expand Down Expand Up @@ -494,6 +500,12 @@ func (db *Database) WriteBlockRangeHash(storeKey string, beginBlockRange, endBlo
}

func (db *Database) Close() error {
// Mark as closed first to signal pruning goroutine to stop
// Use CompareAndSwap to ensure Close is idempotent
if !db.closed.CompareAndSwap(false, true) {
return nil
}

if db.streamHandler != nil {
// Close the pending changes channel to signal the background goroutine to stop
close(db.pendingChanges)
Expand All @@ -504,6 +516,7 @@ func (db *Database) Close() error {
// Only set to nil after background goroutine has finished
db.streamHandler = nil
}

db.cfHandle = nil
if db.storage != nil {
db.storage.Close()
Expand Down
26 changes: 26 additions & 0 deletions sei-db/ss/rocksdb/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/sei-protocol/sei-db/config"
sstest "github.com/sei-protocol/sei-db/ss/test"
"github.com/sei-protocol/sei-db/ss/types"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)

Expand All @@ -25,3 +26,28 @@ func TestStorageTestSuite(t *testing.T) {

suite.Run(t, s)
}

// TestPruneAfterClose verifies that calling Prune() after Close() returns an error
// instead of causing a panic due to nil pointer dereference.
// This is a regression test for the nil pointer panic during node shutdown.
func TestPruneAfterClose(t *testing.T) {
dir := t.TempDir()
cfg := config.DefaultStateStoreConfig()
cfg.Backend = "rocksdb"

db, err := New(dir, cfg)
require.NoError(t, err)

// Write some data
err = db.SetLatestVersion(1)
require.NoError(t, err)

// Close the database
err = db.Close()
require.NoError(t, err)

// Prune should return error, not panic
err = db.Prune(1)
require.Error(t, err)
require.Contains(t, err.Error(), "database is closed")
}
Loading