From 302783ec6ff9fe059b2d68e2b9ea8144b2e1c1a0 Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Tue, 6 Jan 2026 22:51:03 -0800 Subject: [PATCH 01/35] Refactor changelog to generic WAL --- sei-db/changelog/changelog/changelog.go | 264 ---------- sei-db/changelog/changelog/changelog_test.go | 178 ------- sei-db/db_engine/pebbledb/mvcc/db.go | 16 +- sei-db/db_engine/rocksdb/mvcc/db.go | 16 +- sei-db/state_db/sc/memiavl/db.go | 167 +++--- sei-db/state_db/sc/memiavl/db_test.go | 105 +++- sei-db/state_db/sc/memiavl/multitree.go | 4 +- sei-db/state_db/sc/memiavl/snapshot_test.go | 11 +- sei-db/state_db/sc/store.go | 61 ++- sei-db/state_db/ss/store.go | 14 +- .../cmd/seidb/operations/replay_changelog.go | 14 +- .../generic_wal}/subscriber.go | 8 +- .../changelog => wal/generic_wal}/utils.go | 13 +- sei-db/wal/generic_wal/wal.go | 280 ++++++++++ sei-db/wal/generic_wal/wal_test.go | 487 ++++++++++++++++++ sei-db/{changelog => wal}/types/types.go | 15 +- 16 files changed, 1050 insertions(+), 603 deletions(-) delete mode 100644 sei-db/changelog/changelog/changelog.go delete mode 100644 sei-db/changelog/changelog/changelog_test.go rename sei-db/{changelog/changelog => wal/generic_wal}/subscriber.go (91%) rename sei-db/{changelog/changelog => wal/generic_wal}/utils.go (94%) create mode 100644 sei-db/wal/generic_wal/wal.go create mode 100644 sei-db/wal/generic_wal/wal_test.go rename sei-db/{changelog => wal}/types/types.go (70%) diff --git a/sei-db/changelog/changelog/changelog.go b/sei-db/changelog/changelog/changelog.go deleted file mode 100644 index 7ac1951101..0000000000 --- a/sei-db/changelog/changelog/changelog.go +++ /dev/null @@ -1,264 +0,0 @@ -package changelog - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "time" - - "github.com/sei-protocol/sei-chain/sei-db/changelog/types" - errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" - "github.com/sei-protocol/sei-chain/sei-db/common/logger" - "github.com/sei-protocol/sei-chain/sei-db/proto" - "github.com/tidwall/wal" -) - -var _ types.Stream[proto.ChangelogEntry] = (*Stream)(nil) - -type Stream struct { - dir string - log *wal.Log - config Config - logger logger.Logger - writeChannel chan *Message - errSignal chan error - nextOffset uint64 - isClosed bool -} - -type Message struct { - Index uint64 - Data *proto.ChangelogEntry -} - -type Config struct { - DisableFsync bool - ZeroCopy bool - WriteBufferSize int - KeepRecent uint64 - PruneInterval time.Duration -} - -// NewStream creates a new changelog stream that persist the changesets in the log -func NewStream(logger logger.Logger, dir string, config Config) (*Stream, error) { - log, err := open(dir, &wal.Options{ - NoSync: config.DisableFsync, - NoCopy: config.ZeroCopy, - }) - if err != nil { - return nil, err - } - stream := &Stream{ - dir: dir, - log: log, - config: config, - logger: logger, - isClosed: false, - } - // Finding the nextOffset to write - lastIndex, err := log.LastIndex() - if err != nil { - return nil, err - } - stream.nextOffset = lastIndex + 1 - // Start the auto pruning goroutine - if config.KeepRecent > 0 { - go stream.StartPruning(config.KeepRecent, config.PruneInterval) - } - return stream, nil - -} - -// Write will write a new entry to the log at given index. -// Whether the writes is in blocking or async manner depends on the buffer size. -func (stream *Stream) Write(offset uint64, entry proto.ChangelogEntry) error { - channelBufferSize := stream.config.WriteBufferSize - if channelBufferSize > 0 { - if stream.writeChannel == nil { - stream.logger.Info(fmt.Sprintf("async write is enabled with buffer size %d", channelBufferSize)) - stream.startWriteGoroutine() - } - // async write - stream.writeChannel <- &Message{Index: offset, Data: &entry} - } else { - // synchronous write - bz, err := entry.Marshal() - if err != nil { - return err - } - if err := stream.log.Write(offset, bz); err != nil { - return err - } - } - return nil -} - -// WriteNextEntry will write a new entry to the last index of the log. -// Whether the writes is in blocking or async manner depends on the buffer size. -func (stream *Stream) WriteNextEntry(entry proto.ChangelogEntry) error { - nextOffset := stream.nextOffset - err := stream.Write(nextOffset, entry) - if err != nil { - return err - } - stream.nextOffset++ - return nil -} - -// startWriteGoroutine will start a goroutine to write entries to the log. -// This should only be called on initialization if async write is enabled -func (stream *Stream) startWriteGoroutine() { - stream.writeChannel = make(chan *Message, stream.config.WriteBufferSize) - stream.errSignal = make(chan error) - go func() { - batch := wal.Batch{} - defer close(stream.errSignal) - for { - entries := channelBatchRecv(stream.writeChannel) - if len(entries) == 0 { - // channel is closed - break - } - - for _, entry := range entries { - bz, err := entry.Data.Marshal() - if err != nil { - stream.errSignal <- err - return - } - batch.Write(entry.Index, bz) - } - - if err := stream.log.WriteBatch(&batch); err != nil { - stream.errSignal <- err - return - } - batch.Clear() - } - }() -} - -// TruncateAfter will remove all entries that are after the provided `index`. -// In other words the entry at `index` becomes the last entry in the log. -func (stream *Stream) TruncateAfter(index uint64) error { - return stream.log.TruncateBack(index) -} - -// TruncateBefore will remove all entries that are before the provided `index`. -// In other words the entry at `index` becomes the first entry in the log. -func (stream *Stream) TruncateBefore(index uint64) error { - return stream.log.TruncateFront(index) -} - -// CheckError check if there's any failed async writes or not -func (stream *Stream) CheckError() error { - select { - case err := <-stream.errSignal: - // async wal writing failed, we need to abort the state machine - return fmt.Errorf("async wal writing goroutine quit unexpectedly: %w", err) - default: - } - return nil -} - -func (stream *Stream) FirstOffset() (index uint64, err error) { - return stream.log.FirstIndex() -} - -// LastOffset returns the last written offset/index of the log -func (stream *Stream) LastOffset() (index uint64, err error) { - return stream.log.LastIndex() -} - -// ReadAt will read the log entry at the provided index -func (stream *Stream) ReadAt(index uint64) (*proto.ChangelogEntry, error) { - var entry = &proto.ChangelogEntry{} - bz, err := stream.log.Read(index) - if err != nil { - return entry, fmt.Errorf("read log failed, %w", err) - } - if err := entry.Unmarshal(bz); err != nil { - return entry, fmt.Errorf("unmarshal rlog failed, %w", err) - } - return entry, nil -} - -// Replay will read the replay log and process each log entry with the provided function -func (stream *Stream) Replay(start uint64, end uint64, processFn func(index uint64, entry proto.ChangelogEntry) error) error { - for i := start; i <= end; i++ { - var entry proto.ChangelogEntry - bz, err := stream.log.Read(i) - if err != nil { - return fmt.Errorf("read log failed, %w", err) - } - if err := entry.Unmarshal(bz); err != nil { - return fmt.Errorf("unmarshal rlog failed, %w", err) - } - err = processFn(i, entry) - if err != nil { - return err - } - } - return nil -} - -func (stream *Stream) StartPruning(keepRecent uint64, pruneInterval time.Duration) { - for !stream.isClosed { - lastIndex, _ := stream.log.LastIndex() - firstIndex, _ := stream.log.FirstIndex() - if lastIndex > keepRecent && (lastIndex-keepRecent) > firstIndex { - prunePos := lastIndex - keepRecent - err := stream.TruncateBefore(prunePos) - stream.logger.Error(fmt.Sprintf("failed to prune changelog till index %d", prunePos), "err", err) - } - time.Sleep(pruneInterval) - } -} - -func (stream *Stream) Close() error { - if stream.writeChannel == nil { - return nil - } - close(stream.writeChannel) - err := <-stream.errSignal - stream.writeChannel = nil - stream.errSignal = nil - errClose := stream.log.Close() - stream.isClosed = true - return errorutils.Join(err, errClose) -} - -// open opens the replay log, try to truncate the corrupted tail if there's any -func open(dir string, opts *wal.Options) (*wal.Log, error) { - if opts == nil { - opts = wal.DefaultOptions - } - rlog, err := wal.Open(dir, opts) - if errors.Is(err, wal.ErrCorrupt) { - // try to truncate corrupted tail - var fis []os.DirEntry - fis, err = os.ReadDir(dir) - if err != nil { - return nil, fmt.Errorf("read wal dir fail: %w", err) - } - var lastSeg string - for _, fi := range fis { - if fi.IsDir() || len(fi.Name()) < 20 { - continue - } - lastSeg = fi.Name() - } - - if len(lastSeg) == 0 { - return nil, err - } - if err = truncateCorruptedTail(filepath.Join(dir, lastSeg), opts.LogFormat); err != nil { - return nil, fmt.Errorf("truncate corrupted tail fail: %w", err) - } - - // try again - return wal.Open(dir, opts) - } - return rlog, err -} diff --git a/sei-db/changelog/changelog/changelog_test.go b/sei-db/changelog/changelog/changelog_test.go deleted file mode 100644 index 860b39f4b9..0000000000 --- a/sei-db/changelog/changelog/changelog_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package changelog - -import ( - "fmt" - "os" - "path/filepath" - "testing" - - "github.com/sei-protocol/sei-chain/sei-db/common/logger" - "github.com/sei-protocol/sei-chain/sei-db/proto" - iavl "github.com/sei-protocol/sei-chain/sei-iavl" - "github.com/stretchr/testify/require" - "github.com/tidwall/wal" -) - -var ( - ChangeSets = []iavl.ChangeSet{ - {Pairs: MockKVPairs("hello", "world")}, - {Pairs: MockKVPairs("hello1", "world1", "hello2", "world2")}, - {Pairs: MockKVPairs("hello3", "world3")}, - } -) - -func TestOpenAndCorruptedTail(t *testing.T) { - opts := &wal.Options{ - LogFormat: wal.JSON, - } - dir := t.TempDir() - - testCases := []struct { - name string - logs []byte - lastIndex uint64 - }{ - {"failure-1", []byte("\n"), 0}, - {"failure-2", []byte(`{}` + "\n"), 0}, - {"failure-3", []byte(`{"index":"1"}` + "\n"), 0}, - {"failure-4", []byte(`{"index":"1","data":"?"}`), 0}, - {"failure-5", []byte(`{"index":1,"data":"?"}` + "\n" + `{"index":"1","data":"?"}`), 1}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - os.WriteFile(filepath.Join(dir, "00000000000000000001"), tc.logs, 0o600) - - _, err := wal.Open(dir, opts) - require.Equal(t, wal.ErrCorrupt, err) - - log, err := open(dir, opts) - require.NoError(t, err) - - lastIndex, err := log.LastIndex() - require.NoError(t, err) - require.Equal(t, tc.lastIndex, lastIndex) - }) - } -} - -func TestReplay(t *testing.T) { - changelog := prepareTestData(t) - var total = 0 - err := changelog.Replay(1, 2, func(index uint64, entry proto.ChangelogEntry) error { - total++ - switch index { - case 1: - require.Equal(t, "test", entry.Changesets[0].Name) - require.Equal(t, []byte("hello"), entry.Changesets[0].Changeset.Pairs[0].Key) - require.Equal(t, []byte("world"), entry.Changesets[0].Changeset.Pairs[0].Value) - case 2: - require.Equal(t, []byte("hello1"), entry.Changesets[0].Changeset.Pairs[0].Key) - require.Equal(t, []byte("world1"), entry.Changesets[0].Changeset.Pairs[0].Value) - require.Equal(t, []byte("hello2"), entry.Changesets[0].Changeset.Pairs[1].Key) - require.Equal(t, []byte("world2"), entry.Changesets[0].Changeset.Pairs[1].Value) - default: - require.Fail(t, fmt.Sprintf("unexpected index %d", index)) - } - return nil - }) - require.NoError(t, err) - require.Equal(t, 2, total) - err = changelog.Close() - require.NoError(t, err) -} - -func TestRandomRead(t *testing.T) { - changelog := prepareTestData(t) - entry, err := changelog.ReadAt(2) - require.NoError(t, err) - require.Equal(t, []byte("hello1"), entry.Changesets[0].Changeset.Pairs[0].Key) - require.Equal(t, []byte("world1"), entry.Changesets[0].Changeset.Pairs[0].Value) - require.Equal(t, []byte("hello2"), entry.Changesets[0].Changeset.Pairs[1].Key) - require.Equal(t, []byte("world2"), entry.Changesets[0].Changeset.Pairs[1].Value) - entry, err = changelog.ReadAt(1) - require.NoError(t, err) - require.Equal(t, []byte("hello"), entry.Changesets[0].Changeset.Pairs[0].Key) - require.Equal(t, []byte("world"), entry.Changesets[0].Changeset.Pairs[0].Value) - entry, err = changelog.ReadAt(3) - require.NoError(t, err) - require.Equal(t, []byte("hello3"), entry.Changesets[0].Changeset.Pairs[0].Key) - require.Equal(t, []byte("world3"), entry.Changesets[0].Changeset.Pairs[0].Value) -} - -func prepareTestData(t *testing.T) *Stream { - dir := t.TempDir() - changelog, err := NewStream(logger.NewNopLogger(), dir, Config{}) - require.NoError(t, err) - writeTestData(changelog) - return changelog -} - -func writeTestData(changelog *Stream) { - for i, changes := range ChangeSets { - cs := []*proto.NamedChangeSet{ - { - Name: "test", - Changeset: changes, - }, - } - entry := &proto.ChangelogEntry{} - entry.Changesets = cs - _ = changelog.Write(uint64(i+1), *entry) - } -} - -func TestSynchronousWrite(t *testing.T) { - changelog := prepareTestData(t) - lastIndex, err := changelog.LastOffset() - require.NoError(t, err) - require.Equal(t, uint64(3), lastIndex) - -} - -func TestAsyncWrite(t *testing.T) { - dir := t.TempDir() - changelog, err := NewStream(logger.NewNopLogger(), dir, Config{WriteBufferSize: 10}) - require.NoError(t, err) - for i, changes := range ChangeSets { - cs := []*proto.NamedChangeSet{ - { - Name: "test", - Changeset: changes, - }, - } - entry := &proto.ChangelogEntry{} - entry.Changesets = cs - err := changelog.Write(uint64(i+1), *entry) - require.NoError(t, err) - } - err = changelog.Close() - require.NoError(t, err) - changelog, err = NewStream(logger.NewNopLogger(), dir, Config{WriteBufferSize: 10}) - require.NoError(t, err) - lastIndex, err := changelog.LastOffset() - require.NoError(t, err) - require.Equal(t, uint64(3), lastIndex) -} - -func TestOpenWithNilOptions(t *testing.T) { - dir := t.TempDir() - - // Test that open function handles nil options correctly - log, err := open(dir, nil) - require.NoError(t, err) - require.NotNil(t, log) - - // Verify the log is functional by checking first and last index - firstIndex, err := log.FirstIndex() - require.NoError(t, err) - require.Equal(t, uint64(0), firstIndex) - - lastIndex, err := log.LastIndex() - require.NoError(t, err) - require.Equal(t, uint64(0), lastIndex) - - // Clean up - err = log.Close() - require.NoError(t, err) -} diff --git a/sei-db/db_engine/pebbledb/mvcc/db.go b/sei-db/db_engine/pebbledb/mvcc/db.go index 27cc7e532e..301618290f 100644 --- a/sei-db/db_engine/pebbledb/mvcc/db.go +++ b/sei-db/db_engine/pebbledb/mvcc/db.go @@ -15,7 +15,6 @@ import ( "github.com/armon/go-metrics" "github.com/cockroachdb/pebble" "github.com/cockroachdb/pebble/bloom" - "github.com/sei-protocol/sei-chain/sei-db/changelog/changelog" errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/common/utils" @@ -23,6 +22,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/types" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/util" + "github.com/sei-protocol/sei-chain/sei-db/wal/generic_wal" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" "golang.org/x/exp/slices" @@ -71,7 +71,7 @@ type Database struct { storeKeyDirty sync.Map // Changelog used to support async write - streamHandler *changelog.Stream + streamHandler *generic_wal.WAL[proto.ChangelogEntry] // Pending changes to be written to the DB pendingChanges chan VersionedChangesets @@ -164,10 +164,16 @@ func OpenDB(dataDir string, config config.StateStoreConfig) (*Database, error) { if config.KeepRecent < 0 { return nil, errors.New("KeepRecent must be non-negative") } - streamHandler, _ := changelog.NewStream( + streamHandler, _ := generic_wal.NewWAL( + func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, + func(data []byte) (proto.ChangelogEntry, error) { + var e proto.ChangelogEntry + err := e.Unmarshal(data) + return e, err + }, logger.NewNopLogger(), utils.GetChangelogPath(dataDir), - changelog.Config{ + generic_wal.Config{ DisableFsync: true, ZeroCopy: true, KeepRecent: uint64(config.KeepRecent), @@ -482,7 +488,7 @@ func (db *Database) ApplyChangesetAsync(version int64, changesets []*proto.Named } entry.Changesets = changesets entry.Upgrades = nil - err := db.streamHandler.WriteNextEntry(entry) + err := db.streamHandler.Write(entry) if err != nil { return err } diff --git a/sei-db/db_engine/rocksdb/mvcc/db.go b/sei-db/db_engine/rocksdb/mvcc/db.go index 28ff0d6fd7..71ba363b35 100644 --- a/sei-db/db_engine/rocksdb/mvcc/db.go +++ b/sei-db/db_engine/rocksdb/mvcc/db.go @@ -13,7 +13,6 @@ import ( "time" "github.com/linxGnu/grocksdb" - "github.com/sei-protocol/sei-chain/sei-db/changelog/changelog" "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/common/utils" @@ -21,6 +20,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/types" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/util" + "github.com/sei-protocol/sei-chain/sei-db/wal/generic_wal" "golang.org/x/exp/slices" ) @@ -65,7 +65,7 @@ type Database struct { asyncWriteWG sync.WaitGroup // Changelog used to support async write - streamHandler *changelog.Stream + streamHandler *generic_wal.WAL[proto.ChangelogEntry] // Pending changes to be written to the DB pendingChanges chan VersionedChangesets @@ -112,10 +112,16 @@ func OpenDB(dataDir string, config config.StateStoreConfig) (*Database, error) { } database.latestVersion.Store(latestVersion) - streamHandler, _ := changelog.NewStream( + streamHandler, _ := generic_wal.NewWAL( + func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, + func(data []byte) (proto.ChangelogEntry, error) { + var e proto.ChangelogEntry + err := e.Unmarshal(data) + return e, err + }, logger.NewNopLogger(), utils.GetChangelogPath(dataDir), - changelog.Config{ + generic_wal.Config{ DisableFsync: true, ZeroCopy: true, KeepRecent: uint64(config.KeepRecent), @@ -263,7 +269,7 @@ func (db *Database) ApplyChangesetAsync(version int64, changesets []*proto.Named } entry.Changesets = changesets entry.Upgrades = nil - err := db.streamHandler.WriteNextEntry(entry) + err := db.streamHandler.Write(entry) if err != nil { return err } diff --git a/sei-db/state_db/sc/memiavl/db.go b/sei-db/state_db/sc/memiavl/db.go index 458f3db81c..ae61fd2178 100644 --- a/sei-db/state_db/sc/memiavl/db.go +++ b/sei-db/state_db/sc/memiavl/db.go @@ -8,7 +8,6 @@ import ( "os" "path/filepath" "runtime" - "sort" "strconv" "strings" "sync" @@ -18,12 +17,12 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" - "github.com/sei-protocol/sei-chain/sei-db/changelog/changelog" - "github.com/sei-protocol/sei-chain/sei-db/changelog/types" errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/common/utils" "github.com/sei-protocol/sei-chain/sei-db/proto" + "github.com/sei-protocol/sei-chain/sei-db/wal/generic_wal" + "github.com/sei-protocol/sei-chain/sei-db/wal/types" iavl "github.com/sei-protocol/sei-chain/sei-iavl" ) @@ -72,11 +71,8 @@ type DB struct { // make sure only one snapshot rewrite is running pruneSnapshotLock sync.Mutex - // the changelog stream persists all the changesets - streamHandler types.Stream[proto.ChangelogEntry] - - // pending change, will be written into rlog file in next Commit call - pendingLogEntry proto.ChangelogEntry + // the changelog stream persists all the changesets (managed by upper layer) + streamHandler types.GenericWAL[proto.ChangelogEntry] // The assumptions to concurrency: // - The methods on DB are protected by a mutex @@ -199,16 +195,27 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * tree.snapshot.leavesMap.PrepareForRandomRead() } - // Create rlog manager and open the rlog file - streamHandler, err := changelog.NewStream(logger, utils.GetChangelogPath(opts.Dir), changelog.Config{ - DisableFsync: true, - ZeroCopy: true, - WriteBufferSize: opts.AsyncCommitBuffer, - }) + // Create WAL for changelog replay and persistence + streamHandler, err := generic_wal.NewWAL( + func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, + func(data []byte) (proto.ChangelogEntry, error) { + var e proto.ChangelogEntry + err := e.Unmarshal(data) + return e, err + }, + logger, + utils.GetChangelogPath(opts.Dir), + generic_wal.Config{ + DisableFsync: true, + ZeroCopy: true, + WriteBufferSize: opts.AsyncCommitBuffer, + }, + ) if err != nil { return nil, err } + // Replay WAL to catch up to target version if targetVersion == 0 || targetVersion > mtree.Version() { logger.Info("Start catching up and replaying the MemIAVL changelog file") if err := mtree.Catchup(context.Background(), streamHandler, targetVersion); err != nil { @@ -254,6 +261,7 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * return nil, fmt.Errorf("fail to prune snapshots: %w", err) } } + // create worker pool. recv tasks to write snapshot workerPool := pond.New(opts.SnapshotWriterLimit, opts.SnapshotWriterLimit*10) @@ -291,6 +299,12 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * return db, nil } +// GetWAL returns the WAL handler for changelog operations. +// Upper layer (CommitStore) uses this to write to WAL. +func (db *DB) GetWAL() types.GenericWAL[proto.ChangelogEntry] { + return db.streamHandler +} + func removeTmpDirs(rootDir string) error { entries, err := os.ReadDir(rootDir) if err != nil { @@ -337,8 +351,7 @@ func (db *DB) SetInitialVersion(initialVersion int64) error { return initEmptyDB(db.dir, db.initialVersion.Load()) } -// ApplyUpgrades wraps MultiTree.ApplyUpgrades, it also appends the upgrades in a pending log, -// which will be persisted to the rlog in next Commit call. +// ApplyUpgrades wraps MultiTree.ApplyUpgrades to add a lock. func (db *DB) ApplyUpgrades(upgrades []*proto.TreeNameUpgrade) error { db.mtx.Lock() defer db.mtx.Unlock() @@ -347,16 +360,10 @@ func (db *DB) ApplyUpgrades(upgrades []*proto.TreeNameUpgrade) error { return errReadOnly } - if err := db.MultiTree.ApplyUpgrades(upgrades); err != nil { - return err - } - - db.pendingLogEntry.Upgrades = append(db.pendingLogEntry.Upgrades, upgrades...) - return nil + return db.MultiTree.ApplyUpgrades(upgrades) } -// ApplyChangeSets wraps MultiTree.ApplyChangeSets, it also appends the changesets in the pending log, -// which will be persisted to the rlog in next Commit call. +// ApplyChangeSets wraps MultiTree.ApplyChangeSets to add a lock. func (db *DB) ApplyChangeSets(changeSets []*proto.NamedChangeSet) (_err error) { if len(changeSets) == 0 { return nil @@ -378,16 +385,10 @@ func (db *DB) ApplyChangeSets(changeSets []*proto.NamedChangeSet) (_err error) { return errReadOnly } - if len(db.pendingLogEntry.Changesets) > 0 { - return errors.New("don't support multiple ApplyChangeSets calls in the same version") - } - db.pendingLogEntry.Changesets = changeSets - return db.MultiTree.ApplyChangeSets(changeSets) } -// ApplyChangeSet wraps MultiTree.ApplyChangeSet, it also appends the changesets in the pending log, -// which will be persisted to the rlog in next Commit call. +// ApplyChangeSet wraps MultiTree.ApplyChangeSet to add a lock. func (db *DB) ApplyChangeSet(name string, changeSet iavl.ChangeSet) error { if len(changeSet.Pairs) == 0 { return nil @@ -400,41 +401,24 @@ func (db *DB) ApplyChangeSet(name string, changeSet iavl.ChangeSet) error { return errReadOnly } - for _, cs := range db.pendingLogEntry.Changesets { - if cs.Name == name { - return errors.New("don't support multiple ApplyChangeSet calls with the same name in the same version") - } - } - - db.pendingLogEntry.Changesets = append(db.pendingLogEntry.Changesets, &proto.NamedChangeSet{ - Name: name, - Changeset: changeSet, - }) - sort.SliceStable(db.pendingLogEntry.Changesets, func(i, j int) bool { - return db.pendingLogEntry.Changesets[i].Name < db.pendingLogEntry.Changesets[j].Name - }) - return db.MultiTree.ApplyChangeSet(name, changeSet) } // checkAsyncTasks checks the status of background tasks non-blocking-ly and process the result func (db *DB) checkAsyncTasks() error { + var walErr error + if db.streamHandler != nil { + walErr = db.streamHandler.CheckError() + } return errorutils.Join( - db.streamHandler.CheckError(), + walErr, db.checkBackgroundSnapshotRewrite(), ) } -// CommittedVersion returns the latest version written in rlog file, or snapshot version if rlog is empty. -func (db *DB) CommittedVersion() (int64, error) { - lastIndex, err := db.streamHandler.LastOffset() - if err != nil { - return 0, err - } - if lastIndex == 0 { - return db.SnapshotVersion(), nil - } - return utils.IndexToVersion(lastIndex, db.initialVersion.Load()), nil +// CommittedVersion returns the current version of the MultiTree. +func (db *DB) CommittedVersion() int64 { + return db.MultiTree.Version() } // checkBackgroundSnapshotRewrite check the result of background snapshot rewrite, cleans up the old snapshots and switches to a new multitree @@ -455,22 +439,21 @@ func (db *DB) checkBackgroundSnapshotRewrite() error { return fmt.Errorf("background snapshot rewriting failed: %w", result.err) } - // wait for potential pending rlog writings to finish, to make sure we catch up to latest state. - // in real world, block execution should be slower than rlog writing, so this should not block for long. + // wait for potential pending writes to finish, to make sure we catch up to latest state. + // in real world, block execution should be slower than tree updates, so this should not block for long. for { - committedVersion, err := db.CommittedVersion() - if err != nil { - return fmt.Errorf("get committed version failed: %w", err) - } + committedVersion := db.CommittedVersion() if db.lastCommitInfo.Version == committedVersion { break } time.Sleep(time.Nanosecond) } - // catchup the remaining entries in rlog - if err := result.mtree.Catchup(context.Background(), db.streamHandler, 0); err != nil { - return fmt.Errorf("catchup failed: %w", err) + // catchup the remaining entries in rlog (only if WAL is set) + if db.streamHandler != nil { + if err := result.mtree.Catchup(context.Background(), db.streamHandler, 0); err != nil { + return fmt.Errorf("catchup failed: %w", err) + } } // do the switch @@ -526,18 +509,22 @@ func (db *DB) pruneSnapshots() { return } - // truncate Rlog until the earliest remaining snapshot - earliestVersion, err := GetEarliestVersion(db.dir) - if err != nil { - db.logger.Error("failed to find first snapshot", "err", err) - } + // truncate Rlog until the earliest remaining snapshot (only if WAL is set) + if db.streamHandler != nil { + earliestVersion, err := GetEarliestVersion(db.dir) + if err != nil { + db.logger.Error("failed to find first snapshot", "err", err) + return + } - if err := db.streamHandler.TruncateBefore(utils.VersionToIndex(earliestVersion+1, db.initialVersion.Load())); err != nil { - db.logger.Error("failed to truncate rlog", "err", err, "version", earliestVersion+1) + if err := db.streamHandler.TruncateBefore(utils.VersionToIndex(earliestVersion+1, db.initialVersion.Load())); err != nil { + db.logger.Error("failed to truncate rlog", "err", err, "version", earliestVersion+1) + } } } -// Commit wraps SaveVersion to bump the version and writes the pending changes into log files to persist on disk +// Commit wraps SaveVersion to bump the version and finalize the tree state. +// The caller (CommitStore) is responsible for writing to WAL before calling this. func (db *DB) Commit() (version int64, _err error) { startTime := time.Now() defer func() { @@ -562,17 +549,6 @@ func (db *DB) Commit() (version int64, _err error) { return 0, err } - // write to changelog - if db.streamHandler != nil { - db.pendingLogEntry.Version = v - err := db.streamHandler.Write(utils.VersionToIndex(v, db.initialVersion.Load()), db.pendingLogEntry) - if err != nil { - return 0, err - } - } - - db.pendingLogEntry = proto.ChangelogEntry{} - if err := db.checkAsyncTasks(); err != nil { return 0, err } @@ -697,10 +673,8 @@ func (db *DB) reload() error { } func (db *DB) reloadMultiTree(mtree *MultiTree) error { - // catch-up the pending changes - if err := mtree.apply(db.pendingLogEntry); err != nil { - return err - } + // The caller is responsible for ensuring mtree is caught up to the latest state + // (either via Catchup from WAL or by loading a current snapshot). return db.ReplaceWith(mtree) } @@ -810,13 +784,16 @@ func (db *DB) rewriteSnapshotBackground() error { cloned.logger.Info("loaded multitree after snapshot", "elapsed", time.Since(loadStart).Seconds()) // do a best effort catch-up, will do another final catch-up in main thread. - catchupStart := time.Now() - if err := mtree.Catchup(ctx, db.streamHandler, 0); err != nil { - cloned.logger.Error("failed to catchup after snapshot", "error", err) - ch <- snapshotResult{err: err} - return + // Only catch up if WAL is set (managed by upper layer) + if db.streamHandler != nil { + catchupStart := time.Now() + if err := mtree.Catchup(ctx, db.streamHandler, 0); err != nil { + cloned.logger.Error("failed to catchup after snapshot", "error", err) + ch <- snapshotResult{err: err} + return + } + cloned.logger.Info("finished best-effort catchup", "version", cloned.Version(), "latest", mtree.Version(), "elapsed", time.Since(catchupStart).Seconds()) } - cloned.logger.Info("finished best-effort catchup", "version", cloned.Version(), "latest", mtree.Version(), "elapsed", time.Since(catchupStart).Seconds()) ch <- snapshotResult{mtree: mtree} totalElapsed := time.Since(startTime).Seconds() @@ -1137,7 +1114,7 @@ func GetLatestVersion(dir string) (int64, error) { } return 0, err } - lastIndex, err := changelog.GetLastIndex(changelog.LogPath(dir)) + lastIndex, err := generic_wal.GetLastIndex(generic_wal.LogPath(dir)) if err != nil { return 0, err } diff --git a/sei-db/state_db/sc/memiavl/db_test.go b/sei-db/state_db/sc/memiavl/db_test.go index 07e477ff58..0064ece708 100644 --- a/sei-db/state_db/sc/memiavl/db_test.go +++ b/sei-db/state_db/sc/memiavl/db_test.go @@ -19,6 +19,31 @@ import ( "github.com/stretchr/testify/require" ) +// writeToWAL is a test helper that writes pending changes to WAL. +// In production, CommitStore handles this. Tests that need WAL replay use this helper. +func writeToWAL(t *testing.T, db *DB, changesets []*proto.NamedChangeSet, upgrades []*proto.TreeNameUpgrade) { + t.Helper() + wal := db.GetWAL() + if wal == nil { + return + } + entry := proto.ChangelogEntry{ + Version: db.WorkingCommitInfo().Version, + Changesets: changesets, + Upgrades: upgrades, + } + require.NoError(t, wal.Write(entry)) +} + +// initialUpgrades converts InitialStores names to TreeNameUpgrade slice for WAL writing. +func initialUpgrades(stores []string) []*proto.TreeNameUpgrade { + var upgrades []*proto.TreeNameUpgrade + for _, name := range stores { + upgrades = append(upgrades, &proto.TreeNameUpgrade{Name: name}) + } + return upgrades +} + func TestRewriteSnapshot(t *testing.T) { db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: t.TempDir(), @@ -229,14 +254,15 @@ func TestSnapshotTriggerOnIntervalDiff(t *testing.T) { func TestRlog(t *testing.T) { dir := t.TempDir() + initialStores := []string{"test", "delete"} db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: dir, CreateIfMissing: true, - InitialStores: []string{"test", "delete"}, + InitialStores: initialStores, }) require.NoError(t, err) - for _, changes := range ChangeSets { + for i, changes := range ChangeSets { cs := []*proto.NamedChangeSet{ { Name: "test", @@ -244,13 +270,19 @@ func TestRlog(t *testing.T) { }, } require.NoError(t, db.ApplyChangeSets(cs)) + // First WAL entry must include initial upgrades for replay to work + if i == 0 { + writeToWAL(t, db, cs, initialUpgrades(initialStores)) + } else { + writeToWAL(t, db, cs, nil) + } _, err := db.Commit() require.NoError(t, err) } require.Equal(t, 2, len(db.lastCommitInfo.StoreInfos)) - require.NoError(t, db.ApplyUpgrades([]*proto.TreeNameUpgrade{ + upgrades := []*proto.TreeNameUpgrade{ { Name: "newtest", RenameFrom: "test", @@ -259,7 +291,9 @@ func TestRlog(t *testing.T) { Name: "delete", Delete: true, }, - })) + } + require.NoError(t, db.ApplyUpgrades(upgrades)) + writeToWAL(t, db, nil, upgrades) _, err = db.Commit() require.NoError(t, err) @@ -296,14 +330,18 @@ func TestInitialVersion(t *testing.T) { value := "world" for _, initialVersion := range []int64{0, 1, 100} { dir := t.TempDir() + initialStores := []string{name} db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: dir, CreateIfMissing: true, - InitialStores: []string{name}, + InitialStores: initialStores, }) require.NoError(t, err) db.SetInitialVersion(initialVersion) - require.NoError(t, db.ApplyChangeSets(mockNameChangeSet(name, key, value))) + cs1 := mockNameChangeSet(name, key, value) + require.NoError(t, db.ApplyChangeSets(cs1)) + // First WAL entry must include initial upgrades for replay to work + writeToWAL(t, db, cs1, initialUpgrades(initialStores)) v, err := db.Commit() require.NoError(t, err) if initialVersion <= 1 { @@ -313,7 +351,9 @@ func TestInitialVersion(t *testing.T) { } hash := db.LastCommitInfo().StoreInfos[0].CommitId.Hash require.Equal(t, "6032661ab0d201132db7a8fa1da6a0afe427e6278bd122c301197680ab79ca02", hex.EncodeToString(hash)) - require.NoError(t, db.ApplyChangeSets(mockNameChangeSet(name, key, "world1"))) + cs2 := mockNameChangeSet(name, key, "world1") + require.NoError(t, db.ApplyChangeSets(cs2)) + writeToWAL(t, db, cs2, nil) v, err = db.Commit() require.NoError(t, err) hash = db.LastCommitInfo().StoreInfos[0].CommitId.Hash @@ -333,8 +373,11 @@ func TestInitialVersion(t *testing.T) { require.Equal(t, v, db.Version()) require.Equal(t, hex.EncodeToString(hash), hex.EncodeToString(db.LastCommitInfo().StoreInfos[0].CommitId.Hash)) - db.ApplyUpgrades([]*proto.TreeNameUpgrade{{Name: name1}}) - require.NoError(t, db.ApplyChangeSets(mockNameChangeSet(name1, key, value))) + upgrades1 := []*proto.TreeNameUpgrade{{Name: name1}} + db.ApplyUpgrades(upgrades1) + cs3 := mockNameChangeSet(name1, key, value) + require.NoError(t, db.ApplyChangeSets(cs3)) + writeToWAL(t, db, cs3, upgrades1) v, err = db.Commit() require.NoError(t, err) if initialVersion <= 1 { @@ -353,8 +396,11 @@ func TestInitialVersion(t *testing.T) { require.NoError(t, db.RewriteSnapshot(context.Background())) require.NoError(t, db.Reload()) - db.ApplyUpgrades([]*proto.TreeNameUpgrade{{Name: name2}}) - require.NoError(t, db.ApplyChangeSets(mockNameChangeSet(name2, key, value))) + upgrades2 := []*proto.TreeNameUpgrade{{Name: name2}} + db.ApplyUpgrades(upgrades2) + cs4 := mockNameChangeSet(name2, key, value) + require.NoError(t, db.ApplyChangeSets(cs4)) + writeToWAL(t, db, cs4, upgrades2) v, err = db.Commit() require.NoError(t, err) if initialVersion <= 1 { @@ -372,10 +418,11 @@ func TestInitialVersion(t *testing.T) { func TestLoadVersion(t *testing.T) { dir := t.TempDir() + initialStores := []string{"test"} db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: dir, CreateIfMissing: true, - InitialStores: []string{"test"}, + InitialStores: initialStores, }) require.NoError(t, err) @@ -392,6 +439,12 @@ func TestLoadVersion(t *testing.T) { // check the root hash require.Equal(t, RefHashes[db.Version()], db.WorkingCommitInfo().StoreInfos[0].CommitId.Hash) + // First WAL entry must include initial upgrades for replay to work + if i == 0 { + writeToWAL(t, db, cs, initialUpgrades(initialStores)) + } else { + writeToWAL(t, db, cs, nil) + } _, err := db.Commit() require.NoError(t, err) }) @@ -483,15 +536,16 @@ func TestRlogIndexConversion(t *testing.T) { func TestEmptyValue(t *testing.T) { dir := t.TempDir() + initialStores := []string{"test"} db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: dir, - InitialStores: []string{"test"}, + InitialStores: initialStores, CreateIfMissing: true, ZeroCopy: true, }) require.NoError(t, err) - require.NoError(t, db.ApplyChangeSets([]*proto.NamedChangeSet{ + cs1 := []*proto.NamedChangeSet{ {Name: "test", Changeset: iavl.ChangeSet{ Pairs: []*iavl.KVPair{ {Key: []byte("hello1"), Value: []byte("")}, @@ -499,15 +553,20 @@ func TestEmptyValue(t *testing.T) { {Key: []byte("hello3"), Value: []byte("")}, }, }}, - })) + } + require.NoError(t, db.ApplyChangeSets(cs1)) + // First WAL entry must include initial upgrades for replay to work + writeToWAL(t, db, cs1, initialUpgrades(initialStores)) _, err = db.Commit() require.NoError(t, err) - require.NoError(t, db.ApplyChangeSets([]*proto.NamedChangeSet{ + cs2 := []*proto.NamedChangeSet{ {Name: "test", Changeset: iavl.ChangeSet{ Pairs: []*iavl.KVPair{{Key: []byte("hello1"), Delete: true}}, }}, - })) + } + require.NoError(t, db.ApplyChangeSets(cs2)) + writeToWAL(t, db, cs2, nil) version, err := db.Commit() require.NoError(t, err) @@ -623,14 +682,16 @@ func TestRepeatedApplyChangeSet(t *testing.T) { }) require.NoError(t, err) + // Note: Multiple ApplyChangeSets calls are now allowed at DB level. + // The "one changeset per tree per version" validation is enforced by CommitStore. err = db.ApplyChangeSets([]*proto.NamedChangeSet{{Name: "test1"}}) - require.Error(t, err) + require.NoError(t, err) err = db.ApplyChangeSet("test1", iavl.ChangeSet{ Pairs: []*iavl.KVPair{ {Key: []byte("hello2"), Value: []byte("world2")}, }, }) - require.Error(t, err) + require.NoError(t, err) _, err = db.Commit() require.NoError(t, err) @@ -648,18 +709,20 @@ func TestRepeatedApplyChangeSet(t *testing.T) { }) require.NoError(t, err) + // Note: At DB level, multiple ApplyChangeSet calls with the same tree name are now allowed. + // The "one changeset per tree per version" validation is enforced by CommitStore. err = db.ApplyChangeSet("test1", iavl.ChangeSet{ Pairs: []*iavl.KVPair{ {Key: []byte("hello2"), Value: []byte("world2")}, }, }) - require.Error(t, err) + require.NoError(t, err) err = db.ApplyChangeSet("test2", iavl.ChangeSet{ Pairs: []*iavl.KVPair{ {Key: []byte("hello2"), Value: []byte("world2")}, }, }) - require.Error(t, err) + require.NoError(t, err) } func TestLoadMultiTreeWithCancelledContext(t *testing.T) { diff --git a/sei-db/state_db/sc/memiavl/multitree.go b/sei-db/state_db/sc/memiavl/multitree.go index b12d5a20da..0498b8fccf 100644 --- a/sei-db/state_db/sc/memiavl/multitree.go +++ b/sei-db/state_db/sc/memiavl/multitree.go @@ -12,11 +12,11 @@ import ( "time" "github.com/alitto/pond" - "github.com/sei-protocol/sei-chain/sei-db/changelog/types" "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/common/utils" "github.com/sei-protocol/sei-chain/sei-db/proto" + "github.com/sei-protocol/sei-chain/sei-db/wal/types" iavl "github.com/sei-protocol/sei-chain/sei-iavl" "golang.org/x/exp/slices" ) @@ -355,7 +355,7 @@ func (t *MultiTree) UpdateCommitInfo() { } // Catchup replay the new entries in the Rlog file on the tree to catch up to the target or latest version. -func (t *MultiTree) Catchup(ctx context.Context, stream types.Stream[proto.ChangelogEntry], endVersion int64) error { +func (t *MultiTree) Catchup(ctx context.Context, stream types.GenericWAL[proto.ChangelogEntry], endVersion int64) error { startTime := time.Now() lastIndex, err := stream.LastOffset() if err != nil { diff --git a/sei-db/state_db/sc/memiavl/snapshot_test.go b/sei-db/state_db/sc/memiavl/snapshot_test.go index 7db9ea1738..33db34dbfb 100644 --- a/sei-db/state_db/sc/memiavl/snapshot_test.go +++ b/sei-db/state_db/sc/memiavl/snapshot_test.go @@ -141,15 +141,16 @@ func TestSnapshotImportExport(t *testing.T) { } func TestDBSnapshotRestore(t *testing.T) { + initialStores := []string{"test", "test2"} db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: t.TempDir(), CreateIfMissing: true, - InitialStores: []string{"test", "test2"}, + InitialStores: initialStores, AsyncCommitBuffer: -1, }) require.NoError(t, err) - for _, changes := range ChangeSets { + for i, changes := range ChangeSets { cs := []*proto.NamedChangeSet{ { Name: "test", @@ -161,6 +162,12 @@ func TestDBSnapshotRestore(t *testing.T) { }, } require.NoError(t, db.ApplyChangeSets(cs)) + // First WAL entry must include initial upgrades for replay to work + if i == 0 { + writeToWAL(t, db, cs, initialUpgrades(initialStores)) + } else { + writeToWAL(t, db, cs, nil) + } _, err := db.Commit() require.NoError(t, err) testSnapshotRoundTrip(t, db) diff --git a/sei-db/state_db/sc/store.go b/sei-db/state_db/sc/store.go index 60127fb0c3..0a159a0b32 100644 --- a/sei-db/state_db/sc/store.go +++ b/sei-db/state_db/sc/store.go @@ -19,6 +19,9 @@ type CommitStore struct { logger logger.Logger db *memiavl.DB opts memiavl.Options + + // pending changes to be written to WAL on next Commit + pendingLogEntry proto.ChangelogEntry } func NewCommitStore(homeDir string, logger logger.Logger, config config.StateCommitConfig) *CommitStore { @@ -96,6 +99,26 @@ func (cs *CommitStore) LoadVersion(targetVersion int64, copyExisting bool) (type } func (cs *CommitStore) Commit() (int64, error) { + // Get the next version that will be committed + nextVersion := cs.db.WorkingCommitInfo().Version + + // Write to WAL first (ensures durability before tree commit) + wal := cs.db.GetWAL() + if wal != nil { + cs.pendingLogEntry.Version = nextVersion + if err := wal.Write(cs.pendingLogEntry); err != nil { + return 0, fmt.Errorf("failed to write to WAL: %w", err) + } + // Check for async write errors + if err := wal.CheckError(); err != nil { + return 0, fmt.Errorf("WAL async write error: %w", err) + } + } + + // Clear pending entry + cs.pendingLogEntry = proto.ChangelogEntry{} + + // Now commit to the tree return cs.db.Commit() } @@ -112,10 +135,26 @@ func (cs *CommitStore) GetEarliestVersion() (int64, error) { } func (cs *CommitStore) ApplyChangeSets(changesets []*proto.NamedChangeSet) error { + if len(changesets) == 0 { + return nil + } + + // Store in pending log entry for WAL + cs.pendingLogEntry.Changesets = changesets + + // Apply to tree return cs.db.ApplyChangeSets(changesets) } func (cs *CommitStore) ApplyUpgrades(upgrades []*proto.TreeNameUpgrade) error { + if len(upgrades) == 0 { + return nil + } + + // Store in pending log entry for WAL + cs.pendingLogEntry.Upgrades = append(cs.pendingLogEntry.Upgrades, upgrades...) + + // Apply to tree return cs.db.ApplyUpgrades(upgrades) } @@ -135,27 +174,21 @@ func (cs *CommitStore) Exporter(version int64) (types.Exporter, error) { if version < 0 || version > math.MaxUint32 { return nil, fmt.Errorf("version %d out of range", version) } - - exporter, err := memiavl.NewMultiTreeExporter(cs.opts.Dir, uint32(version), cs.opts.OnlyAllowExportOnSnapshotVersion) - if err != nil { - return nil, err - } - return exporter, nil + return memiavl.NewMultiTreeExporter(cs.opts.Dir, uint32(version), cs.opts.OnlyAllowExportOnSnapshotVersion) } func (cs *CommitStore) Importer(version int64) (types.Importer, error) { - if version < 0 || version > math.MaxUint32 { return nil, fmt.Errorf("version %d out of range", version) } - - treeImporter, err := memiavl.NewMultiTreeImporter(cs.opts.Dir, uint64(version)) - if err != nil { - return nil, err - } - return treeImporter, nil + return memiavl.NewMultiTreeImporter(cs.opts.Dir, uint64(version)) } func (cs *CommitStore) Close() error { - return cs.db.Close() + if cs.db != nil { + err := cs.db.Close() + cs.db = nil + return err + } + return nil } diff --git a/sei-db/state_db/ss/store.go b/sei-db/state_db/ss/store.go index 26a2cba9ac..0f884385bc 100644 --- a/sei-db/state_db/ss/store.go +++ b/sei-db/state_db/ss/store.go @@ -3,13 +3,13 @@ package ss import ( "fmt" - "github.com/sei-protocol/sei-chain/sei-db/changelog/changelog" "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/common/utils" "github.com/sei-protocol/sei-chain/sei-db/config" "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/pruning" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/types" + "github.com/sei-protocol/sei-chain/sei-db/wal/generic_wal" ) type BackendType string @@ -61,7 +61,17 @@ func NewStateStore(logger logger.Logger, homeDir string, ssConfig config.StateSt func RecoverStateStore(logger logger.Logger, changelogPath string, stateStore types.StateStore) error { ssLatestVersion := stateStore.GetLatestVersion() logger.Info(fmt.Sprintf("Recovering from changelog %s with latest SS version %d", changelogPath, ssLatestVersion)) - streamHandler, err := changelog.NewStream(logger, changelogPath, changelog.Config{}) + streamHandler, err := generic_wal.NewWAL( + func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, + func(data []byte) (proto.ChangelogEntry, error) { + var e proto.ChangelogEntry + err := e.Unmarshal(data) + return e, err + }, + logger, + changelogPath, + generic_wal.Config{}, + ) if err != nil { return err } diff --git a/sei-db/tools/cmd/seidb/operations/replay_changelog.go b/sei-db/tools/cmd/seidb/operations/replay_changelog.go index 77d3c89ac9..8719a53fe8 100644 --- a/sei-db/tools/cmd/seidb/operations/replay_changelog.go +++ b/sei-db/tools/cmd/seidb/operations/replay_changelog.go @@ -4,12 +4,12 @@ import ( "fmt" "path/filepath" - "github.com/sei-protocol/sei-chain/sei-db/changelog/changelog" "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/config" "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/types" + "github.com/sei-protocol/sei-chain/sei-db/wal/generic_wal" "github.com/spf13/cobra" ) @@ -41,7 +41,17 @@ func executeReplayChangelog(cmd *cobra.Command, _ []string) { } logDir := filepath.Join(dbDir, "changelog") - stream, err := changelog.NewStream(logger.NewNopLogger(), logDir, changelog.Config{}) + stream, err := generic_wal.NewWAL( + func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, + func(data []byte) (proto.ChangelogEntry, error) { + var e proto.ChangelogEntry + err := e.Unmarshal(data) + return e, err + }, + logger.NewNopLogger(), + logDir, + generic_wal.Config{}, + ) if err != nil { panic(err) } diff --git a/sei-db/changelog/changelog/subscriber.go b/sei-db/wal/generic_wal/subscriber.go similarity index 91% rename from sei-db/changelog/changelog/subscriber.go rename to sei-db/wal/generic_wal/subscriber.go index b8fe896303..10683259ad 100644 --- a/sei-db/changelog/changelog/subscriber.go +++ b/sei-db/wal/generic_wal/subscriber.go @@ -1,10 +1,10 @@ -package changelog +package generic_wal import ( "fmt" - "github.com/sei-protocol/sei-chain/sei-db/changelog/types" "github.com/sei-protocol/sei-chain/sei-db/proto" + "github.com/sei-protocol/sei-chain/sei-db/wal/types" ) var _ types.Subscriber[proto.ChangelogEntry] = (*Subscriber)(nil) @@ -47,6 +47,7 @@ func (s *Subscriber) startAsyncProcessing() { if s.chPendingEntries == nil { s.chPendingEntries = make(chan proto.ChangelogEntry, s.maxPendingSize) s.errSignal = make(chan error) + s.stopSignal = make(chan struct{}) go func() { defer close(s.errSignal) for { @@ -64,7 +65,7 @@ func (s *Subscriber) startAsyncProcessing() { } func (s *Subscriber) Close() error { - if s.chPendingEntries != nil { + if s.chPendingEntries == nil { return nil } s.stopSignal <- struct{}{} @@ -72,6 +73,7 @@ func (s *Subscriber) Close() error { err := s.CheckError() s.chPendingEntries = nil s.errSignal = nil + s.stopSignal = nil return err } diff --git a/sei-db/changelog/changelog/utils.go b/sei-db/wal/generic_wal/utils.go similarity index 94% rename from sei-db/changelog/changelog/utils.go rename to sei-db/wal/generic_wal/utils.go index 2500e57dd1..f98810ac3b 100644 --- a/sei-db/changelog/changelog/utils.go +++ b/sei-db/wal/generic_wal/utils.go @@ -1,4 +1,4 @@ -package changelog +package generic_wal import ( "bytes" @@ -9,9 +9,10 @@ import ( "path/filepath" "unsafe" - iavl "github.com/sei-protocol/sei-chain/sei-iavl" "github.com/tidwall/gjson" "github.com/tidwall/wal" + + iavl "github.com/sei-protocol/sei-chain/sei-iavl" ) func LogPath(dir string) string { @@ -90,16 +91,16 @@ func loadNextBinaryEntry(data []byte) (n int, err error) { return n + size, nil } -func channelBatchRecv[T any](ch <-chan *T) []*T { +func channelBatchRecv[T any](ch <-chan T) []T { // block if channel is empty - item := <-ch - if item == nil { + item, ok := <-ch + if !ok { // channel is closed return nil } remaining := len(ch) - result := make([]*T, 0, remaining+1) + result := make([]T, 0, remaining+1) result = append(result, item) for i := 0; i < remaining; i++ { result = append(result, <-ch) diff --git a/sei-db/wal/generic_wal/wal.go b/sei-db/wal/generic_wal/wal.go new file mode 100644 index 0000000000..ef8799d7bd --- /dev/null +++ b/sei-db/wal/generic_wal/wal.go @@ -0,0 +1,280 @@ +package generic_wal + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/tidwall/wal" + + errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" + "github.com/sei-protocol/sei-chain/sei-db/common/logger" + "github.com/sei-protocol/sei-chain/sei-db/wal/types" +) + +// WAL is a generic write-ahead log implementation. +type WAL[T any] struct { + dir string + log *wal.Log + config Config + logger logger.Logger + marshal types.MarshalFn[T] + unmarshal types.UnmarshalFn[T] + writeChannel chan T + errSignal chan error + nextOffset uint64 + isClosed bool +} + +type Config struct { + DisableFsync bool + ZeroCopy bool + WriteBufferSize int + KeepRecent uint64 + PruneInterval time.Duration +} + +// NewWAL creates a new generic write-ahead log that persists entries. +// marshal and unmarshal functions are used to serialize/deserialize entries. +// Example: +// +// NewWAL( +// func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, +// func(data []byte) (proto.ChangelogEntry, error) { +// var e proto.ChangelogEntry +// err := e.Unmarshal(data) +// return e, err +// }, +// logger, dir, config, +// ) +func NewWAL[T any]( + marshal types.MarshalFn[T], + unmarshal types.UnmarshalFn[T], + logger logger.Logger, + dir string, + config Config, +) (*WAL[T], error) { + log, err := open(dir, &wal.Options{ + NoSync: config.DisableFsync, + NoCopy: config.ZeroCopy, + }) + if err != nil { + return nil, err + } + w := &WAL[T]{ + dir: dir, + log: log, + config: config, + logger: logger, + marshal: marshal, + unmarshal: unmarshal, + isClosed: false, + } + // Finding the nextOffset to write + lastIndex, err := log.LastIndex() + if err != nil { + return nil, err + } + w.nextOffset = lastIndex + 1 + // Start the auto pruning goroutine + if config.KeepRecent > 0 { + go w.StartPruning(config.KeepRecent, config.PruneInterval) + } + return w, nil + +} + +// Write will append a new entry to the end of the log. +// Whether the writes is in blocking or async manner depends on the buffer size. +func (walLog *WAL[T]) Write(entry T) error { + channelBufferSize := walLog.config.WriteBufferSize + if channelBufferSize > 0 { + if walLog.writeChannel == nil { + walLog.logger.Info(fmt.Sprintf("async write is enabled with buffer size %d", channelBufferSize)) + walLog.startWriteGoroutine() + } + // async write + walLog.writeChannel <- entry + walLog.nextOffset++ + } else { + // synchronous write + bz, err := walLog.marshal(entry) + if err != nil { + return err + } + if err := walLog.log.Write(walLog.nextOffset, bz); err != nil { + return err + } + walLog.nextOffset++ + } + return nil +} + +// startWriteGoroutine will start a goroutine to write entries to the log. +// This should only be called on initialization if async write is enabled +func (walLog *WAL[T]) startWriteGoroutine() { + walLog.writeChannel = make(chan T, walLog.config.WriteBufferSize) + walLog.errSignal = make(chan error) + // Capture the starting offset for the goroutine + writeOffset := walLog.nextOffset + go func() { + batch := wal.Batch{} + defer close(walLog.errSignal) + for { + entries := channelBatchRecv(walLog.writeChannel) + if len(entries) == 0 { + // channel is closed + break + } + + for _, entry := range entries { + bz, err := walLog.marshal(entry) + if err != nil { + walLog.errSignal <- err + return + } + batch.Write(writeOffset, bz) + writeOffset++ + } + + if err := walLog.log.WriteBatch(&batch); err != nil { + walLog.errSignal <- err + return + } + batch.Clear() + } + }() +} + +// TruncateAfter will remove all entries that are after the provided `index`. +// In other words the entry at `index` becomes the last entry in the log. +func (walLog *WAL[T]) TruncateAfter(index uint64) error { + if err := walLog.log.TruncateBack(index); err != nil { + return err + } + // Update nextOffset to reflect the new end of log + walLog.nextOffset = index + 1 + return nil +} + +// TruncateBefore will remove all entries that are before the provided `index`. +// In other words the entry at `index` becomes the first entry in the log. +func (walLog *WAL[T]) TruncateBefore(index uint64) error { + return walLog.log.TruncateFront(index) +} + +// CheckError check if there's any failed async writes or not +func (walLog *WAL[T]) CheckError() error { + select { + case err := <-walLog.errSignal: + // async wal writing failed, we need to abort the state machine + return fmt.Errorf("async wal writing goroutine quit unexpectedly: %w", err) + default: + } + return nil +} + +func (walLog *WAL[T]) FirstOffset() (index uint64, err error) { + return walLog.log.FirstIndex() +} + +// LastOffset returns the last written offset/index of the log +func (walLog *WAL[T]) LastOffset() (index uint64, err error) { + return walLog.log.LastIndex() +} + +// ReadAt will read the log entry at the provided index +func (walLog *WAL[T]) ReadAt(index uint64) (T, error) { + var zero T + bz, err := walLog.log.Read(index) + if err != nil { + return zero, fmt.Errorf("read log failed, %w", err) + } + entry, err := walLog.unmarshal(bz) + if err != nil { + return zero, fmt.Errorf("unmarshal rlog failed, %w", err) + } + return entry, nil +} + +// Replay will read the replay log and process each log entry with the provided function +func (walLog *WAL[T]) Replay(start uint64, end uint64, processFn func(index uint64, entry T) error) error { + for i := start; i <= end; i++ { + bz, err := walLog.log.Read(i) + if err != nil { + return fmt.Errorf("read log failed, %w", err) + } + entry, err := walLog.unmarshal(bz) + if err != nil { + return fmt.Errorf("unmarshal rlog failed, %w", err) + } + err = processFn(i, entry) + if err != nil { + return err + } + } + return nil +} + +func (walLog *WAL[T]) StartPruning(keepRecent uint64, pruneInterval time.Duration) { + for !walLog.isClosed { + lastIndex, _ := walLog.log.LastIndex() + firstIndex, _ := walLog.log.FirstIndex() + if lastIndex > keepRecent && (lastIndex-keepRecent) > firstIndex { + prunePos := lastIndex - keepRecent + if err := walLog.TruncateBefore(prunePos); err != nil { + walLog.logger.Error(fmt.Sprintf("failed to prune changelog till index %d", prunePos), "err", err) + } + } + time.Sleep(pruneInterval) + } +} + +func (walLog *WAL[T]) Close() error { + walLog.isClosed = true + var err error + if walLog.writeChannel != nil { + close(walLog.writeChannel) + err = <-walLog.errSignal + walLog.writeChannel = nil + walLog.errSignal = nil + } + errClose := walLog.log.Close() + return errorutils.Join(err, errClose) +} + +// open opens the replay log, try to truncate the corrupted tail if there's any +func open(dir string, opts *wal.Options) (*wal.Log, error) { + if opts == nil { + opts = wal.DefaultOptions + } + rlog, err := wal.Open(dir, opts) + if errors.Is(err, wal.ErrCorrupt) { + // try to truncate corrupted tail + var fis []os.DirEntry + fis, err = os.ReadDir(dir) + if err != nil { + return nil, fmt.Errorf("read wal dir fail: %w", err) + } + var lastSeg string + for _, fi := range fis { + if fi.IsDir() || len(fi.Name()) < 20 { + continue + } + lastSeg = fi.Name() + } + + if len(lastSeg) == 0 { + return nil, err + } + if err = truncateCorruptedTail(filepath.Join(dir, lastSeg), opts.LogFormat); err != nil { + return nil, fmt.Errorf("truncate corrupted tail fail: %w", err) + } + + // try again + return wal.Open(dir, opts) + } + return rlog, err +} diff --git a/sei-db/wal/generic_wal/wal_test.go b/sei-db/wal/generic_wal/wal_test.go new file mode 100644 index 0000000000..7ec81be1ff --- /dev/null +++ b/sei-db/wal/generic_wal/wal_test.go @@ -0,0 +1,487 @@ +package generic_wal + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/sei-protocol/sei-chain/sei-db/common/logger" + "github.com/sei-protocol/sei-chain/sei-db/proto" + iavl "github.com/sei-protocol/sei-chain/sei-iavl" + "github.com/stretchr/testify/require" + "github.com/tidwall/wal" +) + +var ( + ChangeSets = []iavl.ChangeSet{ + {Pairs: MockKVPairs("hello", "world")}, + {Pairs: MockKVPairs("hello1", "world1", "hello2", "world2")}, + {Pairs: MockKVPairs("hello3", "world3")}, + } + + // marshal/unmarshal functions for testing + marshalEntry = func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() } + unmarshalEntry = func(data []byte) (proto.ChangelogEntry, error) { + var e proto.ChangelogEntry + err := e.Unmarshal(data) + return e, err + } +) + +func TestOpenAndCorruptedTail(t *testing.T) { + opts := &wal.Options{ + LogFormat: wal.JSON, + } + dir := t.TempDir() + + testCases := []struct { + name string + logs []byte + lastIndex uint64 + }{ + {"failure-1", []byte("\n"), 0}, + {"failure-2", []byte(`{}` + "\n"), 0}, + {"failure-3", []byte(`{"index":"1"}` + "\n"), 0}, + {"failure-4", []byte(`{"index":"1","data":"?"}`), 0}, + {"failure-5", []byte(`{"index":1,"data":"?"}` + "\n" + `{"index":"1","data":"?"}`), 1}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + os.WriteFile(filepath.Join(dir, "00000000000000000001"), tc.logs, 0o600) + + _, err := wal.Open(dir, opts) + require.Equal(t, wal.ErrCorrupt, err) + + log, err := open(dir, opts) + require.NoError(t, err) + + lastIndex, err := log.LastIndex() + require.NoError(t, err) + require.Equal(t, tc.lastIndex, lastIndex) + }) + } +} + +func TestReplay(t *testing.T) { + changelog := prepareTestData(t) + var total = 0 + err := changelog.Replay(1, 2, func(index uint64, entry proto.ChangelogEntry) error { + total++ + switch index { + case 1: + require.Equal(t, "test", entry.Changesets[0].Name) + require.Equal(t, []byte("hello"), entry.Changesets[0].Changeset.Pairs[0].Key) + require.Equal(t, []byte("world"), entry.Changesets[0].Changeset.Pairs[0].Value) + case 2: + require.Equal(t, []byte("hello1"), entry.Changesets[0].Changeset.Pairs[0].Key) + require.Equal(t, []byte("world1"), entry.Changesets[0].Changeset.Pairs[0].Value) + require.Equal(t, []byte("hello2"), entry.Changesets[0].Changeset.Pairs[1].Key) + require.Equal(t, []byte("world2"), entry.Changesets[0].Changeset.Pairs[1].Value) + default: + require.Fail(t, fmt.Sprintf("unexpected index %d", index)) + } + return nil + }) + require.NoError(t, err) + require.Equal(t, 2, total) + err = changelog.Close() + require.NoError(t, err) +} + +func TestRandomRead(t *testing.T) { + changelog := prepareTestData(t) + entry, err := changelog.ReadAt(2) + require.NoError(t, err) + require.Equal(t, []byte("hello1"), entry.Changesets[0].Changeset.Pairs[0].Key) + require.Equal(t, []byte("world1"), entry.Changesets[0].Changeset.Pairs[0].Value) + require.Equal(t, []byte("hello2"), entry.Changesets[0].Changeset.Pairs[1].Key) + require.Equal(t, []byte("world2"), entry.Changesets[0].Changeset.Pairs[1].Value) + entry, err = changelog.ReadAt(1) + require.NoError(t, err) + require.Equal(t, []byte("hello"), entry.Changesets[0].Changeset.Pairs[0].Key) + require.Equal(t, []byte("world"), entry.Changesets[0].Changeset.Pairs[0].Value) + entry, err = changelog.ReadAt(3) + require.NoError(t, err) + require.Equal(t, []byte("hello3"), entry.Changesets[0].Changeset.Pairs[0].Key) + require.Equal(t, []byte("world3"), entry.Changesets[0].Changeset.Pairs[0].Value) +} + +func prepareTestData(t *testing.T) *WAL[proto.ChangelogEntry] { + dir := t.TempDir() + changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) + require.NoError(t, err) + writeTestData(changelog) + return changelog +} + +func writeTestData(changelog *WAL[proto.ChangelogEntry]) { + for _, changes := range ChangeSets { + cs := []*proto.NamedChangeSet{ + { + Name: "test", + Changeset: changes, + }, + } + entry := proto.ChangelogEntry{} + entry.Changesets = cs + _ = changelog.Write(entry) + } +} + +func TestSynchronousWrite(t *testing.T) { + changelog := prepareTestData(t) + lastIndex, err := changelog.LastOffset() + require.NoError(t, err) + require.Equal(t, uint64(3), lastIndex) + +} + +func TestAsyncWrite(t *testing.T) { + dir := t.TempDir() + changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{WriteBufferSize: 10}) + require.NoError(t, err) + for _, changes := range ChangeSets { + cs := []*proto.NamedChangeSet{ + { + Name: "test", + Changeset: changes, + }, + } + entry := &proto.ChangelogEntry{} + entry.Changesets = cs + err := changelog.Write(*entry) + require.NoError(t, err) + } + err = changelog.Close() + require.NoError(t, err) + changelog, err = NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{WriteBufferSize: 10}) + require.NoError(t, err) + lastIndex, err := changelog.LastOffset() + require.NoError(t, err) + require.Equal(t, uint64(3), lastIndex) +} + +func TestOpenWithNilOptions(t *testing.T) { + dir := t.TempDir() + + // Test that open function handles nil options correctly + log, err := open(dir, nil) + require.NoError(t, err) + require.NotNil(t, log) + + // Verify the log is functional by checking first and last index + firstIndex, err := log.FirstIndex() + require.NoError(t, err) + require.Equal(t, uint64(0), firstIndex) + + lastIndex, err := log.LastIndex() + require.NoError(t, err) + require.Equal(t, uint64(0), lastIndex) + + // Clean up + err = log.Close() + require.NoError(t, err) +} + +func TestTruncateAfter(t *testing.T) { + changelog := prepareTestData(t) + defer changelog.Close() + + // Verify we have 3 entries + lastIndex, err := changelog.LastOffset() + require.NoError(t, err) + require.Equal(t, uint64(3), lastIndex) + + // Truncate after index 2 (removes entry 3) + err = changelog.TruncateAfter(2) + require.NoError(t, err) + + // Verify last index is now 2 + lastIndex, err = changelog.LastOffset() + require.NoError(t, err) + require.Equal(t, uint64(2), lastIndex) + + // Verify nextOffset was updated - write a new entry and check its index + entry := &proto.ChangelogEntry{} + entry.Changesets = []*proto.NamedChangeSet{{Name: "new", Changeset: iavl.ChangeSet{Pairs: MockKVPairs("new", "entry")}}} + err = changelog.Write(*entry) + require.NoError(t, err) + + lastIndex, err = changelog.LastOffset() + require.NoError(t, err) + require.Equal(t, uint64(3), lastIndex) +} + +func TestTruncateBefore(t *testing.T) { + changelog := prepareTestData(t) + defer changelog.Close() + + // Verify we have 3 entries starting at 1 + firstIndex, err := changelog.FirstOffset() + require.NoError(t, err) + require.Equal(t, uint64(1), firstIndex) + + lastIndex, err := changelog.LastOffset() + require.NoError(t, err) + require.Equal(t, uint64(3), lastIndex) + + // Truncate before index 2 (removes entry 1) + err = changelog.TruncateBefore(2) + require.NoError(t, err) + + // Verify first index is now 2 + firstIndex, err = changelog.FirstOffset() + require.NoError(t, err) + require.Equal(t, uint64(2), firstIndex) + + // Last index should still be 3 + lastIndex, err = changelog.LastOffset() + require.NoError(t, err) + require.Equal(t, uint64(3), lastIndex) + + // Verify entry 2 is still readable + entry, err := changelog.ReadAt(2) + require.NoError(t, err) + require.Equal(t, []byte("hello1"), entry.Changesets[0].Changeset.Pairs[0].Key) +} + +func TestCloseSyncMode(t *testing.T) { + dir := t.TempDir() + changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) + require.NoError(t, err) + + // Write some data in sync mode + writeTestData(changelog) + + // Close the changelog + err = changelog.Close() + require.NoError(t, err) + + // Verify isClosed is set + require.True(t, changelog.isClosed) + + // Reopen and verify data persisted + changelog2, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) + require.NoError(t, err) + defer changelog2.Close() + + lastIndex, err := changelog2.LastOffset() + require.NoError(t, err) + require.Equal(t, uint64(3), lastIndex) +} + +func TestReadAtNonExistent(t *testing.T) { + changelog := prepareTestData(t) + defer changelog.Close() + + // Try to read an entry that doesn't exist + _, err := changelog.ReadAt(100) + require.Error(t, err) +} + +func TestReplayWithError(t *testing.T) { + changelog := prepareTestData(t) + defer changelog.Close() + + // Replay with a function that returns an error + expectedErr := fmt.Errorf("test error") + err := changelog.Replay(1, 3, func(index uint64, entry proto.ChangelogEntry) error { + if index == 2 { + return expectedErr + } + return nil + }) + require.Error(t, err) + require.Equal(t, expectedErr, err) +} + +func TestReopenAndContinueWrite(t *testing.T) { + dir := t.TempDir() + + // Create and write initial data + changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) + require.NoError(t, err) + writeTestData(changelog) + err = changelog.Close() + require.NoError(t, err) + + // Reopen and continue writing + changelog2, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) + require.NoError(t, err) + + // Verify nextOffset is correctly set after reopen + lastIndex, err := changelog2.LastOffset() + require.NoError(t, err) + require.Equal(t, uint64(3), lastIndex) + + // Write more data + entry := &proto.ChangelogEntry{} + entry.Changesets = []*proto.NamedChangeSet{{Name: "continued", Changeset: iavl.ChangeSet{Pairs: MockKVPairs("key4", "value4")}}} + err = changelog2.Write(*entry) + require.NoError(t, err) + + // Verify new entry is at index 4 + lastIndex, err = changelog2.LastOffset() + require.NoError(t, err) + require.Equal(t, uint64(4), lastIndex) + + // Verify data integrity + readEntry, err := changelog2.ReadAt(4) + require.NoError(t, err) + require.Equal(t, "continued", readEntry.Changesets[0].Name) + require.Equal(t, []byte("key4"), readEntry.Changesets[0].Changeset.Pairs[0].Key) + + err = changelog2.Close() + require.NoError(t, err) +} + +func TestEmptyLog(t *testing.T) { + dir := t.TempDir() + changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) + require.NoError(t, err) + defer changelog.Close() + + // Empty log should have 0 for both first and last index + firstIndex, err := changelog.FirstOffset() + require.NoError(t, err) + require.Equal(t, uint64(0), firstIndex) + + lastIndex, err := changelog.LastOffset() + require.NoError(t, err) + require.Equal(t, uint64(0), lastIndex) +} + +func TestCheckErrorNoError(t *testing.T) { + dir := t.TempDir() + changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{WriteBufferSize: 10}) + require.NoError(t, err) + + // Write some data to initialize async mode + entry := &proto.ChangelogEntry{} + entry.Changesets = []*proto.NamedChangeSet{{Name: "test", Changeset: iavl.ChangeSet{Pairs: MockKVPairs("k", "v")}}} + err = changelog.Write(*entry) + require.NoError(t, err) + + // CheckError should return nil when no errors + err = changelog.CheckError() + require.NoError(t, err) + + err = changelog.Close() + require.NoError(t, err) +} + +func TestFirstAndLastOffset(t *testing.T) { + changelog := prepareTestData(t) + defer changelog.Close() + + firstIndex, err := changelog.FirstOffset() + require.NoError(t, err) + require.Equal(t, uint64(1), firstIndex) + + lastIndex, err := changelog.LastOffset() + require.NoError(t, err) + require.Equal(t, uint64(3), lastIndex) +} + +func TestAsyncWriteReopenAndContinue(t *testing.T) { + dir := t.TempDir() + + // Create with async write and write data + changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{WriteBufferSize: 10}) + require.NoError(t, err) + + for _, changes := range ChangeSets { + cs := []*proto.NamedChangeSet{{Name: "test", Changeset: changes}} + entry := &proto.ChangelogEntry{Changesets: cs} + err := changelog.Write(*entry) + require.NoError(t, err) + } + + err = changelog.Close() + require.NoError(t, err) + + // Reopen with async write and continue + changelog2, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{WriteBufferSize: 10}) + require.NoError(t, err) + + // Write more entries + for i := 0; i < 3; i++ { + entry := &proto.ChangelogEntry{} + entry.Changesets = []*proto.NamedChangeSet{{Name: fmt.Sprintf("batch2-%d", i), Changeset: iavl.ChangeSet{Pairs: MockKVPairs("k", "v")}}} + err := changelog2.Write(*entry) + require.NoError(t, err) + } + + err = changelog2.Close() + require.NoError(t, err) + + // Reopen and verify all 6 entries + changelog3, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) + require.NoError(t, err) + defer changelog3.Close() + + lastIndex, err := changelog3.LastOffset() + require.NoError(t, err) + require.Equal(t, uint64(6), lastIndex) +} + +func TestReplaySingleEntry(t *testing.T) { + changelog := prepareTestData(t) + defer changelog.Close() + + var count int + err := changelog.Replay(2, 2, func(index uint64, entry proto.ChangelogEntry) error { + count++ + require.Equal(t, uint64(2), index) + return nil + }) + require.NoError(t, err) + require.Equal(t, 1, count) +} + +func TestWriteMultipleChangesets(t *testing.T) { + dir := t.TempDir() + changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) + require.NoError(t, err) + defer changelog.Close() + + // Write entry with multiple changesets + entry := &proto.ChangelogEntry{ + Changesets: []*proto.NamedChangeSet{ + {Name: "store1", Changeset: iavl.ChangeSet{Pairs: MockKVPairs("a", "1")}}, + {Name: "store2", Changeset: iavl.ChangeSet{Pairs: MockKVPairs("b", "2")}}, + {Name: "store3", Changeset: iavl.ChangeSet{Pairs: MockKVPairs("c", "3")}}, + }, + } + err = changelog.Write(*entry) + require.NoError(t, err) + + // Read and verify + readEntry, err := changelog.ReadAt(1) + require.NoError(t, err) + require.Len(t, readEntry.Changesets, 3) + require.Equal(t, "store1", readEntry.Changesets[0].Name) + require.Equal(t, "store2", readEntry.Changesets[1].Name) + require.Equal(t, "store3", readEntry.Changesets[2].Name) +} + +func TestGetLastIndex(t *testing.T) { + dir := t.TempDir() + changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) + require.NoError(t, err) + writeTestData(changelog) + err = changelog.Close() + require.NoError(t, err) + + // Use utility function to get last index without opening stream + lastIndex, err := GetLastIndex(dir) + require.NoError(t, err) + require.Equal(t, uint64(3), lastIndex) +} + +func TestLogPath(t *testing.T) { + path := LogPath("/some/dir") + require.Equal(t, "/some/dir/changelog", path) +} diff --git a/sei-db/changelog/types/types.go b/sei-db/wal/types/types.go similarity index 70% rename from sei-db/changelog/types/types.go rename to sei-db/wal/types/types.go index 597b605c25..6642110db7 100644 --- a/sei-db/changelog/types/types.go +++ b/sei-db/wal/types/types.go @@ -1,8 +1,15 @@ package types -type Stream[T any] interface { - // Write will write a new entry to the log at the given index. - Write(offset uint64, entry T) error +// MarshalFn is a function that serializes an entry to bytes. +type MarshalFn[T any] func(entry T) ([]byte, error) + +// UnmarshalFn is a function that deserializes bytes to an entry. +type UnmarshalFn[T any] func(data []byte) (T, error) + +// GenericWAL is a generic write-ahead log interface. +type GenericWAL[T any] interface { + // Write will append a new entry to the end of the log. + Write(entry T) error // CheckError check the error signal of async writes CheckError() error @@ -14,7 +21,7 @@ type Stream[T any] interface { TruncateAfter(offset uint64) error // ReadAt will read the replay log at the given index - ReadAt(offset uint64) (*T, error) + ReadAt(offset uint64) (T, error) // FirstOffset returns the first written index of the log FirstOffset() (offset uint64, err error) From 5b8114243a5c98c26494644b2e3188a238709694 Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Tue, 6 Jan 2026 23:35:28 -0800 Subject: [PATCH 02/35] Add unit test and extract wal initializer --- sei-db/state_db/sc/memiavl/db.go | 49 +- sei-db/state_db/sc/memiavl/db_test.go | 91 +- sei-db/state_db/sc/memiavl/multitree.go | 3 + sei-db/state_db/sc/memiavl/opts.go | 8 +- sei-db/state_db/sc/memiavl/snapshot_test.go | 16 +- sei-db/state_db/sc/store.go | 118 ++- sei-db/state_db/sc/store_test.go | 867 ++++++++++++++++++++ 7 files changed, 1083 insertions(+), 69 deletions(-) create mode 100644 sei-db/state_db/sc/store_test.go diff --git a/sei-db/state_db/sc/memiavl/db.go b/sei-db/state_db/sc/memiavl/db.go index ae61fd2178..120bee83ee 100644 --- a/sei-db/state_db/sc/memiavl/db.go +++ b/sei-db/state_db/sc/memiavl/db.go @@ -195,31 +195,14 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * tree.snapshot.leavesMap.PrepareForRandomRead() } - // Create WAL for changelog replay and persistence - streamHandler, err := generic_wal.NewWAL( - func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, - func(data []byte) (proto.ChangelogEntry, error) { - var e proto.ChangelogEntry - err := e.Unmarshal(data) - return e, err - }, - logger, - utils.GetChangelogPath(opts.Dir), - generic_wal.Config{ - DisableFsync: true, - ZeroCopy: true, - WriteBufferSize: opts.AsyncCommitBuffer, - }, - ) - if err != nil { - return nil, err - } + // Use WAL from options (managed by upper layer like CommitStore) + streamHandler := opts.WAL - // Replay WAL to catch up to target version - if targetVersion == 0 || targetVersion > mtree.Version() { + // Replay WAL to catch up to target version (if WAL is provided) + if streamHandler != nil && (targetVersion == 0 || targetVersion > mtree.Version()) { logger.Info("Start catching up and replaying the MemIAVL changelog file") if err := mtree.Catchup(context.Background(), streamHandler, targetVersion); err != nil { - return nil, errorutils.Join(err, streamHandler.Close()) + return nil, err } logger.Info(fmt.Sprintf("Finished the replay and caught up to version %d", targetVersion)) } @@ -238,11 +221,13 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * } } - // truncate the rlog file - logger.Info("truncate rlog after version: %d", targetVersion) - truncateIndex := utils.VersionToIndex(targetVersion, mtree.initialVersion.Load()) - if err := streamHandler.TruncateAfter(truncateIndex); err != nil { - return nil, fmt.Errorf("fail to truncate rlog file: %w", err) + // truncate the rlog file (if WAL is provided) + if streamHandler != nil { + logger.Info("truncate rlog after version: %d", targetVersion) + truncateIndex := utils.VersionToIndex(targetVersion, mtree.initialVersion.Load()) + if err := streamHandler.TruncateAfter(truncateIndex); err != nil { + return nil, fmt.Errorf("fail to truncate rlog file: %w", err) + } } // prune snapshots that's larger than the target version @@ -837,13 +822,9 @@ func (db *DB) Close() error { db.snapshotRewriteCancelFunc = nil } - // Close stream handler after background goroutine has finished - db.logger.Info("Closing stream handler...") - if db.streamHandler != nil { - err := db.streamHandler.Close() - errs = append(errs, err) - db.streamHandler = nil - } + // Note: streamHandler (WAL) is owned by the upper layer (CommitStore) and should not be closed here. + // Just clear the reference. + db.streamHandler = nil errs = append(errs, db.MultiTree.Close()) diff --git a/sei-db/state_db/sc/memiavl/db_test.go b/sei-db/state_db/sc/memiavl/db_test.go index 0064ece708..e9b64f83c6 100644 --- a/sei-db/state_db/sc/memiavl/db_test.go +++ b/sei-db/state_db/sc/memiavl/db_test.go @@ -15,10 +15,34 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/common/utils" "github.com/sei-protocol/sei-chain/sei-db/proto" + "github.com/sei-protocol/sei-chain/sei-db/wal/generic_wal" + "github.com/sei-protocol/sei-chain/sei-db/wal/types" iavl "github.com/sei-protocol/sei-chain/sei-iavl" "github.com/stretchr/testify/require" ) +// createTestWAL creates a WAL for testing purposes. +func createTestWAL(t *testing.T, dir string) types.GenericWAL[proto.ChangelogEntry] { + t.Helper() + wal, err := generic_wal.NewWAL( + func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, + func(data []byte) (proto.ChangelogEntry, error) { + var e proto.ChangelogEntry + err := e.Unmarshal(data) + return e, err + }, + logger.NewNopLogger(), + utils.GetChangelogPath(dir), + generic_wal.Config{ + DisableFsync: true, + ZeroCopy: true, + WriteBufferSize: 0, + }, + ) + require.NoError(t, err) + return wal +} + // writeToWAL is a test helper that writes pending changes to WAL. // In production, CommitStore handles this. Tests that need WAL replay use this helper. func writeToWAL(t *testing.T, db *DB, changesets []*proto.NamedChangeSet, upgrades []*proto.TreeNameUpgrade) { @@ -184,8 +208,9 @@ func TestRewriteSnapshotBackground(t *testing.T) { entries, err := os.ReadDir(db.dir) require.NoError(t, err) - // three files: snapshot, current link, rlog, LOCK - require.Equal(t, 4, len(entries)) + // three files: snapshot, current link, LOCK + // (no rlog when WAL is not provided) + require.Equal(t, 3, len(entries)) // stopCh is closed by defer above } @@ -255,10 +280,16 @@ func TestSnapshotTriggerOnIntervalDiff(t *testing.T) { func TestRlog(t *testing.T) { dir := t.TempDir() initialStores := []string{"test", "delete"} + + // Create WAL for this test + wal := createTestWAL(t, dir) + defer wal.Close() + db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: dir, CreateIfMissing: true, InitialStores: initialStores, + WAL: wal, }) require.NoError(t, err) @@ -299,7 +330,8 @@ func TestRlog(t *testing.T) { require.NoError(t, db.Close()) - db, err = OpenDB(logger.NewNopLogger(), 0, Options{Dir: dir}) + // Reopen with the same WAL + db, err = OpenDB(logger.NewNopLogger(), 0, Options{Dir: dir, WAL: wal}) require.NoError(t, err) defer db.Close() // Close the reopened DB @@ -331,10 +363,15 @@ func TestInitialVersion(t *testing.T) { for _, initialVersion := range []int64{0, 1, 100} { dir := t.TempDir() initialStores := []string{name} + + // Create WAL for this test iteration + wal := createTestWAL(t, dir) + db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: dir, CreateIfMissing: true, InitialStores: initialStores, + WAL: wal, }) require.NoError(t, err) db.SetInitialVersion(initialVersion) @@ -366,9 +403,9 @@ func TestInitialVersion(t *testing.T) { } require.NoError(t, db.Close()) - db, err = OpenDB(logger.NewNopLogger(), 0, Options{Dir: dir}) + // Reopen with the same WAL + db, err = OpenDB(logger.NewNopLogger(), 0, Options{Dir: dir, WAL: wal}) require.NoError(t, err) - defer db.Close() // Close the reopened DB at end of loop iteration require.Equal(t, uint32(initialVersion), db.initialVersion.Load()) require.Equal(t, v, db.Version()) require.Equal(t, hex.EncodeToString(hash), hex.EncodeToString(db.LastCommitInfo().StoreInfos[0].CommitId.Hash)) @@ -413,16 +450,25 @@ func TestInitialVersion(t *testing.T) { require.Equal(t, name2, info2.Name) require.Equal(t, v, info2.CommitId.Version) require.Equal(t, hex.EncodeToString(info.CommitId.Hash), hex.EncodeToString(info2.CommitId.Hash)) + + require.NoError(t, db.Close()) + require.NoError(t, wal.Close()) } } func TestLoadVersion(t *testing.T) { dir := t.TempDir() initialStores := []string{"test"} + + // Create WAL for this test + wal := createTestWAL(t, dir) + defer wal.Close() + db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: dir, CreateIfMissing: true, InitialStores: initialStores, + WAL: wal, }) require.NoError(t, err) @@ -455,9 +501,11 @@ func TestLoadVersion(t *testing.T) { if v == 0 { continue } + // Read-only loads use the same WAL to replay tmp, err := OpenDB(logger.NewNopLogger(), int64(v), Options{ Dir: dir, ReadOnly: true, + WAL: wal, }) require.NoError(t, err) require.Equal(t, RefHashes[v-1], tmp.TreeByName("test").RootHash()) @@ -537,11 +585,17 @@ func TestRlogIndexConversion(t *testing.T) { func TestEmptyValue(t *testing.T) { dir := t.TempDir() initialStores := []string{"test"} + + // Create WAL for this test + wal := createTestWAL(t, dir) + defer wal.Close() + db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: dir, InitialStores: initialStores, CreateIfMissing: true, ZeroCopy: true, + WAL: wal, }) require.NoError(t, err) @@ -572,7 +626,8 @@ func TestEmptyValue(t *testing.T) { require.NoError(t, db.Close()) - db, err = OpenDB(logger.NewNopLogger(), 0, Options{Dir: dir, ZeroCopy: true}) + // Reopen with the same WAL + db, err = OpenDB(logger.NewNopLogger(), 0, Options{Dir: dir, ZeroCopy: true, WAL: wal}) require.NoError(t, err) defer db.Close() // Close the reopened DB require.Equal(t, version, db.Version()) @@ -762,26 +817,40 @@ func TestLoadMultiTreeWithCancelledContext(t *testing.T) { func TestCatchupWithCancelledContext(t *testing.T) { // Create a DB with some data dir := t.TempDir() + initialStores := []string{"test"} + + // Create WAL for this test + wal := createTestWAL(t, dir) + defer wal.Close() + db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: dir, CreateIfMissing: true, - InitialStores: []string{"test"}, + InitialStores: initialStores, + WAL: wal, }) require.NoError(t, err) defer db.Close() // Add multiple versions to have changelog entries for i := 0; i < 5; i++ { - require.NoError(t, db.ApplyChangeSets([]*proto.NamedChangeSet{ + cs := []*proto.NamedChangeSet{ {Name: "test", Changeset: iavl.ChangeSet{ Pairs: []*iavl.KVPair{{Key: []byte("key"), Value: []byte("value" + strconv.Itoa(i))}}, }}, - })) + } + require.NoError(t, db.ApplyChangeSets(cs)) + // Write to WAL + if i == 0 { + writeToWAL(t, db, cs, initialUpgrades(initialStores)) + } else { + writeToWAL(t, db, cs, nil) + } _, err = db.Commit() require.NoError(t, err) } - // Create snapshot at version 2 + // Create snapshot at version 5 require.NoError(t, db.RewriteSnapshot(context.Background())) // Load the snapshot (at version 5) @@ -797,7 +866,7 @@ func TestCatchupWithCancelledContext(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() // Cancel immediately - err = mtree.Catchup(ctx, db.streamHandler, 0) + err = mtree.Catchup(ctx, wal, 0) // If already caught up, no error; otherwise should get context.Canceled if err != nil { require.Equal(t, context.Canceled, err) diff --git a/sei-db/state_db/sc/memiavl/multitree.go b/sei-db/state_db/sc/memiavl/multitree.go index 0498b8fccf..86b9184d8e 100644 --- a/sei-db/state_db/sc/memiavl/multitree.go +++ b/sei-db/state_db/sc/memiavl/multitree.go @@ -78,6 +78,9 @@ func NewEmptyMultiTree(initialVersion uint32) *MultiTree { func LoadMultiTree(ctx context.Context, dir string, opts Options) (*MultiTree, error) { startTime := time.Now() log := opts.Logger + if log == nil { + log = logger.NewNopLogger() + } metadata, err := readMetadata(dir) if err != nil { return nil, err diff --git a/sei-db/state_db/sc/memiavl/opts.go b/sei-db/state_db/sc/memiavl/opts.go index ad6474890b..0e3b3e8b0a 100644 --- a/sei-db/state_db/sc/memiavl/opts.go +++ b/sei-db/state_db/sc/memiavl/opts.go @@ -6,8 +6,9 @@ import ( "time" "github.com/sei-protocol/sei-chain/sei-db/common/logger" - "github.com/sei-protocol/sei-chain/sei-db/config" + "github.com/sei-protocol/sei-chain/sei-db/proto" + "github.com/sei-protocol/sei-chain/sei-db/wal/types" ) type Options struct { @@ -48,6 +49,11 @@ type Options struct { // Minimum time interval between snapshots // This prevents excessive snapshot creation during catch-up. Default is 1 hour. SnapshotMinTimeInterval time.Duration + + // WAL is the write-ahead log for changelog persistence and replay. + // If nil, no WAL operations will be performed (read-only mode or tests). + // The WAL is managed by the upper layer (CommitStore) and passed in. + WAL types.GenericWAL[proto.ChangelogEntry] } func (opts Options) Validate() error { diff --git a/sei-db/state_db/sc/memiavl/snapshot_test.go b/sei-db/state_db/sc/memiavl/snapshot_test.go index 33db34dbfb..5eb5b380f5 100644 --- a/sei-db/state_db/sc/memiavl/snapshot_test.go +++ b/sei-db/state_db/sc/memiavl/snapshot_test.go @@ -141,12 +141,19 @@ func TestSnapshotImportExport(t *testing.T) { } func TestDBSnapshotRestore(t *testing.T) { + dir := t.TempDir() initialStores := []string{"test", "test2"} + + // Create WAL for this test + wal := createTestWAL(t, dir) + defer wal.Close() + db, err := OpenDB(logger.NewNopLogger(), 0, Options{ - Dir: t.TempDir(), + Dir: dir, CreateIfMissing: true, InitialStores: initialStores, AsyncCommitBuffer: -1, + WAL: wal, }) require.NoError(t, err) @@ -170,17 +177,20 @@ func TestDBSnapshotRestore(t *testing.T) { } _, err := db.Commit() require.NoError(t, err) + + // Create snapshot so export/import test can work without WAL + require.NoError(t, db.RewriteSnapshot(context.Background())) testSnapshotRoundTrip(t, db) } - require.NoError(t, db.RewriteSnapshot(context.Background())) require.NoError(t, db.Reload()) require.Equal(t, len(ChangeSets), int(db.metadata.CommitInfo.Version)) testSnapshotRoundTrip(t, db) } func testSnapshotRoundTrip(t *testing.T, db *DB) { - exporter, err := NewMultiTreeExporter(db.dir, uint32(db.Version()), false) + // Use NewMultiTreeExporter which loads from snapshot on disk + exporter, err := NewMultiTreeExporter(db.dir, uint32(db.Version()), true) // onlyAllowExportOnSnapshotVersion=true require.NoError(t, err) restoreDir := t.TempDir() diff --git a/sei-db/state_db/sc/store.go b/sei-db/state_db/sc/store.go index 0a159a0b32..ced57eae0c 100644 --- a/sei-db/state_db/sc/store.go +++ b/sei-db/state_db/sc/store.go @@ -5,12 +5,15 @@ import ( "math" "time" + "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/common/utils" "github.com/sei-protocol/sei-chain/sei-db/config" "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/memiavl" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" + "github.com/sei-protocol/sei-chain/sei-db/wal/generic_wal" + waltypes "github.com/sei-protocol/sei-chain/sei-db/wal/types" ) var _ types.Committer = (*CommitStore)(nil) @@ -20,6 +23,9 @@ type CommitStore struct { db *memiavl.DB opts memiavl.Options + // WAL for changelog persistence (owned by CommitStore) + wal waltypes.GenericWAL[proto.ChangelogEntry] + // pending changes to be written to WAL on next Commit pendingLogEntry proto.ChangelogEntry } @@ -48,6 +54,25 @@ func NewCommitStore(homeDir string, logger logger.Logger, config config.StateCom return commitStore } +// createWAL creates a new WAL instance for changelog persistence and replay. +func (cs *CommitStore) createWAL() (waltypes.GenericWAL[proto.ChangelogEntry], error) { + return generic_wal.NewWAL( + func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, + func(data []byte) (proto.ChangelogEntry, error) { + var e proto.ChangelogEntry + err := e.Unmarshal(data) + return e, err + }, + cs.logger, + utils.GetChangelogPath(cs.opts.Dir), + generic_wal.Config{ + DisableFsync: true, + ZeroCopy: true, + WriteBufferSize: cs.opts.AsyncCommitBuffer, + }, + ) +} + func (cs *CommitStore) Initialize(initialStores []string) { cs.opts.InitialStores = initialStores } @@ -57,11 +82,23 @@ func (cs *CommitStore) SetInitialVersion(initialVersion int64) error { } func (cs *CommitStore) Rollback(targetVersion int64) error { - options := cs.opts - options.LoadForOverwriting = true + // Close existing resources if cs.db != nil { _ = cs.db.Close() } + // Note: we reuse the existing WAL for rollback - memiavl will truncate it + if cs.wal == nil { + wal, err := cs.createWAL() + if err != nil { + return fmt.Errorf("failed to create WAL: %w", err) + } + cs.wal = wal + } + + options := cs.opts + options.LoadForOverwriting = true + options.WAL = cs.wal + db, err := memiavl.OpenDB(cs.logger, targetVersion, options) if err != nil { return err @@ -70,31 +107,64 @@ func (cs *CommitStore) Rollback(targetVersion int64) error { return nil } -// copyExisting is for creating new memiavl object given existing folder +// LoadVersion loads the specified version of the database. +// If copyExisting is true, creates a read-only copy for querying. func (cs *CommitStore) LoadVersion(targetVersion int64, copyExisting bool) (types.Committer, error) { cs.logger.Info(fmt.Sprintf("SeiDB load target memIAVL version %d, copyExisting = %v\n", targetVersion, copyExisting)) + if copyExisting { - opts := cs.opts - opts.ReadOnly = copyExisting - opts.CreateIfMissing = false - db, err := memiavl.OpenDB(cs.logger, targetVersion, opts) + // Create a read-only copy with its own WAL for replay + newCS := &CommitStore{ + logger: cs.logger, + opts: cs.opts, + } + newCS.opts.ReadOnly = true + newCS.opts.CreateIfMissing = false + + // WAL is needed for replay even in read-only mode + wal, err := newCS.createWAL() + if err != nil { + return nil, fmt.Errorf("failed to create WAL: %w", err) + } + newCS.wal = wal + newCS.opts.WAL = wal + + db, err := memiavl.OpenDB(cs.logger, targetVersion, newCS.opts) if err != nil { + _ = wal.Close() return nil, err } - return &CommitStore{ - logger: cs.logger, - db: db, - opts: opts, - }, nil + newCS.db = db + return newCS, nil } + + // Close existing resources if cs.db != nil { _ = cs.db.Close() } - db, err := memiavl.OpenDB(cs.logger, targetVersion, cs.opts) + if cs.wal != nil { + _ = cs.wal.Close() + cs.wal = nil + } + + // Create WAL for changelog persistence and replay + wal, err := cs.createWAL() if err != nil { + return nil, fmt.Errorf("failed to create WAL: %w", err) + } + + // Pass WAL to memiavl via options + opts := cs.opts + opts.WAL = wal + + db, err := memiavl.OpenDB(cs.logger, targetVersion, opts) + if err != nil { + _ = wal.Close() return nil, err } + cs.db = db + cs.wal = wal return cs, nil } @@ -103,14 +173,13 @@ func (cs *CommitStore) Commit() (int64, error) { nextVersion := cs.db.WorkingCommitInfo().Version // Write to WAL first (ensures durability before tree commit) - wal := cs.db.GetWAL() - if wal != nil { + if cs.wal != nil { cs.pendingLogEntry.Version = nextVersion - if err := wal.Write(cs.pendingLogEntry); err != nil { + if err := cs.wal.Write(cs.pendingLogEntry); err != nil { return 0, fmt.Errorf("failed to write to WAL: %w", err) } // Check for async write errors - if err := wal.CheckError(); err != nil { + if err := cs.wal.CheckError(); err != nil { return 0, fmt.Errorf("WAL async write error: %w", err) } } @@ -185,10 +254,19 @@ func (cs *CommitStore) Importer(version int64) (types.Importer, error) { } func (cs *CommitStore) Close() error { + var errs []error + + // Close DB first (it may still reference WAL) if cs.db != nil { - err := cs.db.Close() + errs = append(errs, cs.db.Close()) cs.db = nil - return err } - return nil + + // Then close WAL + if cs.wal != nil { + errs = append(errs, cs.wal.Close()) + cs.wal = nil + } + + return errors.Join(errs...) } diff --git a/sei-db/state_db/sc/store_test.go b/sei-db/state_db/sc/store_test.go new file mode 100644 index 0000000000..bcaea708ab --- /dev/null +++ b/sei-db/state_db/sc/store_test.go @@ -0,0 +1,867 @@ +package sc + +import ( + "math" + "testing" + + "github.com/sei-protocol/sei-chain/sei-db/common/logger" + "github.com/sei-protocol/sei-chain/sei-db/config" + "github.com/sei-protocol/sei-chain/sei-db/proto" + iavl "github.com/sei-protocol/sei-chain/sei-iavl" + "github.com/stretchr/testify/require" +) + +func TestNewCommitStore(t *testing.T) { + dir := t.TempDir() + cfg := config.StateCommitConfig{ + ZeroCopy: true, + SnapshotInterval: 10, + } + + cs := NewCommitStore(dir, logger.NewNopLogger(), cfg) + require.NotNil(t, cs) + require.NotNil(t, cs.logger) + require.True(t, cs.opts.ZeroCopy) + require.Equal(t, uint32(10), cs.opts.SnapshotInterval) + require.True(t, cs.opts.CreateIfMissing) +} + +func TestNewCommitStoreWithCustomDirectory(t *testing.T) { + homeDir := t.TempDir() + customDir := t.TempDir() + cfg := config.StateCommitConfig{ + Directory: customDir, + } + + cs := NewCommitStore(homeDir, logger.NewNopLogger(), cfg) + require.NotNil(t, cs) + require.Contains(t, cs.opts.Dir, customDir) +} + +func TestInitialize(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + + stores := []string{"store1", "store2", "store3"} + cs.Initialize(stores) + + require.Equal(t, stores, cs.opts.InitialStores) +} + +func TestCommitStoreBasicOperations(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + // Load version 0 to initialize the DB + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + defer cs.Close() + + // Initial version should be 0 + require.Equal(t, int64(0), cs.Version()) + + // Apply changesets + changesets := []*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key1"), Value: []byte("value1")}, + {Key: []byte("key2"), Value: []byte("value2")}, + }, + }, + }, + } + err = cs.ApplyChangeSets(changesets) + require.NoError(t, err) + + // Verify pending entry has the changesets + require.Equal(t, changesets, cs.pendingLogEntry.Changesets) + + // Commit + version, err := cs.Commit() + require.NoError(t, err) + require.Equal(t, int64(1), version) + + // Pending entry should be cleared after commit + require.Nil(t, cs.pendingLogEntry.Changesets) + require.Nil(t, cs.pendingLogEntry.Upgrades) + + // Version should be updated + require.Equal(t, int64(1), cs.Version()) +} + +func TestApplyChangeSetsEmpty(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + defer cs.Close() + + // Empty changesets should be no-op + err = cs.ApplyChangeSets(nil) + require.NoError(t, err) + require.Nil(t, cs.pendingLogEntry.Changesets) + + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{}) + require.NoError(t, err) + require.Nil(t, cs.pendingLogEntry.Changesets) +} + +func TestApplyUpgrades(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + defer cs.Close() + + // Apply upgrades + upgrades := []*proto.TreeNameUpgrade{ + {Name: "newstore1"}, + {Name: "newstore2"}, + } + err = cs.ApplyUpgrades(upgrades) + require.NoError(t, err) + + // Verify pending entry has the upgrades + require.Equal(t, upgrades, cs.pendingLogEntry.Upgrades) + + // Apply more upgrades - should append + moreUpgrades := []*proto.TreeNameUpgrade{ + {Name: "newstore3"}, + } + err = cs.ApplyUpgrades(moreUpgrades) + require.NoError(t, err) + + require.Len(t, cs.pendingLogEntry.Upgrades, 3) +} + +func TestApplyUpgradesEmpty(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + defer cs.Close() + + // Empty upgrades should be no-op + err = cs.ApplyUpgrades(nil) + require.NoError(t, err) + require.Nil(t, cs.pendingLogEntry.Upgrades) + + err = cs.ApplyUpgrades([]*proto.TreeNameUpgrade{}) + require.NoError(t, err) + require.Nil(t, cs.pendingLogEntry.Upgrades) +} + +func TestLoadVersionCopyExisting(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + // First load to create the DB + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + + // For the first commit, we need to include initial upgrades in pending entry + // so they are written to WAL for replay + cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} + + // Apply and commit some data + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key"), Value: []byte("value")}, + }, + }, + }, + }) + require.NoError(t, err) + _, err = cs.Commit() + require.NoError(t, err) + require.NoError(t, cs.Close()) + + // Load with copyExisting=true should create a new readonly CommitStore + newCS, err := cs.LoadVersion(0, true) + require.NoError(t, err) + require.NotNil(t, newCS) + + // The returned store should be different from the original + newCommitStore, ok := newCS.(*CommitStore) + require.True(t, ok) + require.NotSame(t, cs, newCommitStore) + + require.NoError(t, newCommitStore.Close()) +} + +func TestCommitInfo(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + defer cs.Close() + + // WorkingCommitInfo before any commit + workingInfo := cs.WorkingCommitInfo() + require.NotNil(t, workingInfo) + + // Apply and commit + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key"), Value: []byte("value")}, + }, + }, + }, + }) + require.NoError(t, err) + _, err = cs.Commit() + require.NoError(t, err) + + // LastCommitInfo after commit + lastInfo := cs.LastCommitInfo() + require.NotNil(t, lastInfo) + require.Equal(t, int64(1), lastInfo.Version) +} + +func TestGetModuleByName(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test", "other"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + defer cs.Close() + + // Get existing module + module := cs.GetModuleByName("test") + require.NotNil(t, module) + + // Get non-existing module + module = cs.GetModuleByName("nonexistent") + require.Nil(t, module) +} + +func TestExporterVersionValidation(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + defer cs.Close() + + // Negative version should fail + _, err = cs.Exporter(-1) + require.Error(t, err) + require.Contains(t, err.Error(), "out of range") + + // Version > MaxUint32 should fail + _, err = cs.Exporter(math.MaxUint32 + 1) + require.Error(t, err) + require.Contains(t, err.Error(), "out of range") +} + +func TestImporterVersionValidation(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + + // Negative version should fail + _, err := cs.Importer(-1) + require.Error(t, err) + require.Contains(t, err.Error(), "out of range") + + // Version > MaxUint32 should fail + _, err = cs.Importer(math.MaxUint32 + 1) + require.Error(t, err) + require.Contains(t, err.Error(), "out of range") +} + +func TestClose(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + + // Close should succeed + err = cs.Close() + require.NoError(t, err) + + // db should be nil after close + require.Nil(t, cs.db) + + // Close again should be safe (no-op) + err = cs.Close() + require.NoError(t, err) +} + +func TestRollback(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + + // Commit a few versions + for i := 0; i < 3; i++ { + // First commit needs initial upgrades for WAL replay + if i == 0 { + cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} + } + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key"), Value: []byte("value" + string(rune('0'+i)))}, + }, + }, + }, + }) + require.NoError(t, err) + _, err = cs.Commit() + require.NoError(t, err) + } + + require.Equal(t, int64(3), cs.Version()) + + // Rollback to version 2 (truncates WAL after version 2) + err = cs.Rollback(2) + require.NoError(t, err) + require.Equal(t, int64(2), cs.Version()) + + require.NoError(t, cs.Close()) +} + +func TestMultipleCommits(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + defer cs.Close() + + // Multiple commits + for i := 1; i <= 5; i++ { + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key" + string(rune('0'+i))), Value: []byte("value")}, + }, + }, + }, + }) + require.NoError(t, err) + + version, err := cs.Commit() + require.NoError(t, err) + require.Equal(t, int64(i), version) + + // Pending entry should be cleared + require.Nil(t, cs.pendingLogEntry.Changesets) + } + + require.Equal(t, int64(5), cs.Version()) +} + +func TestCommitWithUpgradesAndChangesets(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + defer cs.Close() + + // Apply upgrades first + err = cs.ApplyUpgrades([]*proto.TreeNameUpgrade{ + {Name: "newstore"}, + }) + require.NoError(t, err) + + // Then apply changesets to the new store + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "newstore", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key"), Value: []byte("value")}, + }, + }, + }, + }) + require.NoError(t, err) + + // Both should be in pending entry + require.Len(t, cs.pendingLogEntry.Upgrades, 1) + require.Len(t, cs.pendingLogEntry.Changesets, 1) + + // Commit + version, err := cs.Commit() + require.NoError(t, err) + require.Equal(t, int64(1), version) + + // Pending entry should be cleared + require.Nil(t, cs.pendingLogEntry.Changesets) + require.Nil(t, cs.pendingLogEntry.Upgrades) +} + +func TestSetInitialVersion(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + defer cs.Close() + + // Set initial version + err = cs.SetInitialVersion(100) + require.NoError(t, err) +} + +func TestGetVersions(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + + // Commit a few versions + for i := 0; i < 3; i++ { + // First commit needs initial upgrades for WAL replay + if i == 0 { + cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} + } + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key"), Value: []byte("value")}, + }, + }, + }, + }) + require.NoError(t, err) + _, err = cs.Commit() + require.NoError(t, err) + } + require.NoError(t, cs.Close()) + + // Create new CommitStore to test GetLatestVersion + cs2 := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs2.Initialize([]string{"test"}) + + latestVersion, err := cs2.GetLatestVersion() + require.NoError(t, err) + require.Equal(t, int64(3), latestVersion) +} + +func TestCreateWAL(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + // createWAL should create a valid WAL instance + wal, err := cs.createWAL() + require.NoError(t, err) + require.NotNil(t, wal) + + // WAL should be functional - write an entry + entry := proto.ChangelogEntry{ + Version: 1, + Upgrades: []*proto.TreeNameUpgrade{ + {Name: "test"}, + }, + } + err = wal.Write(entry) + require.NoError(t, err) + + // Clean up + require.NoError(t, wal.Close()) +} + +func TestLoadVersionReadOnlyWithWALReplay(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + // First load to create the DB + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + + // Write data with WAL entries + cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key1"), Value: []byte("value1")}, + }, + }, + }, + }) + require.NoError(t, err) + _, err = cs.Commit() + require.NoError(t, err) + + // Write more data + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key2"), Value: []byte("value2")}, + }, + }, + }, + }) + require.NoError(t, err) + _, err = cs.Commit() + require.NoError(t, err) + + require.Equal(t, int64(2), cs.Version()) + + // Load read-only copy - should replay from WAL + readOnlyCS, err := cs.LoadVersion(0, true) + require.NoError(t, err) + require.NotNil(t, readOnlyCS) + + // The read-only copy should have the same version after WAL replay + roCommitStore := readOnlyCS.(*CommitStore) + require.Equal(t, int64(2), roCommitStore.Version()) + + // The read-only copy should have its own WAL + require.NotNil(t, roCommitStore.wal) + + // Clean up + require.NoError(t, roCommitStore.Close()) + require.NoError(t, cs.Close()) +} + +func TestLoadVersionReadOnlyCreatesOwnWAL(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + // First load to create the DB + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + + // Commit some data with WAL entries + cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key"), Value: []byte("value")}, + }, + }, + }, + }) + require.NoError(t, err) + _, err = cs.Commit() + require.NoError(t, err) + + // Create multiple read-only copies + readOnly1, err := cs.LoadVersion(0, true) + require.NoError(t, err) + require.NotNil(t, readOnly1) + + readOnly2, err := cs.LoadVersion(0, true) + require.NoError(t, err) + require.NotNil(t, readOnly2) + + // Each should have its own WAL instance + ro1 := readOnly1.(*CommitStore) + ro2 := readOnly2.(*CommitStore) + require.NotNil(t, ro1.wal) + require.NotNil(t, ro2.wal) + require.NotSame(t, ro1.wal, ro2.wal) + + // Clean up + require.NoError(t, ro1.Close()) + require.NoError(t, ro2.Close()) + require.NoError(t, cs.Close()) +} + +func TestWALPersistenceAcrossRestart(t *testing.T) { + dir := t.TempDir() + + // First session: write data + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + + // Write and commit + cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key1"), Value: []byte("value1")}, + {Key: []byte("key2"), Value: []byte("value2")}, + }, + }, + }, + }) + require.NoError(t, err) + _, err = cs.Commit() + require.NoError(t, err) + + // More commits + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key3"), Value: []byte("value3")}, + }, + }, + }, + }) + require.NoError(t, err) + _, err = cs.Commit() + require.NoError(t, err) + + require.Equal(t, int64(2), cs.Version()) + require.NoError(t, cs.Close()) + + // Second session: reload and verify WAL replay + cs2 := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs2.Initialize([]string{"test"}) + + _, err = cs2.LoadVersion(0, false) + require.NoError(t, err) + + // Version should be restored via WAL replay + require.Equal(t, int64(2), cs2.Version()) + + // Data should be accessible + tree := cs2.GetModuleByName("test") + require.NotNil(t, tree) + + require.NoError(t, cs2.Close()) +} + +func TestRollbackWithWAL(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + + // Commit multiple versions + for i := 0; i < 5; i++ { + if i == 0 { + cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} + } + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key"), Value: []byte("value" + string(rune('0'+i)))}, + }, + }, + }, + }) + require.NoError(t, err) + _, err = cs.Commit() + require.NoError(t, err) + } + + require.Equal(t, int64(5), cs.Version()) + require.NotNil(t, cs.wal) + + // Rollback to version 3 + err = cs.Rollback(3) + require.NoError(t, err) + require.Equal(t, int64(3), cs.Version()) + + // WAL should still exist after rollback + require.NotNil(t, cs.wal) + + require.NoError(t, cs.Close()) + + // Reopen and verify rollback persisted + cs2 := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs2.Initialize([]string{"test"}) + + _, err = cs2.LoadVersion(0, false) + require.NoError(t, err) + + // Version should be 3 after replay + require.Equal(t, int64(3), cs2.Version()) + + require.NoError(t, cs2.Close()) +} + +func TestRollbackCreatesWALIfNeeded(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + // Load and commit + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + + cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key"), Value: []byte("value")}, + }, + }, + }, + }) + require.NoError(t, err) + _, err = cs.Commit() + require.NoError(t, err) + + // Close to clear WAL + require.NoError(t, cs.Close()) + + // WAL should be nil after close + require.Nil(t, cs.wal) + + // Rollback should create a new WAL + err = cs.Rollback(1) + require.NoError(t, err) + + // WAL should be created + require.NotNil(t, cs.wal) + + require.NoError(t, cs.Close()) +} + +func TestCloseReleasesWAL(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + + // WAL should exist after load + require.NotNil(t, cs.wal) + + // Close + require.NoError(t, cs.Close()) + + // WAL should be nil after close + require.Nil(t, cs.wal) + require.Nil(t, cs.db) +} + +func TestLoadVersionReusesExistingWAL(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + // First load + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + + // WAL should be created + require.NotNil(t, cs.wal) + + // Commit some data + cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key"), Value: []byte("value")}, + }, + }, + }, + }) + require.NoError(t, err) + _, err = cs.Commit() + require.NoError(t, err) + + // Second load (non-copy) should close and recreate WAL + _, err = cs.LoadVersion(0, false) + require.NoError(t, err) + + // WAL should still exist + require.NotNil(t, cs.wal) + + // Version should be replayed + require.Equal(t, int64(1), cs.Version()) + + require.NoError(t, cs.Close()) +} + +func TestReadOnlyCopyCannotCommit(t *testing.T) { + dir := t.TempDir() + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + // First load + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + + // Commit initial data + cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key"), Value: []byte("value")}, + }, + }, + }, + }) + require.NoError(t, err) + _, err = cs.Commit() + require.NoError(t, err) + + // Load read-only copy + readOnly, err := cs.LoadVersion(0, true) + require.NoError(t, err) + + roCS := readOnly.(*CommitStore) + + // Read-only copy should have read-only option set + require.True(t, roCS.opts.ReadOnly) + + // Attempting to commit on read-only copy should fail + // (this would fail at the memiavl.DB level) + _, err = roCS.Commit() + require.Error(t, err) + + require.NoError(t, roCS.Close()) + require.NoError(t, cs.Close()) +} From 08deff1c62353657a69971ec9df8250ae4463a9b Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Thu, 8 Jan 2026 03:03:09 -0800 Subject: [PATCH 03/35] Fix go lint --- sei-db/ledger_db/receipt/store.go | 24 ++++++++++++++++++++++++ sei-db/wal/generic_wal/wal_test.go | 7 ++++--- 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 sei-db/ledger_db/receipt/store.go diff --git a/sei-db/ledger_db/receipt/store.go b/sei-db/ledger_db/receipt/store.go new file mode 100644 index 0000000000..d318c7964f --- /dev/null +++ b/sei-db/ledger_db/receipt/store.go @@ -0,0 +1,24 @@ +package receipt + +PebbleDB backend -> current +Parquet backend + +Interface: SS store +Impl: MVCC PebbleDB + +1. Add interface for receipt store +2. Add PebbleDB backend receipt store impl (MVCC Pebble) +3. Refactor existing receipt SS store to use new receipt interface with PebbleDB backend + +When designing receipt store interface: + - Check how we write + - Check how we read (getReceiptByHash, ethGetLog) +4. Add ReceiptCache impl +5. Add parquet backend impl + + +ReceiptStore interface -> User/App deal with +- Cache Layer +- DB layer + - Pebble + - Parquet diff --git a/sei-db/wal/generic_wal/wal_test.go b/sei-db/wal/generic_wal/wal_test.go index 7ec81be1ff..7a9ec90392 100644 --- a/sei-db/wal/generic_wal/wal_test.go +++ b/sei-db/wal/generic_wal/wal_test.go @@ -6,11 +6,12 @@ import ( "path/filepath" "testing" + "github.com/stretchr/testify/require" + "github.com/tidwall/wal" + "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/proto" iavl "github.com/sei-protocol/sei-chain/sei-iavl" - "github.com/stretchr/testify/require" - "github.com/tidwall/wal" ) var ( @@ -21,7 +22,7 @@ var ( } // marshal/unmarshal functions for testing - marshalEntry = func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() } + marshalEntry = func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() } unmarshalEntry = func(data []byte) (proto.ChangelogEntry, error) { var e proto.ChangelogEntry err := e.Unmarshal(data) From 55d217c9552ef0758c334eef841667da43a6c033 Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Thu, 8 Jan 2026 03:07:16 -0800 Subject: [PATCH 04/35] Fix race condition --- sei-db/ledger_db/receipt/store.go | 24 ------------------------ sei-db/wal/generic_wal/wal.go | 9 +++++---- sei-db/wal/generic_wal/wal_test.go | 2 +- 3 files changed, 6 insertions(+), 29 deletions(-) delete mode 100644 sei-db/ledger_db/receipt/store.go diff --git a/sei-db/ledger_db/receipt/store.go b/sei-db/ledger_db/receipt/store.go deleted file mode 100644 index d318c7964f..0000000000 --- a/sei-db/ledger_db/receipt/store.go +++ /dev/null @@ -1,24 +0,0 @@ -package receipt - -PebbleDB backend -> current -Parquet backend - -Interface: SS store -Impl: MVCC PebbleDB - -1. Add interface for receipt store -2. Add PebbleDB backend receipt store impl (MVCC Pebble) -3. Refactor existing receipt SS store to use new receipt interface with PebbleDB backend - -When designing receipt store interface: - - Check how we write - - Check how we read (getReceiptByHash, ethGetLog) -4. Add ReceiptCache impl -5. Add parquet backend impl - - -ReceiptStore interface -> User/App deal with -- Cache Layer -- DB layer - - Pebble - - Parquet diff --git a/sei-db/wal/generic_wal/wal.go b/sei-db/wal/generic_wal/wal.go index ef8799d7bd..cbb32e38ed 100644 --- a/sei-db/wal/generic_wal/wal.go +++ b/sei-db/wal/generic_wal/wal.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "sync/atomic" "time" "github.com/tidwall/wal" @@ -25,7 +26,7 @@ type WAL[T any] struct { writeChannel chan T errSignal chan error nextOffset uint64 - isClosed bool + isClosed atomic.Bool } type Config struct { @@ -70,7 +71,7 @@ func NewWAL[T any]( logger: logger, marshal: marshal, unmarshal: unmarshal, - isClosed: false, + // isClosed is zero-initialized to false (atomic.Bool) } // Finding the nextOffset to write lastIndex, err := log.LastIndex() @@ -219,7 +220,7 @@ func (walLog *WAL[T]) Replay(start uint64, end uint64, processFn func(index uint } func (walLog *WAL[T]) StartPruning(keepRecent uint64, pruneInterval time.Duration) { - for !walLog.isClosed { + for !walLog.isClosed.Load() { lastIndex, _ := walLog.log.LastIndex() firstIndex, _ := walLog.log.FirstIndex() if lastIndex > keepRecent && (lastIndex-keepRecent) > firstIndex { @@ -233,7 +234,7 @@ func (walLog *WAL[T]) StartPruning(keepRecent uint64, pruneInterval time.Duratio } func (walLog *WAL[T]) Close() error { - walLog.isClosed = true + walLog.isClosed.Store(true) var err error if walLog.writeChannel != nil { close(walLog.writeChannel) diff --git a/sei-db/wal/generic_wal/wal_test.go b/sei-db/wal/generic_wal/wal_test.go index 7a9ec90392..7adf39818c 100644 --- a/sei-db/wal/generic_wal/wal_test.go +++ b/sei-db/wal/generic_wal/wal_test.go @@ -261,7 +261,7 @@ func TestCloseSyncMode(t *testing.T) { require.NoError(t, err) // Verify isClosed is set - require.True(t, changelog.isClosed) + require.True(t, changelog.isClosed.Load()) // Reopen and verify data persisted changelog2, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) From 1d5f2bbabf43400e0f96b60571c091e9095782e6 Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Thu, 8 Jan 2026 03:12:10 -0800 Subject: [PATCH 05/35] Address comment --- sei-db/db_engine/pebbledb/mvcc/db.go | 5 ++++- sei-db/db_engine/rocksdb/mvcc/db.go | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/sei-db/db_engine/pebbledb/mvcc/db.go b/sei-db/db_engine/pebbledb/mvcc/db.go index 881c8883cd..4783d1e3f0 100644 --- a/sei-db/db_engine/pebbledb/mvcc/db.go +++ b/sei-db/db_engine/pebbledb/mvcc/db.go @@ -169,7 +169,7 @@ func OpenDB(dataDir string, config config.StateStoreConfig) (*Database, error) { _ = db.Close() return nil, errors.New("KeepRecent must be non-negative") } - streamHandler, _ := generic_wal.NewWAL( + streamHandler, err := generic_wal.NewWAL( func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, func(data []byte) (proto.ChangelogEntry, error) { var e proto.ChangelogEntry @@ -185,6 +185,9 @@ func OpenDB(dataDir string, config config.StateStoreConfig) (*Database, error) { PruneInterval: time.Duration(config.PruneIntervalSeconds) * time.Second, }, ) + if err != nil { + panic(err) + } database.streamHandler = streamHandler database.asyncWriteWG.Add(1) go database.writeAsyncInBackground() diff --git a/sei-db/db_engine/rocksdb/mvcc/db.go b/sei-db/db_engine/rocksdb/mvcc/db.go index 00939298a4..f7716f5440 100644 --- a/sei-db/db_engine/rocksdb/mvcc/db.go +++ b/sei-db/db_engine/rocksdb/mvcc/db.go @@ -112,7 +112,7 @@ func OpenDB(dataDir string, config config.StateStoreConfig) (*Database, error) { } database.latestVersion.Store(latestVersion) - streamHandler, _ := generic_wal.NewWAL( + streamHandler, err := generic_wal.NewWAL( func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, func(data []byte) (proto.ChangelogEntry, error) { var e proto.ChangelogEntry @@ -128,6 +128,9 @@ func OpenDB(dataDir string, config config.StateStoreConfig) (*Database, error) { PruneInterval: time.Duration(config.PruneIntervalSeconds) * time.Second, }, ) + if err != nil { + panic(err) + } database.streamHandler = streamHandler go database.writeAsyncInBackground() From 24749b26887f5285eede0c3607ca1c8697503a91 Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Thu, 8 Jan 2026 14:32:41 -0800 Subject: [PATCH 06/35] Extract the truncation logic out --- sei-db/state_db/sc/memiavl/db.go | 86 +++++++-- sei-db/state_db/sc/memiavl/db_test.go | 239 ++++++++++++++++++++++++ sei-db/state_db/sc/memiavl/multitree.go | 54 +++--- sei-db/state_db/sc/store.go | 56 +++++- sei-db/state_db/sc/store_test.go | 182 ++++++++++++++++++ sei-db/wal/generic_wal/wal.go | 45 ++++- 6 files changed, 614 insertions(+), 48 deletions(-) diff --git a/sei-db/state_db/sc/memiavl/db.go b/sei-db/state_db/sc/memiavl/db.go index 120bee83ee..78d9e223d4 100644 --- a/sei-db/state_db/sc/memiavl/db.go +++ b/sei-db/state_db/sc/memiavl/db.go @@ -73,6 +73,10 @@ type DB struct { // the changelog stream persists all the changesets (managed by upper layer) streamHandler types.GenericWAL[proto.ChangelogEntry] + // walIndexDelta is the difference: version - walIndex for any entry. + // Since both WAL indices and versions are strictly contiguous, this delta is constant. + // Computed once when opening the DB from the first WAL entry. + walIndexDelta int64 // The assumptions to concurrency: // - The methods on DB are protected by a mutex @@ -198,6 +202,17 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * // Use WAL from options (managed by upper layer like CommitStore) streamHandler := opts.WAL + // Compute WAL index delta (only needed once per DB open) + var walIndexDelta int64 + var walHasEntries bool + if streamHandler != nil { + var err error + walIndexDelta, walHasEntries, err = computeWALIndexDelta(streamHandler) + if err != nil { + return nil, fmt.Errorf("failed to compute WAL index delta: %w", err) + } + } + // Replay WAL to catch up to target version (if WAL is provided) if streamHandler != nil && (targetVersion == 0 || targetVersion > mtree.Version()) { logger.Info("Start catching up and replaying the MemIAVL changelog file") @@ -221,12 +236,15 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * } } - // truncate the rlog file (if WAL is provided) - if streamHandler != nil { + // truncate the rlog file (if WAL is provided and has entries) + if streamHandler != nil && walHasEntries { logger.Info("truncate rlog after version: %d", targetVersion) - truncateIndex := utils.VersionToIndex(targetVersion, mtree.initialVersion.Load()) - if err := streamHandler.TruncateAfter(truncateIndex); err != nil { - return nil, fmt.Errorf("fail to truncate rlog file: %w", err) + // Use O(1) conversion: walIndex = version - delta + truncateIndex := targetVersion - walIndexDelta + if truncateIndex > 0 { + if err := streamHandler.TruncateAfter(uint64(truncateIndex)); err != nil { + return nil, fmt.Errorf("fail to truncate rlog file: %w", err) + } } } @@ -262,6 +280,7 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * fileLock: fileLock, readOnly: opts.ReadOnly, streamHandler: streamHandler, + walIndexDelta: walIndexDelta, snapshotKeepRecent: opts.SnapshotKeepRecent, snapshotInterval: opts.SnapshotInterval, snapshotMinTimeInterval: opts.SnapshotMinTimeInterval, @@ -290,6 +309,12 @@ func (db *DB) GetWAL() types.GenericWAL[proto.ChangelogEntry] { return db.streamHandler } +// GetWALIndexDelta returns the precomputed delta between version and WAL index. +// This allows O(1) conversion: version = walIndex + delta, walIndex = version - delta +func (db *DB) GetWALIndexDelta() int64 { + return db.walIndexDelta +} + func removeTmpDirs(rootDir string) error { entries, err := os.ReadDir(rootDir) if err != nil { @@ -457,7 +482,8 @@ func (db *DB) checkBackgroundSnapshotRewrite() error { return nil } -// pruneSnapshot prune the old snapshots +// pruneSnapshots prunes old snapshots, keeping only snapshotKeepRecent recent ones. +// Note: WAL truncation is now handled by CommitStore after each commit. func (db *DB) pruneSnapshots() { // wait until last prune finish db.pruneSnapshotLock.Lock() @@ -493,19 +519,47 @@ func (db *DB) pruneSnapshots() { db.logger.Error("fail to prune snapshots", "err", err) return } +} - // truncate Rlog until the earliest remaining snapshot (only if WAL is set) - if db.streamHandler != nil { - earliestVersion, err := GetEarliestVersion(db.dir) - if err != nil { - db.logger.Error("failed to find first snapshot", "err", err) - return - } +// computeWALIndexDelta computes the constant delta between version and WAL index. +// Since both are strictly contiguous, we only need to read one entry. +// Returns (delta, hasEntries, error). hasEntries is false if WAL is empty. +func computeWALIndexDelta(stream types.GenericWAL[proto.ChangelogEntry]) (int64, bool, error) { + firstIndex, err := stream.FirstOffset() + if err != nil { + return 0, false, err + } + if firstIndex == 0 { + return 0, false, nil // empty WAL + } - if err := db.streamHandler.TruncateBefore(utils.VersionToIndex(earliestVersion+1, db.initialVersion.Load())); err != nil { - db.logger.Error("failed to truncate rlog", "err", err, "version", earliestVersion+1) - } + // Read just the first entry to compute delta + var firstVersion int64 + err = stream.Replay(firstIndex, firstIndex, func(index uint64, entry proto.ChangelogEntry) error { + firstVersion = entry.Version + return nil + }) + if err != nil { + return 0, false, err } + + // delta = version - index, so for any entry: version = index + delta + return firstVersion - int64(firstIndex), true, nil +} + +// versionToWALIndex converts a version to its corresponding WAL index using the precomputed delta. +// Returns 0 if the version would result in an invalid (negative or zero) index. +func (db *DB) versionToWALIndex(version int64) uint64 { + index := version - db.walIndexDelta + if index <= 0 { + return 0 + } + return uint64(index) +} + +// walIndexToVersion converts a WAL index to its corresponding version using the precomputed delta. +func (db *DB) walIndexToVersion(index uint64) int64 { + return int64(index) + db.walIndexDelta } // Commit wraps SaveVersion to bump the version and finalize the tree state. diff --git a/sei-db/state_db/sc/memiavl/db_test.go b/sei-db/state_db/sc/memiavl/db_test.go index e9b64f83c6..6e55ce7a79 100644 --- a/sei-db/state_db/sc/memiavl/db_test.go +++ b/sei-db/state_db/sc/memiavl/db_test.go @@ -582,6 +582,245 @@ func TestRlogIndexConversion(t *testing.T) { } } +// TestWALIndexDeltaComputation tests the O(1) delta-based WAL index conversion. +// This is critical because: +// 1. WAL indices and versions are both strictly contiguous +// 2. We compute delta once from the first WAL entry: delta = firstVersion - firstIndex +// 3. All conversions are then O(1): walIndex = version - delta +func TestWALIndexDeltaComputation(t *testing.T) { + testCases := []struct { + name string + initialVersion uint32 + numVersions int + rollbackTo int64 + }{ + { + name: "delta=0 (version starts at 1)", + initialVersion: 0, + numVersions: 5, + rollbackTo: 3, + }, + { + name: "delta=9 (version starts at 10)", + initialVersion: 10, + numVersions: 5, + rollbackTo: 12, + }, + { + name: "delta=99 (version starts at 100)", + initialVersion: 100, + numVersions: 5, + rollbackTo: 102, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + initialStores := []string{"test"} + + // Create WAL + wal := createTestWAL(t, dir) + + // Open DB with initial version + db, err := OpenDB(logger.NewNopLogger(), 0, Options{ + Dir: dir, + CreateIfMissing: true, + InitialStores: initialStores, + InitialVersion: tc.initialVersion, + WAL: wal, + }) + require.NoError(t, err) + + // Commit multiple versions + for i := 0; i < tc.numVersions; i++ { + cs := []*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key"), Value: []byte("value" + strconv.Itoa(i))}, + }, + }, + }, + } + require.NoError(t, db.ApplyChangeSets(cs)) + + // First entry needs upgrades for replay + var upgrades []*proto.TreeNameUpgrade + if i == 0 { + upgrades = initialUpgrades(initialStores) + } + writeToWAL(t, db, cs, upgrades) + _, err = db.Commit() + require.NoError(t, err) + } + + // When initialVersion=0, first commit is version 1, so after N commits: version = N + // When initialVersion=X, first commit is version X, so after N commits: version = X + N - 1 + expectedVersion := int64(tc.numVersions) + if tc.initialVersion > 0 { + expectedVersion = int64(tc.initialVersion) + int64(tc.numVersions) - 1 + } + require.Equal(t, expectedVersion, db.Version()) + + // Note: walIndexDelta is 0 here because it was computed at open time when WAL was empty. + // We'll verify the correct delta after reopening. + + require.NoError(t, db.Close()) + require.NoError(t, wal.Close()) + + // Reopen to verify delta is computed correctly from WAL entries + walReopen := createTestWAL(t, dir) + dbReopen, err := OpenDB(logger.NewNopLogger(), 0, Options{ + Dir: dir, + WAL: walReopen, + }) + require.NoError(t, err) + + // Now verify delta is computed correctly + // delta = firstVersion - firstIndex + // When initialVersion=0: firstVersion = 1, firstIndex = 1, delta = 0 + // When initialVersion=X: firstVersion = X, firstIndex = 1, delta = X - 1 + expectedDelta := int64(0) + if tc.initialVersion > 0 { + expectedDelta = int64(tc.initialVersion) - 1 + } + require.Equal(t, expectedDelta, dbReopen.walIndexDelta, "WAL index delta should be computed correctly") + + // Test versionToWALIndex + for i := 0; i < tc.numVersions; i++ { + var version int64 + if tc.initialVersion == 0 { + version = int64(i + 1) // versions: 1, 2, 3, 4, 5 + } else { + version = int64(tc.initialVersion) + int64(i) // versions: 10, 11, 12, 13, 14 + } + expectedIndex := uint64(i + 1) // WAL indices: 1, 2, 3, 4, 5 + require.Equal(t, expectedIndex, dbReopen.versionToWALIndex(version), + "versionToWALIndex(%d) should return %d", version, expectedIndex) + } + + require.NoError(t, dbReopen.Close()) + require.NoError(t, walReopen.Close()) + + // Now test rollback with LoadForOverwriting + wal2 := createTestWAL(t, dir) + db2, err := OpenDB(logger.NewNopLogger(), tc.rollbackTo, Options{ + Dir: dir, + LoadForOverwriting: true, + WAL: wal2, + }) + require.NoError(t, err) + + // Verify rollback worked + require.Equal(t, tc.rollbackTo, db2.Version(), "Version should be rolled back to %d", tc.rollbackTo) + + // Verify WAL was truncated correctly + lastIndex, err := wal2.LastOffset() + require.NoError(t, err) + expectedLastIndex := uint64(tc.rollbackTo - db2.walIndexDelta) + require.Equal(t, expectedLastIndex, lastIndex, "WAL should be truncated to index %d", expectedLastIndex) + + require.NoError(t, db2.Close()) + require.NoError(t, wal2.Close()) + + // Reopen without LoadForOverwriting to verify persistence + wal3 := createTestWAL(t, dir) + db3, err := OpenDB(logger.NewNopLogger(), 0, Options{ + Dir: dir, + WAL: wal3, + }) + require.NoError(t, err) + require.Equal(t, tc.rollbackTo, db3.Version(), "Version should persist as %d after reopen", tc.rollbackTo) + + require.NoError(t, db3.Close()) + require.NoError(t, wal3.Close()) + }) + } +} + +// TestWALIndexDeltaWithZeroDelta specifically tests the case where delta=0. +// This was a bug where `walIndexDelta != 0` condition incorrectly skipped truncation +// when versions started at 1 (making delta = 1 - 1 = 0). +func TestWALIndexDeltaWithZeroDelta(t *testing.T) { + dir := t.TempDir() + initialStores := []string{"test"} + + wal := createTestWAL(t, dir) + + // Create DB with default initial version (0, so versions start at 1) + db, err := OpenDB(logger.NewNopLogger(), 0, Options{ + Dir: dir, + CreateIfMissing: true, + InitialStores: initialStores, + WAL: wal, + }) + require.NoError(t, err) + + // Commit 5 versions (1, 2, 3, 4, 5) + for i := 0; i < 5; i++ { + cs := []*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key"), Value: []byte("value" + strconv.Itoa(i))}, + }, + }, + }, + } + require.NoError(t, db.ApplyChangeSets(cs)) + + var upgrades []*proto.TreeNameUpgrade + if i == 0 { + upgrades = initialUpgrades(initialStores) + } + writeToWAL(t, db, cs, upgrades) + _, err = db.Commit() + require.NoError(t, err) + } + + require.Equal(t, int64(5), db.Version()) + // Critical: delta should be 0 (version 1 - index 1 = 0) + require.Equal(t, int64(0), db.walIndexDelta, "Delta should be 0 when versions start at 1") + + require.NoError(t, db.Close()) + require.NoError(t, wal.Close()) + + // Rollback to version 3 + wal2 := createTestWAL(t, dir) + db2, err := OpenDB(logger.NewNopLogger(), 3, Options{ + Dir: dir, + LoadForOverwriting: true, + WAL: wal2, + }) + require.NoError(t, err) + + // This is the key assertion that would have failed with the bug + require.Equal(t, int64(3), db2.Version(), "Rollback should work even when delta=0") + + // Verify WAL truncation + lastIndex, err := wal2.LastOffset() + require.NoError(t, err) + require.Equal(t, uint64(3), lastIndex, "WAL should be truncated to index 3") + + require.NoError(t, db2.Close()) + require.NoError(t, wal2.Close()) + + // Verify rollback persisted after reopen + wal3 := createTestWAL(t, dir) + db3, err := OpenDB(logger.NewNopLogger(), 0, Options{ + Dir: dir, + WAL: wal3, + }) + require.NoError(t, err) + require.Equal(t, int64(3), db3.Version(), "Rollback should persist after reopen") + + require.NoError(t, db3.Close()) + require.NoError(t, wal3.Close()) +} + func TestEmptyValue(t *testing.T) { dir := t.TempDir() initialStores := []string{"test"} diff --git a/sei-db/state_db/sc/memiavl/multitree.go b/sei-db/state_db/sc/memiavl/multitree.go index 86b9184d8e..50a57db9f8 100644 --- a/sei-db/state_db/sc/memiavl/multitree.go +++ b/sei-db/state_db/sc/memiavl/multitree.go @@ -358,41 +358,48 @@ func (t *MultiTree) UpdateCommitInfo() { } // Catchup replay the new entries in the Rlog file on the tree to catch up to the target or latest version. +// This function iterates through actual WAL entries and filters by the Version field stored in each entry, +// rather than assuming WAL index equals VersionToIndex(version). This handles cases where WAL indices +// don't match version mapping (e.g., after state sync, WAL pruning, etc.). func (t *MultiTree) Catchup(ctx context.Context, stream types.GenericWAL[proto.ChangelogEntry], endVersion int64) error { startTime := time.Now() + + // Get actual WAL index range + firstIndex, err := stream.FirstOffset() + if err != nil { + return fmt.Errorf("read rlog first index failed, %w", err) + } lastIndex, err := stream.LastOffset() if err != nil { return fmt.Errorf("read rlog last index failed, %w", err) } - iv := t.initialVersion.Load() - firstIndex := utils.VersionToIndex(utils.NextVersion(t.Version(), iv), iv) - if firstIndex > lastIndex { - // already up-to-date + // Empty WAL - nothing to replay + if lastIndex == 0 || firstIndex > lastIndex { return nil } - endIndex := lastIndex - if endVersion != 0 { - endIndex = utils.VersionToIndex(endVersion, iv) - } - - if endIndex < firstIndex { - return fmt.Errorf("target index %d is pruned", endIndex) - } - - if endIndex > lastIndex { - return fmt.Errorf("target index %d is in the future, latest index: %d", endIndex, lastIndex) - } + currentVersion := t.Version() var replayCount = 0 - err = stream.Replay(firstIndex, endIndex, func(index uint64, entry proto.ChangelogEntry) error { + err = stream.Replay(firstIndex, lastIndex, func(index uint64, entry proto.ChangelogEntry) error { // Check for cancellation select { case <-ctx.Done(): return ctx.Err() default: } + + // Filter by version: only apply entries newer than current tree version + if entry.Version <= currentVersion { + return nil // skip entries we already have + } + + // If endVersion is specified, stop at that version + if endVersion != 0 && entry.Version > endVersion { + return nil // skip entries beyond target + } + if err := t.ApplyUpgrades(entry.Upgrades); err != nil { return err } @@ -407,7 +414,7 @@ func (t *MultiTree) Catchup(ctx context.Context, stream types.GenericWAL[proto.C tree.ApplyChangeSetAsync(iavl.ChangeSet{}) } } - t.lastCommitInfo.Version = utils.NextVersion(t.lastCommitInfo.Version, t.initialVersion.Load()) + t.lastCommitInfo.Version = entry.Version t.lastCommitInfo.StoreInfos = []proto.StoreInfo{} replayCount++ if replayCount%1000 == 0 { @@ -423,11 +430,14 @@ func (t *MultiTree) Catchup(ctx context.Context, stream types.GenericWAL[proto.C if err != nil { return err } - t.UpdateCommitInfo() - replayElapsed := time.Since(startTime).Seconds() - t.logger.Info(fmt.Sprintf("Total replayed %d entries in %.1fs (%.1f entries/sec).\n", - replayCount, replayElapsed, float64(replayCount)/replayElapsed)) + if replayCount > 0 { + t.UpdateCommitInfo() + replayElapsed := time.Since(startTime).Seconds() + t.logger.Info(fmt.Sprintf("Total replayed %d entries in %.1fs (%.1f entries/sec).\n", + replayCount, replayElapsed, float64(replayCount)/replayElapsed)) + } + return nil } diff --git a/sei-db/state_db/sc/store.go b/sei-db/state_db/sc/store.go index ced57eae0c..292e5daa91 100644 --- a/sei-db/state_db/sc/store.go +++ b/sei-db/state_db/sc/store.go @@ -188,7 +188,61 @@ func (cs *CommitStore) Commit() (int64, error) { cs.pendingLogEntry = proto.ChangelogEntry{} // Now commit to the tree - return cs.db.Commit() + version, err := cs.db.Commit() + if err != nil { + return 0, err + } + + // Try to truncate WAL after commit (non-blocking, errors are logged) + cs.tryTruncateWAL() + + return version, nil +} + +// tryTruncateWAL checks if WAL can be truncated based on earliest snapshot version. +// This is safe because we only need WAL entries for versions newer than the earliest snapshot. +// Called after each commit to keep WAL size bounded. +func (cs *CommitStore) tryTruncateWAL() { + if cs.wal == nil { + return + } + + // Get WAL's first index + firstWALIndex, err := cs.wal.FirstOffset() + if err != nil { + cs.logger.Error("failed to get WAL first offset", "err", err) + return + } + if firstWALIndex == 0 { + return // empty WAL, nothing to truncate + } + + // Get earliest snapshot version + earliestSnapshotVersion, err := cs.GetEarliestVersion() + if err != nil { + // This can happen if no snapshots exist yet, which is normal + return + } + + // Compute WAL's earliest version using delta + // delta = firstVersion - firstIndex, so firstVersion = firstIndex + delta + walDelta := cs.db.GetWALIndexDelta() + walEarliestVersion := int64(firstWALIndex) + walDelta + + // If WAL's earliest version is less than snapshot's earliest version, + // we can safely truncate those WAL entries + if walEarliestVersion < earliestSnapshotVersion { + // Truncate WAL entries with version < earliestSnapshotVersion + // WAL index for earliestSnapshotVersion = earliestSnapshotVersion - delta + truncateIndex := earliestSnapshotVersion - walDelta + if truncateIndex > int64(firstWALIndex) { + if err := cs.wal.TruncateBefore(uint64(truncateIndex)); err != nil { + cs.logger.Error("failed to truncate WAL", "err", err, "truncateIndex", truncateIndex) + } else { + cs.logger.Debug("truncated WAL", "beforeIndex", truncateIndex, "earliestSnapshotVersion", earliestSnapshotVersion) + } + } + } } func (cs *CommitStore) Version() int64 { diff --git a/sei-db/state_db/sc/store_test.go b/sei-db/state_db/sc/store_test.go index bcaea708ab..ea00433dee 100644 --- a/sei-db/state_db/sc/store_test.go +++ b/sei-db/state_db/sc/store_test.go @@ -865,3 +865,185 @@ func TestReadOnlyCopyCannotCommit(t *testing.T) { require.NoError(t, roCS.Close()) require.NoError(t, cs.Close()) } + +// TestWALTruncationOnCommit tests that WAL is automatically truncated after commits +// when the earliest snapshot version advances past WAL entries. +func TestWALTruncationOnCommit(t *testing.T) { + dir := t.TempDir() + + // Configure with snapshot interval to trigger snapshot creation + cfg := config.StateCommitConfig{ + SnapshotInterval: 2, // Create snapshot every 2 blocks + SnapshotKeepRecent: 1, // Keep only 1 recent snapshot + } + cs := NewCommitStore(dir, logger.NewNopLogger(), cfg) + cs.Initialize([]string{"test"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + + // Commit multiple versions to trigger snapshot creation and WAL truncation + for i := 0; i < 10; i++ { + if i == 0 { + cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} + } + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key"), Value: []byte("value" + string(rune('0'+i)))}, + }, + }, + }, + }) + require.NoError(t, err) + _, err = cs.Commit() + require.NoError(t, err) + } + + // Verify current version + require.Equal(t, int64(10), cs.Version()) + + // Get WAL state + firstWALIndex, err := cs.wal.FirstOffset() + require.NoError(t, err) + + // Get earliest snapshot version - may not exist yet if snapshots are async + earliestSnapshot, err := cs.GetEarliestVersion() + if err != nil { + // No snapshots yet (async snapshot creation), that's okay for this test + t.Logf("No snapshots created yet (async): %v", err) + require.NoError(t, cs.Close()) + return + } + + // WAL's first index should be greater than 1 if truncation happened + // (meaning early entries were removed) + // The exact value depends on snapshot creation timing and pruning + t.Logf("WAL first index: %d, earliest snapshot: %d", firstWALIndex, earliestSnapshot) + + // Key assertion: WAL entries before earliest snapshot should be truncated + // WAL version = index + delta, so WAL first version = firstIndex + delta + walDelta := cs.db.GetWALIndexDelta() + walFirstVersion := int64(firstWALIndex) + walDelta + require.GreaterOrEqual(t, walFirstVersion, earliestSnapshot, + "WAL first version should be >= earliest snapshot version after truncation") + + require.NoError(t, cs.Close()) +} + +// TestWALTruncationWithNoSnapshots tests that WAL truncation handles the case +// when no snapshots exist yet (should not panic or error). +func TestWALTruncationWithNoSnapshots(t *testing.T) { + dir := t.TempDir() + + // No snapshot interval configured, so no snapshots will be created + cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs.Initialize([]string{"test"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + + // Commit a version + cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key"), Value: []byte("value")}, + }, + }, + }, + }) + require.NoError(t, err) + + // Commit should succeed even though no snapshots exist + // (tryTruncateWAL should handle this gracefully) + _, err = cs.Commit() + require.NoError(t, err) + + // WAL should still have entries + firstIndex, err := cs.wal.FirstOffset() + require.NoError(t, err) + require.Equal(t, uint64(1), firstIndex, "WAL should not be truncated when no snapshots exist") + + require.NoError(t, cs.Close()) +} + +// TestWALTruncationDelta tests that WAL truncation correctly uses the delta +// for version-to-index conversion with non-zero initial version. +func TestWALTruncationDelta(t *testing.T) { + dir := t.TempDir() + + cfg := config.StateCommitConfig{ + SnapshotInterval: 2, + SnapshotKeepRecent: 1, + } + cs := NewCommitStore(dir, logger.NewNopLogger(), cfg) + cs.Initialize([]string{"test"}) + + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + + // Set initial version to 100 + err = cs.SetInitialVersion(100) + require.NoError(t, err) + + // Commit multiple versions + for i := 0; i < 10; i++ { + if i == 0 { + cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} + } + err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key"), Value: []byte("value" + string(rune('0'+i)))}, + }, + }, + }, + }) + require.NoError(t, err) + _, err = cs.Commit() + require.NoError(t, err) + } + + // Verify version (should be 100 + 9 = 109) + require.Equal(t, int64(109), cs.Version()) + + // Close and reopen to verify delta is computed correctly from WAL + require.NoError(t, cs.Close()) + + // Reopen + cs2 := NewCommitStore(dir, logger.NewNopLogger(), cfg) + _, err = cs2.LoadVersion(0, false) + require.NoError(t, err) + + // Now verify delta is correct (computed from WAL entries) + walDelta := cs2.db.GetWALIndexDelta() + require.Equal(t, int64(99), walDelta, "Delta should be 99 (firstVersion 100 - firstIndex 1)") + + // Verify WAL truncation respects delta + firstWALIndex, err := cs2.wal.FirstOffset() + require.NoError(t, err) + + // Get earliest snapshot version - may not exist yet if snapshots are async + earliestSnapshot, err := cs2.GetEarliestVersion() + if err != nil { + t.Logf("No snapshots created yet: %v", err) + require.NoError(t, cs2.Close()) + return + } + + walFirstVersion := int64(firstWALIndex) + walDelta + t.Logf("WAL first index: %d, WAL first version: %d, earliest snapshot: %d", + firstWALIndex, walFirstVersion, earliestSnapshot) + + require.GreaterOrEqual(t, walFirstVersion, earliestSnapshot, + "WAL first version should be >= earliest snapshot version") + + require.NoError(t, cs2.Close()) +} diff --git a/sei-db/wal/generic_wal/wal.go b/sei-db/wal/generic_wal/wal.go index cbb32e38ed..d910119b2b 100644 --- a/sei-db/wal/generic_wal/wal.go +++ b/sei-db/wal/generic_wal/wal.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "sync" "sync/atomic" "time" @@ -27,6 +28,8 @@ type WAL[T any] struct { errSignal chan error nextOffset uint64 isClosed atomic.Bool + closeCh chan struct{} // signals shutdown to background goroutines + wg sync.WaitGroup // tracks background goroutines (pruning) } type Config struct { @@ -71,6 +74,7 @@ func NewWAL[T any]( logger: logger, marshal: marshal, unmarshal: unmarshal, + closeCh: make(chan struct{}), // isClosed is zero-initialized to false (atomic.Bool) } // Finding the nextOffset to write @@ -81,7 +85,11 @@ func NewWAL[T any]( w.nextOffset = lastIndex + 1 // Start the auto pruning goroutine if config.KeepRecent > 0 { - go w.StartPruning(config.KeepRecent, config.PruneInterval) + w.wg.Add(1) + go func() { + defer w.wg.Done() + w.StartPruning(config.KeepRecent, config.PruneInterval) + }() } return w, nil @@ -220,21 +228,40 @@ func (walLog *WAL[T]) Replay(start uint64, end uint64, processFn func(index uint } func (walLog *WAL[T]) StartPruning(keepRecent uint64, pruneInterval time.Duration) { - for !walLog.isClosed.Load() { - lastIndex, _ := walLog.log.LastIndex() - firstIndex, _ := walLog.log.FirstIndex() - if lastIndex > keepRecent && (lastIndex-keepRecent) > firstIndex { - prunePos := lastIndex - keepRecent - if err := walLog.TruncateBefore(prunePos); err != nil { - walLog.logger.Error(fmt.Sprintf("failed to prune changelog till index %d", prunePos), "err", err) + // Use a minimum interval to avoid tight loops + if pruneInterval <= 0 { + pruneInterval = time.Minute // Default to 1 minute if not specified + } + + ticker := time.NewTicker(pruneInterval) + defer ticker.Stop() + + for { + select { + case <-walLog.closeCh: + return + case <-ticker.C: + lastIndex, _ := walLog.log.LastIndex() + firstIndex, _ := walLog.log.FirstIndex() + if lastIndex > keepRecent && (lastIndex-keepRecent) > firstIndex { + prunePos := lastIndex - keepRecent + if err := walLog.TruncateBefore(prunePos); err != nil { + walLog.logger.Error(fmt.Sprintf("failed to prune changelog till index %d", prunePos), "err", err) + } } } - time.Sleep(pruneInterval) } } func (walLog *WAL[T]) Close() error { walLog.isClosed.Store(true) + + // Signal background goroutines to stop + close(walLog.closeCh) + + // Wait for background goroutines (pruning) to finish before closing resources + walLog.wg.Wait() + var err error if walLog.writeChannel != nil { close(walLog.writeChannel) From 39f8db8add50ee76f6eab516336acb110072e9fc Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Thu, 8 Jan 2026 14:44:09 -0800 Subject: [PATCH 07/35] Flatten WAL packages --- sei-db/db_engine/pebbledb/mvcc/db.go | 15 ++++++++------- sei-db/db_engine/rocksdb/mvcc/db.go | 11 ++++++----- sei-db/state_db/sc/memiavl/db.go | 11 +++++------ sei-db/state_db/sc/memiavl/db_test.go | 12 ++++++------ sei-db/state_db/sc/memiavl/multitree.go | 7 ++++--- sei-db/state_db/sc/memiavl/opts.go | 4 ++-- sei-db/state_db/sc/store.go | 11 +++++------ sei-db/state_db/ss/store.go | 6 +++--- .../cmd/seidb/operations/replay_changelog.go | 9 +++++---- sei-db/wal/{generic_wal => }/subscriber.go | 5 ++--- sei-db/wal/{types => }/types.go | 4 ++-- sei-db/wal/{generic_wal => }/utils.go | 2 +- sei-db/wal/{generic_wal => }/wal.go | 11 +++++------ sei-db/wal/{generic_wal => }/wal_test.go | 2 +- 14 files changed, 55 insertions(+), 55 deletions(-) rename sei-db/wal/{generic_wal => }/subscriber.go (92%) rename sei-db/wal/{types => }/types.go (96%) rename sei-db/wal/{generic_wal => }/utils.go (99%) rename sei-db/wal/{generic_wal => }/wal.go (97%) rename sei-db/wal/{generic_wal => }/wal_test.go (99%) diff --git a/sei-db/db_engine/pebbledb/mvcc/db.go b/sei-db/db_engine/pebbledb/mvcc/db.go index 4783d1e3f0..6bdcdd426c 100644 --- a/sei-db/db_engine/pebbledb/mvcc/db.go +++ b/sei-db/db_engine/pebbledb/mvcc/db.go @@ -15,6 +15,10 @@ import ( "github.com/armon/go-metrics" "github.com/cockroachdb/pebble" "github.com/cockroachdb/pebble/bloom" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + "golang.org/x/exp/slices" + errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/common/utils" @@ -22,10 +26,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/types" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/util" - "github.com/sei-protocol/sei-chain/sei-db/wal/generic_wal" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/metric" - "golang.org/x/exp/slices" + "github.com/sei-protocol/sei-chain/sei-db/wal" ) const ( @@ -71,7 +72,7 @@ type Database struct { storeKeyDirty sync.Map // Changelog used to support async write - streamHandler *generic_wal.WAL[proto.ChangelogEntry] + streamHandler *wal.WAL[proto.ChangelogEntry] // Pending changes to be written to the DB pendingChanges chan VersionedChangesets @@ -169,7 +170,7 @@ func OpenDB(dataDir string, config config.StateStoreConfig) (*Database, error) { _ = db.Close() return nil, errors.New("KeepRecent must be non-negative") } - streamHandler, err := generic_wal.NewWAL( + streamHandler, err := wal.NewWAL( func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, func(data []byte) (proto.ChangelogEntry, error) { var e proto.ChangelogEntry @@ -178,7 +179,7 @@ func OpenDB(dataDir string, config config.StateStoreConfig) (*Database, error) { }, logger.NewNopLogger(), utils.GetChangelogPath(dataDir), - generic_wal.Config{ + wal.Config{ DisableFsync: true, ZeroCopy: true, KeepRecent: uint64(config.KeepRecent), diff --git a/sei-db/db_engine/rocksdb/mvcc/db.go b/sei-db/db_engine/rocksdb/mvcc/db.go index f7716f5440..752a1937ed 100644 --- a/sei-db/db_engine/rocksdb/mvcc/db.go +++ b/sei-db/db_engine/rocksdb/mvcc/db.go @@ -13,6 +13,8 @@ import ( "time" "github.com/linxGnu/grocksdb" + "golang.org/x/exp/slices" + "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/common/utils" @@ -20,8 +22,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/types" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/util" - "github.com/sei-protocol/sei-chain/sei-db/wal/generic_wal" - "golang.org/x/exp/slices" + "github.com/sei-protocol/sei-chain/sei-db/wal" ) const ( @@ -65,7 +66,7 @@ type Database struct { asyncWriteWG sync.WaitGroup // Changelog used to support async write - streamHandler *generic_wal.WAL[proto.ChangelogEntry] + streamHandler *wal.WAL[proto.ChangelogEntry] // Pending changes to be written to the DB pendingChanges chan VersionedChangesets @@ -112,7 +113,7 @@ func OpenDB(dataDir string, config config.StateStoreConfig) (*Database, error) { } database.latestVersion.Store(latestVersion) - streamHandler, err := generic_wal.NewWAL( + streamHandler, err := wal.NewWAL( func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, func(data []byte) (proto.ChangelogEntry, error) { var e proto.ChangelogEntry @@ -121,7 +122,7 @@ func OpenDB(dataDir string, config config.StateStoreConfig) (*Database, error) { }, logger.NewNopLogger(), utils.GetChangelogPath(dataDir), - generic_wal.Config{ + wal.Config{ DisableFsync: true, ZeroCopy: true, KeepRecent: uint64(config.KeepRecent), diff --git a/sei-db/state_db/sc/memiavl/db.go b/sei-db/state_db/sc/memiavl/db.go index 78d9e223d4..2ac80cf35d 100644 --- a/sei-db/state_db/sc/memiavl/db.go +++ b/sei-db/state_db/sc/memiavl/db.go @@ -21,8 +21,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/common/utils" "github.com/sei-protocol/sei-chain/sei-db/proto" - "github.com/sei-protocol/sei-chain/sei-db/wal/generic_wal" - "github.com/sei-protocol/sei-chain/sei-db/wal/types" + "github.com/sei-protocol/sei-chain/sei-db/wal" iavl "github.com/sei-protocol/sei-chain/sei-iavl" ) @@ -72,7 +71,7 @@ type DB struct { pruneSnapshotLock sync.Mutex // the changelog stream persists all the changesets (managed by upper layer) - streamHandler types.GenericWAL[proto.ChangelogEntry] + streamHandler wal.GenericWAL[proto.ChangelogEntry] // walIndexDelta is the difference: version - walIndex for any entry. // Since both WAL indices and versions are strictly contiguous, this delta is constant. // Computed once when opening the DB from the first WAL entry. @@ -305,7 +304,7 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * // GetWAL returns the WAL handler for changelog operations. // Upper layer (CommitStore) uses this to write to WAL. -func (db *DB) GetWAL() types.GenericWAL[proto.ChangelogEntry] { +func (db *DB) GetWAL() wal.GenericWAL[proto.ChangelogEntry] { return db.streamHandler } @@ -524,7 +523,7 @@ func (db *DB) pruneSnapshots() { // computeWALIndexDelta computes the constant delta between version and WAL index. // Since both are strictly contiguous, we only need to read one entry. // Returns (delta, hasEntries, error). hasEntries is false if WAL is empty. -func computeWALIndexDelta(stream types.GenericWAL[proto.ChangelogEntry]) (int64, bool, error) { +func computeWALIndexDelta(stream wal.GenericWAL[proto.ChangelogEntry]) (int64, bool, error) { firstIndex, err := stream.FirstOffset() if err != nil { return 0, false, err @@ -1149,7 +1148,7 @@ func GetLatestVersion(dir string) (int64, error) { } return 0, err } - lastIndex, err := generic_wal.GetLastIndex(generic_wal.LogPath(dir)) + lastIndex, err := wal.GetLastIndex(wal.LogPath(dir)) if err != nil { return 0, err } diff --git a/sei-db/state_db/sc/memiavl/db_test.go b/sei-db/state_db/sc/memiavl/db_test.go index 6e55ce7a79..ca6df4b576 100644 --- a/sei-db/state_db/sc/memiavl/db_test.go +++ b/sei-db/state_db/sc/memiavl/db_test.go @@ -11,20 +11,20 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/common/utils" "github.com/sei-protocol/sei-chain/sei-db/proto" - "github.com/sei-protocol/sei-chain/sei-db/wal/generic_wal" - "github.com/sei-protocol/sei-chain/sei-db/wal/types" + "github.com/sei-protocol/sei-chain/sei-db/wal" iavl "github.com/sei-protocol/sei-chain/sei-iavl" - "github.com/stretchr/testify/require" ) // createTestWAL creates a WAL for testing purposes. -func createTestWAL(t *testing.T, dir string) types.GenericWAL[proto.ChangelogEntry] { +func createTestWAL(t *testing.T, dir string) wal.GenericWAL[proto.ChangelogEntry] { t.Helper() - wal, err := generic_wal.NewWAL( + wal, err := wal.NewWAL( func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, func(data []byte) (proto.ChangelogEntry, error) { var e proto.ChangelogEntry @@ -33,7 +33,7 @@ func createTestWAL(t *testing.T, dir string) types.GenericWAL[proto.ChangelogEnt }, logger.NewNopLogger(), utils.GetChangelogPath(dir), - generic_wal.Config{ + wal.Config{ DisableFsync: true, ZeroCopy: true, WriteBufferSize: 0, diff --git a/sei-db/state_db/sc/memiavl/multitree.go b/sei-db/state_db/sc/memiavl/multitree.go index 50a57db9f8..0c8fea15bd 100644 --- a/sei-db/state_db/sc/memiavl/multitree.go +++ b/sei-db/state_db/sc/memiavl/multitree.go @@ -12,13 +12,14 @@ import ( "time" "github.com/alitto/pond" + "golang.org/x/exp/slices" + "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/common/utils" "github.com/sei-protocol/sei-chain/sei-db/proto" - "github.com/sei-protocol/sei-chain/sei-db/wal/types" + "github.com/sei-protocol/sei-chain/sei-db/wal" iavl "github.com/sei-protocol/sei-chain/sei-iavl" - "golang.org/x/exp/slices" ) const ( @@ -361,7 +362,7 @@ func (t *MultiTree) UpdateCommitInfo() { // This function iterates through actual WAL entries and filters by the Version field stored in each entry, // rather than assuming WAL index equals VersionToIndex(version). This handles cases where WAL indices // don't match version mapping (e.g., after state sync, WAL pruning, etc.). -func (t *MultiTree) Catchup(ctx context.Context, stream types.GenericWAL[proto.ChangelogEntry], endVersion int64) error { +func (t *MultiTree) Catchup(ctx context.Context, stream wal.GenericWAL[proto.ChangelogEntry], endVersion int64) error { startTime := time.Now() // Get actual WAL index range diff --git a/sei-db/state_db/sc/memiavl/opts.go b/sei-db/state_db/sc/memiavl/opts.go index 0e3b3e8b0a..c2a714be72 100644 --- a/sei-db/state_db/sc/memiavl/opts.go +++ b/sei-db/state_db/sc/memiavl/opts.go @@ -8,7 +8,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/config" "github.com/sei-protocol/sei-chain/sei-db/proto" - "github.com/sei-protocol/sei-chain/sei-db/wal/types" + "github.com/sei-protocol/sei-chain/sei-db/wal" ) type Options struct { @@ -53,7 +53,7 @@ type Options struct { // WAL is the write-ahead log for changelog persistence and replay. // If nil, no WAL operations will be performed (read-only mode or tests). // The WAL is managed by the upper layer (CommitStore) and passed in. - WAL types.GenericWAL[proto.ChangelogEntry] + WAL wal.GenericWAL[proto.ChangelogEntry] } func (opts Options) Validate() error { diff --git a/sei-db/state_db/sc/store.go b/sei-db/state_db/sc/store.go index 292e5daa91..dbeec50e98 100644 --- a/sei-db/state_db/sc/store.go +++ b/sei-db/state_db/sc/store.go @@ -12,8 +12,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/memiavl" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" - "github.com/sei-protocol/sei-chain/sei-db/wal/generic_wal" - waltypes "github.com/sei-protocol/sei-chain/sei-db/wal/types" + "github.com/sei-protocol/sei-chain/sei-db/wal" ) var _ types.Committer = (*CommitStore)(nil) @@ -24,7 +23,7 @@ type CommitStore struct { opts memiavl.Options // WAL for changelog persistence (owned by CommitStore) - wal waltypes.GenericWAL[proto.ChangelogEntry] + wal wal.GenericWAL[proto.ChangelogEntry] // pending changes to be written to WAL on next Commit pendingLogEntry proto.ChangelogEntry @@ -55,8 +54,8 @@ func NewCommitStore(homeDir string, logger logger.Logger, config config.StateCom } // createWAL creates a new WAL instance for changelog persistence and replay. -func (cs *CommitStore) createWAL() (waltypes.GenericWAL[proto.ChangelogEntry], error) { - return generic_wal.NewWAL( +func (cs *CommitStore) createWAL() (wal.GenericWAL[proto.ChangelogEntry], error) { + return wal.NewWAL( func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, func(data []byte) (proto.ChangelogEntry, error) { var e proto.ChangelogEntry @@ -65,7 +64,7 @@ func (cs *CommitStore) createWAL() (waltypes.GenericWAL[proto.ChangelogEntry], e }, cs.logger, utils.GetChangelogPath(cs.opts.Dir), - generic_wal.Config{ + wal.Config{ DisableFsync: true, ZeroCopy: true, WriteBufferSize: cs.opts.AsyncCommitBuffer, diff --git a/sei-db/state_db/ss/store.go b/sei-db/state_db/ss/store.go index 0f884385bc..d65523b1d1 100644 --- a/sei-db/state_db/ss/store.go +++ b/sei-db/state_db/ss/store.go @@ -9,7 +9,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/pruning" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/types" - "github.com/sei-protocol/sei-chain/sei-db/wal/generic_wal" + "github.com/sei-protocol/sei-chain/sei-db/wal" ) type BackendType string @@ -61,7 +61,7 @@ func NewStateStore(logger logger.Logger, homeDir string, ssConfig config.StateSt func RecoverStateStore(logger logger.Logger, changelogPath string, stateStore types.StateStore) error { ssLatestVersion := stateStore.GetLatestVersion() logger.Info(fmt.Sprintf("Recovering from changelog %s with latest SS version %d", changelogPath, ssLatestVersion)) - streamHandler, err := generic_wal.NewWAL( + streamHandler, err := wal.NewWAL( func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, func(data []byte) (proto.ChangelogEntry, error) { var e proto.ChangelogEntry @@ -70,7 +70,7 @@ func RecoverStateStore(logger logger.Logger, changelogPath string, stateStore ty }, logger, changelogPath, - generic_wal.Config{}, + wal.Config{}, ) if err != nil { return err diff --git a/sei-db/tools/cmd/seidb/operations/replay_changelog.go b/sei-db/tools/cmd/seidb/operations/replay_changelog.go index 8719a53fe8..5ba6c02c8d 100644 --- a/sei-db/tools/cmd/seidb/operations/replay_changelog.go +++ b/sei-db/tools/cmd/seidb/operations/replay_changelog.go @@ -4,13 +4,14 @@ import ( "fmt" "path/filepath" + "github.com/spf13/cobra" + "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/config" "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/types" - "github.com/sei-protocol/sei-chain/sei-db/wal/generic_wal" - "github.com/spf13/cobra" + "github.com/sei-protocol/sei-chain/sei-db/wal" ) var ssStore types.StateStore @@ -41,7 +42,7 @@ func executeReplayChangelog(cmd *cobra.Command, _ []string) { } logDir := filepath.Join(dbDir, "changelog") - stream, err := generic_wal.NewWAL( + stream, err := wal.NewWAL( func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, func(data []byte) (proto.ChangelogEntry, error) { var e proto.ChangelogEntry @@ -50,7 +51,7 @@ func executeReplayChangelog(cmd *cobra.Command, _ []string) { }, logger.NewNopLogger(), logDir, - generic_wal.Config{}, + wal.Config{}, ) if err != nil { panic(err) diff --git a/sei-db/wal/generic_wal/subscriber.go b/sei-db/wal/subscriber.go similarity index 92% rename from sei-db/wal/generic_wal/subscriber.go rename to sei-db/wal/subscriber.go index 10683259ad..cdaa764082 100644 --- a/sei-db/wal/generic_wal/subscriber.go +++ b/sei-db/wal/subscriber.go @@ -1,13 +1,12 @@ -package generic_wal +package wal import ( "fmt" "github.com/sei-protocol/sei-chain/sei-db/proto" - "github.com/sei-protocol/sei-chain/sei-db/wal/types" ) -var _ types.Subscriber[proto.ChangelogEntry] = (*Subscriber)(nil) +var _ Processor[proto.ChangelogEntry] = (*Subscriber)(nil) type Subscriber struct { maxPendingSize int diff --git a/sei-db/wal/types/types.go b/sei-db/wal/types.go similarity index 96% rename from sei-db/wal/types/types.go rename to sei-db/wal/types.go index 6642110db7..7180daade1 100644 --- a/sei-db/wal/types/types.go +++ b/sei-db/wal/types.go @@ -1,4 +1,4 @@ -package types +package wal // MarshalFn is a function that serializes an entry to bytes. type MarshalFn[T any] func(entry T) ([]byte, error) @@ -35,7 +35,7 @@ type GenericWAL[T any] interface { Close() error } -type Subscriber[T any] interface { +type Processor[T any] interface { // Start starts the subscriber processing goroutine Start() diff --git a/sei-db/wal/generic_wal/utils.go b/sei-db/wal/utils.go similarity index 99% rename from sei-db/wal/generic_wal/utils.go rename to sei-db/wal/utils.go index f98810ac3b..d374f3a9e0 100644 --- a/sei-db/wal/generic_wal/utils.go +++ b/sei-db/wal/utils.go @@ -1,4 +1,4 @@ -package generic_wal +package wal import ( "bytes" diff --git a/sei-db/wal/generic_wal/wal.go b/sei-db/wal/wal.go similarity index 97% rename from sei-db/wal/generic_wal/wal.go rename to sei-db/wal/wal.go index d910119b2b..5ff87e3ad7 100644 --- a/sei-db/wal/generic_wal/wal.go +++ b/sei-db/wal/wal.go @@ -1,4 +1,4 @@ -package generic_wal +package wal import ( "errors" @@ -13,7 +13,6 @@ import ( errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/common/logger" - "github.com/sei-protocol/sei-chain/sei-db/wal/types" ) // WAL is a generic write-ahead log implementation. @@ -22,8 +21,8 @@ type WAL[T any] struct { log *wal.Log config Config logger logger.Logger - marshal types.MarshalFn[T] - unmarshal types.UnmarshalFn[T] + marshal MarshalFn[T] + unmarshal UnmarshalFn[T] writeChannel chan T errSignal chan error nextOffset uint64 @@ -54,8 +53,8 @@ type Config struct { // logger, dir, config, // ) func NewWAL[T any]( - marshal types.MarshalFn[T], - unmarshal types.UnmarshalFn[T], + marshal MarshalFn[T], + unmarshal UnmarshalFn[T], logger logger.Logger, dir string, config Config, diff --git a/sei-db/wal/generic_wal/wal_test.go b/sei-db/wal/wal_test.go similarity index 99% rename from sei-db/wal/generic_wal/wal_test.go rename to sei-db/wal/wal_test.go index 7adf39818c..869b61ae1b 100644 --- a/sei-db/wal/generic_wal/wal_test.go +++ b/sei-db/wal/wal_test.go @@ -1,4 +1,4 @@ -package generic_wal +package wal import ( "fmt" From bb0511ac93d6ce133d414d4294616e197e04a834 Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Thu, 8 Jan 2026 14:47:03 -0800 Subject: [PATCH 08/35] Fix lint --- sei-db/state_db/sc/store_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sei-db/state_db/sc/store_test.go b/sei-db/state_db/sc/store_test.go index ea00433dee..f9ea77a8da 100644 --- a/sei-db/state_db/sc/store_test.go +++ b/sei-db/state_db/sc/store_test.go @@ -4,11 +4,12 @@ import ( "math" "testing" + "github.com/stretchr/testify/require" + "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/config" "github.com/sei-protocol/sei-chain/sei-db/proto" iavl "github.com/sei-protocol/sei-chain/sei-iavl" - "github.com/stretchr/testify/require" ) func TestNewCommitStore(t *testing.T) { @@ -873,8 +874,8 @@ func TestWALTruncationOnCommit(t *testing.T) { // Configure with snapshot interval to trigger snapshot creation cfg := config.StateCommitConfig{ - SnapshotInterval: 2, // Create snapshot every 2 blocks - SnapshotKeepRecent: 1, // Keep only 1 recent snapshot + SnapshotInterval: 2, // Create snapshot every 2 blocks + SnapshotKeepRecent: 1, // Keep only 1 recent snapshot } cs := NewCommitStore(dir, logger.NewNopLogger(), cfg) cs.Initialize([]string{"test"}) From 456f02bcdb42480f2ecf1331fd4ed6187aff4e34 Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Thu, 8 Jan 2026 15:27:49 -0800 Subject: [PATCH 09/35] Fix go lint --- sei-db/state_db/sc/memiavl/db.go | 3 +++ sei-db/state_db/sc/store.go | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/sei-db/state_db/sc/memiavl/db.go b/sei-db/state_db/sc/memiavl/db.go index 2ac80cf35d..24679e5b60 100644 --- a/sei-db/state_db/sc/memiavl/db.go +++ b/sei-db/state_db/sc/memiavl/db.go @@ -543,6 +543,7 @@ func computeWALIndexDelta(stream wal.GenericWAL[proto.ChangelogEntry]) (int64, b } // delta = version - index, so for any entry: version = index + delta + // #nosec G115 -- WAL indices are always much smaller than MaxInt64 in practice return firstVersion - int64(firstIndex), true, nil } @@ -553,11 +554,13 @@ func (db *DB) versionToWALIndex(version int64) uint64 { if index <= 0 { return 0 } + // #nosec G115 -- index is guaranteed positive by the check above return uint64(index) } // walIndexToVersion converts a WAL index to its corresponding version using the precomputed delta. func (db *DB) walIndexToVersion(index uint64) int64 { + // #nosec G115 -- WAL indices are always much smaller than MaxInt64 in practice return int64(index) + db.walIndexDelta } diff --git a/sei-db/state_db/sc/store.go b/sei-db/state_db/sc/store.go index dbeec50e98..02635e1977 100644 --- a/sei-db/state_db/sc/store.go +++ b/sei-db/state_db/sc/store.go @@ -226,6 +226,7 @@ func (cs *CommitStore) tryTruncateWAL() { // Compute WAL's earliest version using delta // delta = firstVersion - firstIndex, so firstVersion = firstIndex + delta walDelta := cs.db.GetWALIndexDelta() + // #nosec G115 -- WAL indices are always much smaller than MaxInt64 in practice walEarliestVersion := int64(firstWALIndex) + walDelta // If WAL's earliest version is less than snapshot's earliest version, @@ -234,7 +235,8 @@ func (cs *CommitStore) tryTruncateWAL() { // Truncate WAL entries with version < earliestSnapshotVersion // WAL index for earliestSnapshotVersion = earliestSnapshotVersion - delta truncateIndex := earliestSnapshotVersion - walDelta - if truncateIndex > int64(firstWALIndex) { + // #nosec G115 -- truncateIndex is guaranteed > firstWALIndex (positive) by the outer check + if truncateIndex > int64(firstWALIndex) && truncateIndex > 0 { if err := cs.wal.TruncateBefore(uint64(truncateIndex)); err != nil { cs.logger.Error("failed to truncate WAL", "err", err, "truncateIndex", truncateIndex) } else { From 17f2752bd2a51c24f2d8677e30108d50cced637a Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Fri, 9 Jan 2026 09:04:56 -0800 Subject: [PATCH 10/35] Rename wal subscriber --- sei-db/wal/{subscriber.go => processor.go} | 18 +++++++++--------- sei-db/wal/types.go | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) rename sei-db/wal/{subscriber.go => processor.go} (80%) diff --git a/sei-db/wal/subscriber.go b/sei-db/wal/processor.go similarity index 80% rename from sei-db/wal/subscriber.go rename to sei-db/wal/processor.go index cdaa764082..cbb999bc87 100644 --- a/sei-db/wal/subscriber.go +++ b/sei-db/wal/processor.go @@ -6,9 +6,9 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/proto" ) -var _ Processor[proto.ChangelogEntry] = (*Subscriber)(nil) +var _ GenericWALProcessor[proto.ChangelogEntry] = (*Processor)(nil) -type Subscriber struct { +type Processor struct { maxPendingSize int chPendingEntries chan proto.ChangelogEntry errSignal chan error @@ -19,8 +19,8 @@ type Subscriber struct { func NewSubscriber( maxPendingSize int, processFn func(entry proto.ChangelogEntry) error, -) *Subscriber { - subscriber := &Subscriber{ +) *Processor { + subscriber := &Processor{ maxPendingSize: maxPendingSize, processFn: processFn, } @@ -28,13 +28,13 @@ func NewSubscriber( return subscriber } -func (s *Subscriber) Start() { +func (s *Processor) Start() { if s.maxPendingSize > 0 { s.startAsyncProcessing() } } -func (s *Subscriber) ProcessEntry(entry proto.ChangelogEntry) error { +func (s *Processor) ProcessEntry(entry proto.ChangelogEntry) error { if s.maxPendingSize <= 0 { return s.processFn(entry) } @@ -42,7 +42,7 @@ func (s *Subscriber) ProcessEntry(entry proto.ChangelogEntry) error { return s.CheckError() } -func (s *Subscriber) startAsyncProcessing() { +func (s *Processor) startAsyncProcessing() { if s.chPendingEntries == nil { s.chPendingEntries = make(chan proto.ChangelogEntry, s.maxPendingSize) s.errSignal = make(chan error) @@ -63,7 +63,7 @@ func (s *Subscriber) startAsyncProcessing() { } } -func (s *Subscriber) Close() error { +func (s *Processor) Close() error { if s.chPendingEntries == nil { return nil } @@ -76,7 +76,7 @@ func (s *Subscriber) Close() error { return err } -func (s *Subscriber) CheckError() error { +func (s *Processor) CheckError() error { select { case err := <-s.errSignal: // async wal writing failed, we need to abort the state machine diff --git a/sei-db/wal/types.go b/sei-db/wal/types.go index 7180daade1..5292e35694 100644 --- a/sei-db/wal/types.go +++ b/sei-db/wal/types.go @@ -35,7 +35,7 @@ type GenericWAL[T any] interface { Close() error } -type Processor[T any] interface { +type GenericWALProcessor[T any] interface { // Start starts the subscriber processing goroutine Start() From 6a1bc68d7776793716808570d7bb4a407f69bca9 Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Fri, 9 Jan 2026 14:16:54 -0800 Subject: [PATCH 11/35] Catch up from the correct range instead of earliest offset --- sei-db/state_db/sc/memiavl/db.go | 8 +++--- sei-db/state_db/sc/memiavl/db_test.go | 3 ++- sei-db/state_db/sc/memiavl/multitree.go | 33 ++++++++++++++++++------- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/sei-db/state_db/sc/memiavl/db.go b/sei-db/state_db/sc/memiavl/db.go index 24679e5b60..aab2672828 100644 --- a/sei-db/state_db/sc/memiavl/db.go +++ b/sei-db/state_db/sc/memiavl/db.go @@ -213,9 +213,9 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * } // Replay WAL to catch up to target version (if WAL is provided) - if streamHandler != nil && (targetVersion == 0 || targetVersion > mtree.Version()) { + if streamHandler != nil && walHasEntries && (targetVersion == 0 || targetVersion > mtree.Version()) { logger.Info("Start catching up and replaying the MemIAVL changelog file") - if err := mtree.Catchup(context.Background(), streamHandler, targetVersion); err != nil { + if err := mtree.Catchup(context.Background(), streamHandler, walIndexDelta, targetVersion); err != nil { return nil, err } logger.Info(fmt.Sprintf("Finished the replay and caught up to version %d", targetVersion)) @@ -460,7 +460,7 @@ func (db *DB) checkBackgroundSnapshotRewrite() error { // catchup the remaining entries in rlog (only if WAL is set) if db.streamHandler != nil { - if err := result.mtree.Catchup(context.Background(), db.streamHandler, 0); err != nil { + if err := result.mtree.Catchup(context.Background(), db.streamHandler, db.walIndexDelta, 0); err != nil { return fmt.Errorf("catchup failed: %w", err) } } @@ -828,7 +828,7 @@ func (db *DB) rewriteSnapshotBackground() error { // Only catch up if WAL is set (managed by upper layer) if db.streamHandler != nil { catchupStart := time.Now() - if err := mtree.Catchup(ctx, db.streamHandler, 0); err != nil { + if err := mtree.Catchup(ctx, db.streamHandler, db.walIndexDelta, 0); err != nil { cloned.logger.Error("failed to catchup after snapshot", "error", err) ch <- snapshotResult{err: err} return diff --git a/sei-db/state_db/sc/memiavl/db_test.go b/sei-db/state_db/sc/memiavl/db_test.go index ca6df4b576..b7f911a4ff 100644 --- a/sei-db/state_db/sc/memiavl/db_test.go +++ b/sei-db/state_db/sc/memiavl/db_test.go @@ -1105,7 +1105,8 @@ func TestCatchupWithCancelledContext(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() // Cancel immediately - err = mtree.Catchup(ctx, wal, 0) + // delta=0 for this test since we're just testing context cancellation + err = mtree.Catchup(ctx, wal, 0, 0) // If already caught up, no error; otherwise should get context.Canceled if err != nil { require.Equal(t, context.Canceled, err) diff --git a/sei-db/state_db/sc/memiavl/multitree.go b/sei-db/state_db/sc/memiavl/multitree.go index 0c8fea15bd..66e678b721 100644 --- a/sei-db/state_db/sc/memiavl/multitree.go +++ b/sei-db/state_db/sc/memiavl/multitree.go @@ -358,11 +358,10 @@ func (t *MultiTree) UpdateCommitInfo() { t.lastCommitInfo = *t.buildCommitInfo(t.lastCommitInfo.Version) } -// Catchup replay the new entries in the Rlog file on the tree to catch up to the target or latest version. -// This function iterates through actual WAL entries and filters by the Version field stored in each entry, -// rather than assuming WAL index equals VersionToIndex(version). This handles cases where WAL indices -// don't match version mapping (e.g., after state sync, WAL pruning, etc.). -func (t *MultiTree) Catchup(ctx context.Context, stream wal.GenericWAL[proto.ChangelogEntry], endVersion int64) error { +// Catchup replays WAL entries to catch up the tree to the target or latest version. +// delta is the difference between version and WAL index (version = walIndex + delta). +// endVersion specifies the target version (0 means catch up to latest). +func (t *MultiTree) Catchup(ctx context.Context, stream wal.GenericWAL[proto.ChangelogEntry], delta int64, endVersion int64) error { startTime := time.Now() // Get actual WAL index range @@ -382,8 +381,24 @@ func (t *MultiTree) Catchup(ctx context.Context, stream wal.GenericWAL[proto.Cha currentVersion := t.Version() + // Calculate start index: walIndex = version - delta + // We want to start from currentVersion + 1 + startIndexSigned := currentVersion + 1 - delta + + // Ensure startIndex is within valid range (handle negative case before uint64 conversion) + var startIndex uint64 + if startIndexSigned <= 0 || uint64(startIndexSigned) < firstIndex { + startIndex = firstIndex + } else { + startIndex = uint64(startIndexSigned) + } + if startIndex > lastIndex { + // Nothing to replay - tree is already caught up + return nil + } + var replayCount = 0 - err = stream.Replay(firstIndex, lastIndex, func(index uint64, entry proto.ChangelogEntry) error { + err = stream.Replay(startIndex, lastIndex, func(index uint64, entry proto.ChangelogEntry) error { // Check for cancellation select { case <-ctx.Done(): @@ -391,14 +406,14 @@ func (t *MultiTree) Catchup(ctx context.Context, stream wal.GenericWAL[proto.Cha default: } - // Filter by version: only apply entries newer than current tree version + // Safety check: skip entries we already have (should not happen with correct startIndex) if entry.Version <= currentVersion { - return nil // skip entries we already have + return nil } // If endVersion is specified, stop at that version if endVersion != 0 && entry.Version > endVersion { - return nil // skip entries beyond target + return nil } if err := t.ApplyUpgrades(entry.Upgrades); err != nil { From 668ce4c1004ffa8a7f9e83d5907868f69358960e Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Fri, 9 Jan 2026 23:05:31 -0800 Subject: [PATCH 12/35] Fix committed version to return persisted height --- sei-db/state_db/sc/memiavl/db.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/sei-db/state_db/sc/memiavl/db.go b/sei-db/state_db/sc/memiavl/db.go index aab2672828..e70ddd4160 100644 --- a/sei-db/state_db/sc/memiavl/db.go +++ b/sei-db/state_db/sc/memiavl/db.go @@ -427,6 +427,17 @@ func (db *DB) checkAsyncTasks() error { // CommittedVersion returns the current version of the MultiTree. func (db *DB) CommittedVersion() int64 { + // Prefer the WAL's last offset, converted via walIndexDelta, to avoid relying + // on potentially stale tree metadata. Fall back to the tree version on error + // or when WAL is absent/empty. + if db.streamHandler != nil { + lastOffset, err := db.streamHandler.LastOffset() + if err != nil { + db.logger.Error("failed to read WAL last offset for committed version", "err", err) + } else if lastOffset > 0 { + return int64(lastOffset) + db.walIndexDelta + } + } return db.MultiTree.Version() } From 29a84be5f7439598b8af65979c1b29650e5e99fd Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Fri, 9 Jan 2026 23:49:07 -0800 Subject: [PATCH 13/35] Fix commitstore open wal --- sei-db/state_db/sc/memiavl/db.go | 50 ++++++++----------- sei-db/state_db/sc/store.go | 82 +++++++++++++------------------- sei-db/state_db/sc/store_test.go | 27 +++++------ 3 files changed, 64 insertions(+), 95 deletions(-) diff --git a/sei-db/state_db/sc/memiavl/db.go b/sei-db/state_db/sc/memiavl/db.go index e70ddd4160..e8b67cb49f 100644 --- a/sei-db/state_db/sc/memiavl/db.go +++ b/sei-db/state_db/sc/memiavl/db.go @@ -70,8 +70,6 @@ type DB struct { // make sure only one snapshot rewrite is running pruneSnapshotLock sync.Mutex - // the changelog stream persists all the changesets (managed by upper layer) - streamHandler wal.GenericWAL[proto.ChangelogEntry] // walIndexDelta is the difference: version - walIndex for any entry. // Since both WAL indices and versions are strictly contiguous, this delta is constant. // Computed once when opening the DB from the first WAL entry. @@ -199,23 +197,23 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * } // Use WAL from options (managed by upper layer like CommitStore) - streamHandler := opts.WAL + walHandler := opts.WAL // Compute WAL index delta (only needed once per DB open) var walIndexDelta int64 var walHasEntries bool - if streamHandler != nil { + if walHandler != nil { var err error - walIndexDelta, walHasEntries, err = computeWALIndexDelta(streamHandler) + walIndexDelta, walHasEntries, err = computeWALIndexDelta(walHandler) if err != nil { return nil, fmt.Errorf("failed to compute WAL index delta: %w", err) } } // Replay WAL to catch up to target version (if WAL is provided) - if streamHandler != nil && walHasEntries && (targetVersion == 0 || targetVersion > mtree.Version()) { + if walHandler != nil && walHasEntries && (targetVersion == 0 || targetVersion > mtree.Version()) { logger.Info("Start catching up and replaying the MemIAVL changelog file") - if err := mtree.Catchup(context.Background(), streamHandler, walIndexDelta, targetVersion); err != nil { + if err := mtree.Catchup(context.Background(), walHandler, walIndexDelta, targetVersion); err != nil { return nil, err } logger.Info(fmt.Sprintf("Finished the replay and caught up to version %d", targetVersion)) @@ -236,12 +234,12 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * } // truncate the rlog file (if WAL is provided and has entries) - if streamHandler != nil && walHasEntries { + if walHandler != nil && walHasEntries { logger.Info("truncate rlog after version: %d", targetVersion) // Use O(1) conversion: walIndex = version - delta truncateIndex := targetVersion - walIndexDelta if truncateIndex > 0 { - if err := streamHandler.TruncateAfter(uint64(truncateIndex)); err != nil { + if err := walHandler.TruncateAfter(uint64(truncateIndex)); err != nil { return nil, fmt.Errorf("fail to truncate rlog file: %w", err) } } @@ -278,7 +276,6 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * dir: opts.Dir, fileLock: fileLock, readOnly: opts.ReadOnly, - streamHandler: streamHandler, walIndexDelta: walIndexDelta, snapshotKeepRecent: opts.SnapshotKeepRecent, snapshotInterval: opts.SnapshotInterval, @@ -305,7 +302,7 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * // GetWAL returns the WAL handler for changelog operations. // Upper layer (CommitStore) uses this to write to WAL. func (db *DB) GetWAL() wal.GenericWAL[proto.ChangelogEntry] { - return db.streamHandler + return db.opts.WAL } // GetWALIndexDelta returns the precomputed delta between version and WAL index. @@ -416,13 +413,10 @@ func (db *DB) ApplyChangeSet(name string, changeSet iavl.ChangeSet) error { // checkAsyncTasks checks the status of background tasks non-blocking-ly and process the result func (db *DB) checkAsyncTasks() error { var walErr error - if db.streamHandler != nil { - walErr = db.streamHandler.CheckError() + if wal := db.GetWAL(); wal != nil { + walErr = wal.CheckError() } - return errorutils.Join( - walErr, - db.checkBackgroundSnapshotRewrite(), - ) + return errorutils.Join(walErr, db.checkBackgroundSnapshotRewrite()) } // CommittedVersion returns the current version of the MultiTree. @@ -430,8 +424,8 @@ func (db *DB) CommittedVersion() int64 { // Prefer the WAL's last offset, converted via walIndexDelta, to avoid relying // on potentially stale tree metadata. Fall back to the tree version on error // or when WAL is absent/empty. - if db.streamHandler != nil { - lastOffset, err := db.streamHandler.LastOffset() + if wal := db.GetWAL(); wal != nil { + lastOffset, err := wal.LastOffset() if err != nil { db.logger.Error("failed to read WAL last offset for committed version", "err", err) } else if lastOffset > 0 { @@ -469,9 +463,9 @@ func (db *DB) checkBackgroundSnapshotRewrite() error { time.Sleep(time.Nanosecond) } - // catchup the remaining entries in rlog (only if WAL is set) - if db.streamHandler != nil { - if err := result.mtree.Catchup(context.Background(), db.streamHandler, db.walIndexDelta, 0); err != nil { + // catchup the remaining entries in rlog + if wal := db.GetWAL(); wal != nil { + if err := result.mtree.Catchup(context.Background(), wal, db.walIndexDelta, 0); err != nil { return fmt.Errorf("catchup failed: %w", err) } } @@ -836,10 +830,9 @@ func (db *DB) rewriteSnapshotBackground() error { cloned.logger.Info("loaded multitree after snapshot", "elapsed", time.Since(loadStart).Seconds()) // do a best effort catch-up, will do another final catch-up in main thread. - // Only catch up if WAL is set (managed by upper layer) - if db.streamHandler != nil { + if wal := db.GetWAL(); wal != nil { catchupStart := time.Now() - if err := mtree.Catchup(ctx, db.streamHandler, db.walIndexDelta, 0); err != nil { + if err := mtree.Catchup(ctx, wal, db.walIndexDelta, 0); err != nil { cloned.logger.Error("failed to catchup after snapshot", "error", err) ch <- snapshotResult{err: err} return @@ -867,8 +860,7 @@ func (db *DB) Close() error { db.pruneSnapshotLock.Lock() defer db.pruneSnapshotLock.Unlock() - // Close rewrite channel first - must wait for background goroutine before closing streamHandler - // because the goroutine may still be using streamHandler + // Close rewrite channel first - must wait for background goroutine before closing WAL db.logger.Info("Closing rewrite channel...") if db.snapshotRewriteChan != nil { db.snapshotRewriteCancelFunc() @@ -889,9 +881,7 @@ func (db *DB) Close() error { db.snapshotRewriteCancelFunc = nil } - // Note: streamHandler (WAL) is owned by the upper layer (CommitStore) and should not be closed here. - // Just clear the reference. - db.streamHandler = nil + // WAL lifecycle is owned by upper layer (CommitStore); do not close or clear it here. errs = append(errs, db.MultiTree.Close()) diff --git a/sei-db/state_db/sc/store.go b/sei-db/state_db/sc/store.go index 02635e1977..3f73d01bb0 100644 --- a/sei-db/state_db/sc/store.go +++ b/sei-db/state_db/sc/store.go @@ -18,9 +18,11 @@ import ( var _ types.Committer = (*CommitStore)(nil) type CommitStore struct { - logger logger.Logger - db *memiavl.DB - opts memiavl.Options + logger logger.Logger + db *memiavl.DB + opts memiavl.Options + homeDir string + cfg config.StateCommitConfig // WAL for changelog persistence (owned by CommitStore) wal wal.GenericWAL[proto.ChangelogEntry] @@ -34,8 +36,9 @@ func NewCommitStore(homeDir string, logger logger.Logger, config config.StateCom if config.Directory != "" { scDir = config.Directory } + commitDBPath := utils.GetCommitStorePath(scDir) opts := memiavl.Options{ - Dir: utils.GetCommitStorePath(scDir), + Dir: commitDBPath, ZeroCopy: config.ZeroCopy, AsyncCommitBuffer: config.AsyncCommitBuffer, SnapshotInterval: config.SnapshotInterval, @@ -47,29 +50,38 @@ func NewCommitStore(homeDir string, logger logger.Logger, config config.StateCom OnlyAllowExportOnSnapshotVersion: config.OnlyAllowExportOnSnapshotVersion, } commitStore := &CommitStore{ - logger: logger, - opts: opts, + logger: logger, + opts: opts, + homeDir: homeDir, + cfg: config, } - return commitStore -} -// createWAL creates a new WAL instance for changelog persistence and replay. -func (cs *CommitStore) createWAL() (wal.GenericWAL[proto.ChangelogEntry], error) { - return wal.NewWAL( + // Create WAL once per CommitStore instance. The WAL path is derived from StateCommitConfig + // (via scDir -> committer.db -> changelog). + w, err := wal.NewWAL( func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, func(data []byte) (proto.ChangelogEntry, error) { var e proto.ChangelogEntry err := e.Unmarshal(data) return e, err }, - cs.logger, - utils.GetChangelogPath(cs.opts.Dir), + commitStore.logger, + utils.GetChangelogPath(commitDBPath), wal.Config{ DisableFsync: true, ZeroCopy: true, - WriteBufferSize: cs.opts.AsyncCommitBuffer, + WriteBufferSize: commitStore.opts.AsyncCommitBuffer, }, ) + if err != nil { + // Keep CommitStore constructible (signature has no error), but fail fast at runtime + // if caller tries to use WAL-dependent features. + commitStore.logger.Error("failed to create WAL", "err", err) + } else { + commitStore.wal = w + commitStore.opts.WAL = w + } + return commitStore } func (cs *CommitStore) Initialize(initialStores []string) { @@ -85,14 +97,6 @@ func (cs *CommitStore) Rollback(targetVersion int64) error { if cs.db != nil { _ = cs.db.Close() } - // Note: we reuse the existing WAL for rollback - memiavl will truncate it - if cs.wal == nil { - wal, err := cs.createWAL() - if err != nil { - return fmt.Errorf("failed to create WAL: %w", err) - } - cs.wal = wal - } options := cs.opts options.LoadForOverwriting = true @@ -112,25 +116,15 @@ func (cs *CommitStore) LoadVersion(targetVersion int64, copyExisting bool) (type cs.logger.Info(fmt.Sprintf("SeiDB load target memIAVL version %d, copyExisting = %v\n", targetVersion, copyExisting)) if copyExisting { - // Create a read-only copy with its own WAL for replay - newCS := &CommitStore{ - logger: cs.logger, - opts: cs.opts, - } + // Create a read-only copy via NewCommitStore (WAL creation happens only there) + newCS := NewCommitStore(cs.homeDir, cs.logger, cs.cfg) + newCS.opts = cs.opts newCS.opts.ReadOnly = true newCS.opts.CreateIfMissing = false - - // WAL is needed for replay even in read-only mode - wal, err := newCS.createWAL() - if err != nil { - return nil, fmt.Errorf("failed to create WAL: %w", err) - } - newCS.wal = wal - newCS.opts.WAL = wal + newCS.opts.WAL = newCS.wal db, err := memiavl.OpenDB(cs.logger, targetVersion, newCS.opts) if err != nil { - _ = wal.Close() return nil, err } newCS.db = db @@ -142,28 +136,18 @@ func (cs *CommitStore) LoadVersion(targetVersion int64, copyExisting bool) (type _ = cs.db.Close() } if cs.wal != nil { - _ = cs.wal.Close() - cs.wal = nil + // WAL is created once in NewCommitStore; do not recreate on LoadVersion. + // Keep it open and reuse. } - // Create WAL for changelog persistence and replay - wal, err := cs.createWAL() - if err != nil { - return nil, fmt.Errorf("failed to create WAL: %w", err) - } - - // Pass WAL to memiavl via options opts := cs.opts - opts.WAL = wal - + opts.WAL = cs.wal db, err := memiavl.OpenDB(cs.logger, targetVersion, opts) if err != nil { - _ = wal.Close() return nil, err } cs.db = db - cs.wal = wal return cs, nil } diff --git a/sei-db/state_db/sc/store_test.go b/sei-db/state_db/sc/store_test.go index f9ea77a8da..3f3461ddd3 100644 --- a/sei-db/state_db/sc/store_test.go +++ b/sei-db/state_db/sc/store_test.go @@ -483,10 +483,8 @@ func TestCreateWAL(t *testing.T) { cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) cs.Initialize([]string{"test"}) - // createWAL should create a valid WAL instance - wal, err := cs.createWAL() - require.NoError(t, err) - require.NotNil(t, wal) + // WAL is created during NewCommitStore + require.NotNil(t, cs.wal) // WAL should be functional - write an entry entry := proto.ChangelogEntry{ @@ -495,11 +493,11 @@ func TestCreateWAL(t *testing.T) { {Name: "test"}, }, } - err = wal.Write(entry) + err := cs.wal.Write(entry) require.NoError(t, err) // Clean up - require.NoError(t, wal.Close()) + require.NoError(t, cs.Close()) } func TestLoadVersionReadOnlyWithWALReplay(t *testing.T) { @@ -751,17 +749,14 @@ func TestRollbackCreatesWALIfNeeded(t *testing.T) { // Close to clear WAL require.NoError(t, cs.Close()) - // WAL should be nil after close - require.Nil(t, cs.wal) - - // Rollback should create a new WAL - err = cs.Rollback(1) - require.NoError(t, err) - - // WAL should be created - require.NotNil(t, cs.wal) + // After Close(), create a new CommitStore (WAL creation happens in NewCommitStore) + cs2 := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) + cs2.Initialize([]string{"test"}) + require.NotNil(t, cs2.wal) - require.NoError(t, cs.Close()) + // Rollback should work + require.NoError(t, cs2.Rollback(1)) + require.NoError(t, cs2.Close()) } func TestCloseReleasesWAL(t *testing.T) { From b1b490b6f1e3b5c9a1eeb8721c1b457bcf322a5f Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Fri, 9 Jan 2026 23:53:36 -0800 Subject: [PATCH 14/35] Fix go lint --- sei-db/state_db/sc/memiavl/db.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sei-db/state_db/sc/memiavl/db.go b/sei-db/state_db/sc/memiavl/db.go index e8b67cb49f..4fea6aa4f1 100644 --- a/sei-db/state_db/sc/memiavl/db.go +++ b/sei-db/state_db/sc/memiavl/db.go @@ -429,6 +429,11 @@ func (db *DB) CommittedVersion() int64 { if err != nil { db.logger.Error("failed to read WAL last offset for committed version", "err", err) } else if lastOffset > 0 { + // Defensive bound check: uint64 -> int64 conversion can overflow in theory. + if lastOffset > uint64(math.MaxInt64) { + db.logger.Error("WAL last offset overflows int64", "lastOffset", lastOffset) + return math.MaxInt64 + } return int64(lastOffset) + db.walIndexDelta } } From 1c34b9e20ebb992745b2212cf798b821c0d9a573 Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Sat, 10 Jan 2026 00:21:19 -0800 Subject: [PATCH 15/35] Add logs to CI --- .github/workflows/integration-test.yml | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 1908176138..f3e7ef282a 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -176,6 +176,44 @@ jobs: done unset IFS # revert the internal field separator back to default + # The integration test runner executes commands inside docker containers (see integration_test/scripts/runner.py). + # Logs written to build/generated/logs therefore live inside the containers, not on the GitHub runner host. + - name: Collect docker node logs (container filesystem) + if: ${{ always() }} + run: | + set -euo pipefail + mkdir -p artifacts/container-logs + for c in sei-node-0 sei-node-1 sei-node-2 sei-node-3 sei-rpc-node; do + if ! docker ps --format '{{.Names}}' | grep -q "^${c}$"; then + echo "Container ${c} not running; skipping" + continue + fi + echo "Locating build/generated/logs in ${c}..." + LOG_DIR=$(docker exec "${c}" /bin/bash -lc 'set -e; for root in /root /workspace /; do d=$(find "$root" -maxdepth 6 -type d -path "*/build/generated/logs" 2>/dev/null | head -n1 || true); if [ -n "$d" ]; then echo "$d"; exit 0; fi; done; exit 0') + if [ -z "${LOG_DIR}" ]; then + echo "No build/generated/logs found in ${c}" + continue + fi + echo "Found ${LOG_DIR} in ${c}" + mkdir -p "artifacts/container-logs/${c}" + # Copy logs out for artifact upload + docker cp "${c}:${LOG_DIR}" "artifacts/container-logs/${c}/" || true + done + + - name: Print last 200 lines of node logs on failure + if: ${{ failure() }} + run: | + set -euo pipefail + for c in sei-node-0 sei-node-1 sei-node-2 sei-node-3; do + echo "==================== ${c} (docker logs tail) ====================" + docker logs --tail 200 "${c}" || true + echo "==================== ${c} (seid log file tail) ====================" + # Try to print the per-node log file if present in the copied artifacts + if [ -f "artifacts/container-logs/${c}/logs/seid-${c#sei-node-}.log" ]; then + tail -200 "artifacts/container-logs/${c}/logs/seid-${c#sei-node-}.log" || true + fi + done + - name: Prepare log artifact name if: ${{ always() }} id: log_artifact_meta @@ -197,6 +235,10 @@ jobs: else echo "No logs directory found" fi + # Also include container logs (these are the real node logs for most integration tests) + if [ -d artifacts/container-logs ]; then + cp -r artifacts/container-logs "$LOG_ROOT/" + fi - name: Upload logs directory if: ${{ always() }} From eda9d618d71a6f34f9c0fce49ccec3b4b4bf3ac0 Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Sat, 10 Jan 2026 00:30:02 -0800 Subject: [PATCH 16/35] Fix lint --- sei-db/state_db/sc/store.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sei-db/state_db/sc/store.go b/sei-db/state_db/sc/store.go index 3f73d01bb0..dc7f867e2f 100644 --- a/sei-db/state_db/sc/store.go +++ b/sei-db/state_db/sc/store.go @@ -135,10 +135,7 @@ func (cs *CommitStore) LoadVersion(targetVersion int64, copyExisting bool) (type if cs.db != nil { _ = cs.db.Close() } - if cs.wal != nil { - // WAL is created once in NewCommitStore; do not recreate on LoadVersion. - // Keep it open and reuse. - } + // WAL is created once in NewCommitStore; keep it open and reuse. opts := cs.opts opts.WAL = cs.wal From 7ca9216e3a68067cb3e9fc9331133a51c22b8171 Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Sat, 10 Jan 2026 01:09:51 -0800 Subject: [PATCH 17/35] Try to fix the initiali store logic --- sei-db/state_db/sc/memiavl/db.go | 30 ++++++++++++++++--------- sei-db/state_db/sc/memiavl/multitree.go | 14 +++++++++++- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/sei-db/state_db/sc/memiavl/db.go b/sei-db/state_db/sc/memiavl/db.go index 4fea6aa4f1..17e83be945 100644 --- a/sei-db/state_db/sc/memiavl/db.go +++ b/sei-db/state_db/sc/memiavl/db.go @@ -210,6 +210,25 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * } } + // Ensure initial store trees exist BEFORE replaying WAL. + // On a fresh DB (version 0), the snapshot can contain 0 trees but the chain has + // non-empty IAVL stores. Historically the initial stores existed before WAL replay. + // + // Note: We intentionally do NOT read from WAL here. "Add tree" upgrades are + // idempotent in MultiTree.ApplyUpgrades (it will skip existing trees), so it is + // safe to apply InitialStores unconditionally. + if !opts.ReadOnly && mtree.Version() == 0 && len(opts.InitialStores) > 0 { + var upgrades []*proto.TreeNameUpgrade + for _, name := range opts.InitialStores { + upgrades = append(upgrades, &proto.TreeNameUpgrade{Name: name}) + } + if len(upgrades) > 0 { + if err := mtree.ApplyUpgrades(upgrades); err != nil { + return nil, fmt.Errorf("failed to apply initial stores before WAL replay: %w", err) + } + } + } + // Replay WAL to catch up to target version (if WAL is provided) if walHandler != nil && walHasEntries && (targetVersion == 0 || targetVersion > mtree.Version()) { logger.Info("Start catching up and replaying the MemIAVL changelog file") @@ -285,17 +304,6 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * opts: opts, } - if !db.readOnly && db.Version() == 0 && len(opts.InitialStores) > 0 { - // do the initial upgrade with the `opts.InitialStores` - var upgrades []*proto.TreeNameUpgrade - for _, name := range opts.InitialStores { - upgrades = append(upgrades, &proto.TreeNameUpgrade{Name: name}) - } - if err := db.ApplyUpgrades(upgrades); err != nil { - return nil, errorutils.Join(err, db.Close()) - } - } - return db, nil } diff --git a/sei-db/state_db/sc/memiavl/multitree.go b/sei-db/state_db/sc/memiavl/multitree.go index 66e678b721..235c014b60 100644 --- a/sei-db/state_db/sc/memiavl/multitree.go +++ b/sei-db/state_db/sc/memiavl/multitree.go @@ -264,6 +264,14 @@ func (t *MultiTree) ApplyUpgrades(upgrades []*proto.TreeNameUpgrade) error { t.trees[i].Name = upgrade.Name default: // add tree + // Idempotency: adding an existing tree should be a no-op. This can happen + // if callers apply InitialStores up front and WAL replay also contains the + // same "add tree" upgrades. + if slices.IndexFunc(t.trees, func(entry NamedTree) bool { + return entry.Name == upgrade.Name + }) >= 0 { + continue + } v := utils.NextVersion(t.Version(), t.initialVersion.Load()) if v < 0 || v > math.MaxUint32 { return fmt.Errorf("version overflows uint32: %d", v) @@ -422,7 +430,11 @@ func (t *MultiTree) Catchup(ctx context.Context, stream wal.GenericWAL[proto.Cha updatedTrees := make(map[string]bool) for _, cs := range entry.Changesets { treeName := cs.Name - t.TreeByName(treeName).ApplyChangeSetAsync(cs.Changeset) + tree := t.TreeByName(treeName) + if tree == nil { + return fmt.Errorf("unknown tree name %s during WAL replay (missing initial stores / upgrades)", treeName) + } + tree.ApplyChangeSetAsync(cs.Changeset) updatedTrees[treeName] = true } for _, tree := range t.trees { From f2262023cc8c88b3a5ceb0fa086c7cd520857849 Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Mon, 12 Jan 2026 23:38:45 -0800 Subject: [PATCH 18/35] Add unit test --- sei-db/state_db/sc/memiavl/db_test.go | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/sei-db/state_db/sc/memiavl/db_test.go b/sei-db/state_db/sc/memiavl/db_test.go index b7f911a4ff..8c20f12be1 100644 --- a/sei-db/state_db/sc/memiavl/db_test.go +++ b/sei-db/state_db/sc/memiavl/db_test.go @@ -582,6 +582,47 @@ func TestRlogIndexConversion(t *testing.T) { } } +// Regression test: on a fresh DB (version 0), the initial snapshot can contain 0 trees, +// but WAL replay may already contain changesets for initial store names. OpenDB must +// ensure InitialStores are applied before replay, otherwise replay will fail with +// "unknown tree name" (or panic via nil tree). +func TestOpenDB_ReplayWithEmptySnapshotUsesInitialStores(t *testing.T) { + dir := t.TempDir() + + // Create WAL entry that references a store name, but has no upgrades. + // This simulates a WAL that only has changesets (e.g. initial upgrades were not + // written due to refactor/order changes), while snapshot is still empty. + w := createTestWAL(t, dir) + defer func() { _ = w.Close() }() + + require.NoError(t, w.Write(proto.ChangelogEntry{ + Version: 1, + Changesets: []*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{{Key: []byte("k"), Value: []byte("v")}}, + }, + }, + }, + Upgrades: nil, + })) + + db, err := OpenDB(logger.NewNopLogger(), 0, Options{ + Dir: dir, + CreateIfMissing: true, // creates an empty snapshot (0 trees) via initEmptyDB + InitialStores: []string{"test"}, + WAL: w, + // Disable background snapshots; not relevant for this test. + SnapshotInterval: 0, + }) + require.NoError(t, err) + defer func() { _ = db.Close() }() + + require.NotNil(t, db.TreeByName("test")) + require.Equal(t, []byte("v"), db.TreeByName("test").Get([]byte("k"))) +} + // TestWALIndexDeltaComputation tests the O(1) delta-based WAL index conversion. // This is critical because: // 1. WAL indices and versions are both strictly contiguous From b705353dab4ec24014529d00c15b35311f6c1cfd Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Tue, 13 Jan 2026 02:16:34 -0800 Subject: [PATCH 19/35] MemIAVL should still have its own changelog as WAL implementation --- sei-db/state_db/sc/memiavl/db.go | 162 +++++++++++--- sei-db/state_db/sc/memiavl/db_test.go | 226 +++----------------- sei-db/state_db/sc/memiavl/opts.go | 7 - sei-db/state_db/sc/memiavl/snapshot_test.go | 13 +- sei-db/state_db/sc/store.go | 129 +---------- sei-db/state_db/sc/store_test.go | 131 ++++-------- 6 files changed, 202 insertions(+), 466 deletions(-) diff --git a/sei-db/state_db/sc/memiavl/db.go b/sei-db/state_db/sc/memiavl/db.go index 17e83be945..52d13b0f32 100644 --- a/sei-db/state_db/sc/memiavl/db.go +++ b/sei-db/state_db/sc/memiavl/db.go @@ -53,6 +53,13 @@ type DB struct { readOnly bool opts Options + // streamHandler is the changelog WAL owned by MemIAVL. + // It is opened during OpenDB (if present / allowed) and closed in DB.Close(). + streamHandler wal.GenericWAL[proto.ChangelogEntry] + // pendingLogEntry accumulates changes (changesets + upgrades) to be written + // into the changelog WAL on the next Commit(). + pendingLogEntry proto.ChangelogEntry + // result channel of snapshot rewrite goroutine snapshotRewriteChan chan snapshotResult // context cancel function to cancel the snapshot rewrite goroutine @@ -196,41 +203,58 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * tree.snapshot.leavesMap.PrepareForRandomRead() } - // Use WAL from options (managed by upper layer like CommitStore) - walHandler := opts.WAL + // MemIAVL owns changelog lifecycle: always open the WAL here. + // Even in read-only mode we may need WAL replay to reconstruct non-snapshot versions. + var walHandler wal.GenericWAL[proto.ChangelogEntry] + changelogDir := utils.GetChangelogPath(opts.Dir) + if st, err := os.Stat(changelogDir); err == nil && !st.IsDir() { + return nil, fmt.Errorf("changelog path exists but is not a directory: %s", changelogDir) + } else if err != nil && os.IsNotExist(err) { + // Create empty WAL directory if missing. This preserves the invariant that db.GetWAL() is non-nil. + // In read-only mode this is still safe: we won't write entries unless Commit() is called (which is disallowed). + if err := os.MkdirAll(changelogDir, 0o755); err != nil { + return nil, fmt.Errorf("failed to create changelog directory: %w", err) + } + } + + writeBuf := 0 + if !opts.ReadOnly { + writeBuf = opts.AsyncCommitBuffer + } + w, err := wal.NewWAL( + func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, + func(data []byte) (proto.ChangelogEntry, error) { + var e proto.ChangelogEntry + err := e.Unmarshal(data) + return e, err + }, + logger, + changelogDir, + wal.Config{ + DisableFsync: true, + ZeroCopy: true, + WriteBufferSize: writeBuf, + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to open changelog WAL: %w", err) + } + walHandler = w // Compute WAL index delta (only needed once per DB open) var walIndexDelta int64 var walHasEntries bool - if walHandler != nil { - var err error - walIndexDelta, walHasEntries, err = computeWALIndexDelta(walHandler) - if err != nil { - return nil, fmt.Errorf("failed to compute WAL index delta: %w", err) - } + walIndexDelta, walHasEntries, err = computeWALIndexDelta(walHandler) + if err != nil { + return nil, fmt.Errorf("failed to compute WAL index delta: %w", err) } - - // Ensure initial store trees exist BEFORE replaying WAL. - // On a fresh DB (version 0), the snapshot can contain 0 trees but the chain has - // non-empty IAVL stores. Historically the initial stores existed before WAL replay. - // - // Note: We intentionally do NOT read from WAL here. "Add tree" upgrades are - // idempotent in MultiTree.ApplyUpgrades (it will skip existing trees), so it is - // safe to apply InitialStores unconditionally. - if !opts.ReadOnly && mtree.Version() == 0 && len(opts.InitialStores) > 0 { - var upgrades []*proto.TreeNameUpgrade - for _, name := range opts.InitialStores { - upgrades = append(upgrades, &proto.TreeNameUpgrade{Name: name}) - } - if len(upgrades) > 0 { - if err := mtree.ApplyUpgrades(upgrades); err != nil { - return nil, fmt.Errorf("failed to apply initial stores before WAL replay: %w", err) - } - } + // If WAL is empty, set delta so first WAL entry aligns with NextVersion(). + if !walHasEntries { + walIndexDelta = mtree.WorkingCommitInfo().Version - 1 } - // Replay WAL to catch up to target version (if WAL is provided) - if walHandler != nil && walHasEntries && (targetVersion == 0 || targetVersion > mtree.Version()) { + // Replay WAL to catch up to target version (if WAL has entries) + if walHasEntries && (targetVersion == 0 || targetVersion > mtree.Version()) { logger.Info("Start catching up and replaying the MemIAVL changelog file") if err := mtree.Catchup(context.Background(), walHandler, walIndexDelta, targetVersion); err != nil { return nil, err @@ -253,7 +277,7 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * } // truncate the rlog file (if WAL is provided and has entries) - if walHandler != nil && walHasEntries { + if walHasEntries { logger.Info("truncate rlog after version: %d", targetVersion) // Use O(1) conversion: walIndex = version - delta truncateIndex := targetVersion - walIndexDelta @@ -296,6 +320,7 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * fileLock: fileLock, readOnly: opts.ReadOnly, walIndexDelta: walIndexDelta, + streamHandler: walHandler, snapshotKeepRecent: opts.SnapshotKeepRecent, snapshotInterval: opts.SnapshotInterval, snapshotMinTimeInterval: opts.SnapshotMinTimeInterval, @@ -304,13 +329,26 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * opts: opts, } + // Apply initial stores on a fresh DB (version 0) so they get persisted to WAL. + // This creates the trees and populates pendingLogEntry, which will be written + // to WAL on the first Commit(). + // ApplyUpgrades is idempotent (skips existing trees), so this is safe. + if !opts.ReadOnly && db.Version() == 0 && len(opts.InitialStores) > 0 { + var upgrades []*proto.TreeNameUpgrade + for _, name := range opts.InitialStores { + upgrades = append(upgrades, &proto.TreeNameUpgrade{Name: name}) + } + if err := db.ApplyUpgrades(upgrades); err != nil { + return nil, fmt.Errorf("failed to apply initial stores: %w", err) + } + } + return db, nil } // GetWAL returns the WAL handler for changelog operations. -// Upper layer (CommitStore) uses this to write to WAL. func (db *DB) GetWAL() wal.GenericWAL[proto.ChangelogEntry] { - return db.opts.WAL + return db.streamHandler } // GetWALIndexDelta returns the precomputed delta between version and WAL index. @@ -374,6 +412,9 @@ func (db *DB) ApplyUpgrades(upgrades []*proto.TreeNameUpgrade) error { return errReadOnly } + if len(upgrades) > 0 { + db.pendingLogEntry.Upgrades = append(db.pendingLogEntry.Upgrades, upgrades...) + } return db.MultiTree.ApplyUpgrades(upgrades) } @@ -399,6 +440,8 @@ func (db *DB) ApplyChangeSets(changeSets []*proto.NamedChangeSet) (_err error) { return errReadOnly } + // Overwrite pending changesets for this commit; callers typically provide them once per block. + db.pendingLogEntry.Changesets = changeSets return db.MultiTree.ApplyChangeSets(changeSets) } @@ -415,6 +458,10 @@ func (db *DB) ApplyChangeSet(name string, changeSet iavl.ChangeSet) error { return errReadOnly } + db.pendingLogEntry.Changesets = append(db.pendingLogEntry.Changesets, &proto.NamedChangeSet{ + Name: name, + Changeset: changeSet, + }) return db.MultiTree.ApplyChangeSet(name, changeSet) } @@ -583,7 +630,7 @@ func (db *DB) walIndexToVersion(index uint64) int64 { } // Commit wraps SaveVersion to bump the version and finalize the tree state. -// The caller (CommitStore) is responsible for writing to WAL before calling this. +// MemIAVL owns the changelog: it writes the pending changelog entry before committing the tree. func (db *DB) Commit() (version int64, _err error) { startTime := time.Now() defer func() { @@ -603,6 +650,19 @@ func (db *DB) Commit() (version int64, _err error) { return 0, errReadOnly } + nextVersion := db.MultiTree.WorkingCommitInfo().Version + if wal := db.GetWAL(); wal != nil { + entry := db.pendingLogEntry + entry.Version = nextVersion + if err := wal.Write(entry); err != nil { + return 0, fmt.Errorf("failed to write changelog WAL: %w", err) + } + if err := wal.CheckError(); err != nil { + return 0, fmt.Errorf("changelog WAL async write error: %w", err) + } + } + db.pendingLogEntry = proto.ChangelogEntry{} + v, err := db.MultiTree.SaveVersion(true) if err != nil { return 0, err @@ -614,10 +674,42 @@ func (db *DB) Commit() (version int64, _err error) { // Rewrite tree snapshot if applicable db.rewriteIfApplicable(v) + db.tryTruncateWAL() return v, nil } +// tryTruncateWAL best-effort truncates old WAL entries that are older than the earliest snapshot. +func (db *DB) tryTruncateWAL() { + if db.streamHandler == nil { + return + } + firstWALIndex, err := db.streamHandler.FirstOffset() + if err != nil || firstWALIndex == 0 { + return + } + earliestSnapshotVersion, err := GetEarliestVersion(db.dir) + if err != nil { + return + } + if firstWALIndex > uint64(math.MaxInt64) { + db.logger.Error("WAL first offset overflows int64; skipping truncation", "firstWALIndex", firstWALIndex) + return + } + walEarliestVersion := int64(firstWALIndex) + db.walIndexDelta + if walEarliestVersion >= earliestSnapshotVersion { + return + } + truncateIndex := earliestSnapshotVersion - db.walIndexDelta + if truncateIndex <= 0 || truncateIndex <= int64(firstWALIndex) { + return + } + // #nosec G115 -- truncateIndex is checked to be positive and <= MaxInt64 above. + if err := db.streamHandler.TruncateBefore(uint64(truncateIndex)); err != nil { + db.logger.Error("failed to truncate changelog WAL", "err", err, "truncateIndex", truncateIndex) + } +} + func (db *DB) Copy() *DB { db.mtx.Lock() defer db.mtx.Unlock() @@ -894,7 +986,11 @@ func (db *DB) Close() error { db.snapshotRewriteCancelFunc = nil } - // WAL lifecycle is owned by upper layer (CommitStore); do not close or clear it here. + // Close WAL after snapshot rewrite goroutine has fully exited. + if db.streamHandler != nil { + errs = append(errs, db.streamHandler.Close()) + db.streamHandler = nil + } errs = append(errs, db.MultiTree.Close()) diff --git a/sei-db/state_db/sc/memiavl/db_test.go b/sei-db/state_db/sc/memiavl/db_test.go index 8c20f12be1..647014f2ea 100644 --- a/sei-db/state_db/sc/memiavl/db_test.go +++ b/sei-db/state_db/sc/memiavl/db_test.go @@ -17,57 +17,9 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/common/utils" "github.com/sei-protocol/sei-chain/sei-db/proto" - "github.com/sei-protocol/sei-chain/sei-db/wal" iavl "github.com/sei-protocol/sei-chain/sei-iavl" ) -// createTestWAL creates a WAL for testing purposes. -func createTestWAL(t *testing.T, dir string) wal.GenericWAL[proto.ChangelogEntry] { - t.Helper() - wal, err := wal.NewWAL( - func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, - func(data []byte) (proto.ChangelogEntry, error) { - var e proto.ChangelogEntry - err := e.Unmarshal(data) - return e, err - }, - logger.NewNopLogger(), - utils.GetChangelogPath(dir), - wal.Config{ - DisableFsync: true, - ZeroCopy: true, - WriteBufferSize: 0, - }, - ) - require.NoError(t, err) - return wal -} - -// writeToWAL is a test helper that writes pending changes to WAL. -// In production, CommitStore handles this. Tests that need WAL replay use this helper. -func writeToWAL(t *testing.T, db *DB, changesets []*proto.NamedChangeSet, upgrades []*proto.TreeNameUpgrade) { - t.Helper() - wal := db.GetWAL() - if wal == nil { - return - } - entry := proto.ChangelogEntry{ - Version: db.WorkingCommitInfo().Version, - Changesets: changesets, - Upgrades: upgrades, - } - require.NoError(t, wal.Write(entry)) -} - -// initialUpgrades converts InitialStores names to TreeNameUpgrade slice for WAL writing. -func initialUpgrades(stores []string) []*proto.TreeNameUpgrade { - var upgrades []*proto.TreeNameUpgrade - for _, name := range stores { - upgrades = append(upgrades, &proto.TreeNameUpgrade{Name: name}) - } - return upgrades -} - func TestRewriteSnapshot(t *testing.T) { db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: t.TempDir(), @@ -208,9 +160,8 @@ func TestRewriteSnapshotBackground(t *testing.T) { entries, err := os.ReadDir(db.dir) require.NoError(t, err) - // three files: snapshot, current link, LOCK - // (no rlog when WAL is not provided) - require.Equal(t, 3, len(entries)) + // snapshot, current link, LOCK, changelog WAL dir + require.Equal(t, 4, len(entries)) // stopCh is closed by defer above } @@ -281,19 +232,14 @@ func TestRlog(t *testing.T) { dir := t.TempDir() initialStores := []string{"test", "delete"} - // Create WAL for this test - wal := createTestWAL(t, dir) - defer wal.Close() - db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: dir, CreateIfMissing: true, InitialStores: initialStores, - WAL: wal, }) require.NoError(t, err) - for i, changes := range ChangeSets { + for _, changes := range ChangeSets { cs := []*proto.NamedChangeSet{ { Name: "test", @@ -301,12 +247,6 @@ func TestRlog(t *testing.T) { }, } require.NoError(t, db.ApplyChangeSets(cs)) - // First WAL entry must include initial upgrades for replay to work - if i == 0 { - writeToWAL(t, db, cs, initialUpgrades(initialStores)) - } else { - writeToWAL(t, db, cs, nil) - } _, err := db.Commit() require.NoError(t, err) } @@ -324,14 +264,13 @@ func TestRlog(t *testing.T) { }, } require.NoError(t, db.ApplyUpgrades(upgrades)) - writeToWAL(t, db, nil, upgrades) _, err = db.Commit() require.NoError(t, err) require.NoError(t, db.Close()) - // Reopen with the same WAL - db, err = OpenDB(logger.NewNopLogger(), 0, Options{Dir: dir, WAL: wal}) + // Reopen (MemIAVL will open the changelog from disk) + db, err = OpenDB(logger.NewNopLogger(), 0, Options{Dir: dir, InitialStores: initialStores}) require.NoError(t, err) defer db.Close() // Close the reopened DB @@ -364,21 +303,15 @@ func TestInitialVersion(t *testing.T) { dir := t.TempDir() initialStores := []string{name} - // Create WAL for this test iteration - wal := createTestWAL(t, dir) - db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: dir, CreateIfMissing: true, InitialStores: initialStores, - WAL: wal, }) require.NoError(t, err) db.SetInitialVersion(initialVersion) cs1 := mockNameChangeSet(name, key, value) require.NoError(t, db.ApplyChangeSets(cs1)) - // First WAL entry must include initial upgrades for replay to work - writeToWAL(t, db, cs1, initialUpgrades(initialStores)) v, err := db.Commit() require.NoError(t, err) if initialVersion <= 1 { @@ -390,7 +323,6 @@ func TestInitialVersion(t *testing.T) { require.Equal(t, "6032661ab0d201132db7a8fa1da6a0afe427e6278bd122c301197680ab79ca02", hex.EncodeToString(hash)) cs2 := mockNameChangeSet(name, key, "world1") require.NoError(t, db.ApplyChangeSets(cs2)) - writeToWAL(t, db, cs2, nil) v, err = db.Commit() require.NoError(t, err) hash = db.LastCommitInfo().StoreInfos[0].CommitId.Hash @@ -403,8 +335,8 @@ func TestInitialVersion(t *testing.T) { } require.NoError(t, db.Close()) - // Reopen with the same WAL - db, err = OpenDB(logger.NewNopLogger(), 0, Options{Dir: dir, WAL: wal}) + // Reopen (MemIAVL will open the changelog from disk) + db, err = OpenDB(logger.NewNopLogger(), 0, Options{Dir: dir, InitialStores: initialStores}) require.NoError(t, err) require.Equal(t, uint32(initialVersion), db.initialVersion.Load()) require.Equal(t, v, db.Version()) @@ -414,7 +346,6 @@ func TestInitialVersion(t *testing.T) { db.ApplyUpgrades(upgrades1) cs3 := mockNameChangeSet(name1, key, value) require.NoError(t, db.ApplyChangeSets(cs3)) - writeToWAL(t, db, cs3, upgrades1) v, err = db.Commit() require.NoError(t, err) if initialVersion <= 1 { @@ -437,7 +368,6 @@ func TestInitialVersion(t *testing.T) { db.ApplyUpgrades(upgrades2) cs4 := mockNameChangeSet(name2, key, value) require.NoError(t, db.ApplyChangeSets(cs4)) - writeToWAL(t, db, cs4, upgrades2) v, err = db.Commit() require.NoError(t, err) if initialVersion <= 1 { @@ -452,7 +382,6 @@ func TestInitialVersion(t *testing.T) { require.Equal(t, hex.EncodeToString(info.CommitId.Hash), hex.EncodeToString(info2.CommitId.Hash)) require.NoError(t, db.Close()) - require.NoError(t, wal.Close()) } } @@ -460,15 +389,10 @@ func TestLoadVersion(t *testing.T) { dir := t.TempDir() initialStores := []string{"test"} - // Create WAL for this test - wal := createTestWAL(t, dir) - defer wal.Close() - db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: dir, CreateIfMissing: true, InitialStores: initialStores, - WAL: wal, }) require.NoError(t, err) @@ -484,13 +408,6 @@ func TestLoadVersion(t *testing.T) { // check the root hash require.Equal(t, RefHashes[db.Version()], db.WorkingCommitInfo().StoreInfos[0].CommitId.Hash) - - // First WAL entry must include initial upgrades for replay to work - if i == 0 { - writeToWAL(t, db, cs, initialUpgrades(initialStores)) - } else { - writeToWAL(t, db, cs, nil) - } _, err := db.Commit() require.NoError(t, err) }) @@ -503,9 +420,9 @@ func TestLoadVersion(t *testing.T) { } // Read-only loads use the same WAL to replay tmp, err := OpenDB(logger.NewNopLogger(), int64(v), Options{ - Dir: dir, - ReadOnly: true, - WAL: wal, + Dir: dir, + ReadOnly: true, + InitialStores: initialStores, }) require.NoError(t, err) require.Equal(t, RefHashes[v-1], tmp.TreeByName("test").RootHash()) @@ -584,45 +501,6 @@ func TestRlogIndexConversion(t *testing.T) { // Regression test: on a fresh DB (version 0), the initial snapshot can contain 0 trees, // but WAL replay may already contain changesets for initial store names. OpenDB must -// ensure InitialStores are applied before replay, otherwise replay will fail with -// "unknown tree name" (or panic via nil tree). -func TestOpenDB_ReplayWithEmptySnapshotUsesInitialStores(t *testing.T) { - dir := t.TempDir() - - // Create WAL entry that references a store name, but has no upgrades. - // This simulates a WAL that only has changesets (e.g. initial upgrades were not - // written due to refactor/order changes), while snapshot is still empty. - w := createTestWAL(t, dir) - defer func() { _ = w.Close() }() - - require.NoError(t, w.Write(proto.ChangelogEntry{ - Version: 1, - Changesets: []*proto.NamedChangeSet{ - { - Name: "test", - Changeset: iavl.ChangeSet{ - Pairs: []*iavl.KVPair{{Key: []byte("k"), Value: []byte("v")}}, - }, - }, - }, - Upgrades: nil, - })) - - db, err := OpenDB(logger.NewNopLogger(), 0, Options{ - Dir: dir, - CreateIfMissing: true, // creates an empty snapshot (0 trees) via initEmptyDB - InitialStores: []string{"test"}, - WAL: w, - // Disable background snapshots; not relevant for this test. - SnapshotInterval: 0, - }) - require.NoError(t, err) - defer func() { _ = db.Close() }() - - require.NotNil(t, db.TreeByName("test")) - require.Equal(t, []byte("v"), db.TreeByName("test").Get([]byte("k"))) -} - // TestWALIndexDeltaComputation tests the O(1) delta-based WAL index conversion. // This is critical because: // 1. WAL indices and versions are both strictly contiguous @@ -660,16 +538,12 @@ func TestWALIndexDeltaComputation(t *testing.T) { dir := t.TempDir() initialStores := []string{"test"} - // Create WAL - wal := createTestWAL(t, dir) - // Open DB with initial version db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: dir, CreateIfMissing: true, InitialStores: initialStores, InitialVersion: tc.initialVersion, - WAL: wal, }) require.NoError(t, err) @@ -686,13 +560,6 @@ func TestWALIndexDeltaComputation(t *testing.T) { }, } require.NoError(t, db.ApplyChangeSets(cs)) - - // First entry needs upgrades for replay - var upgrades []*proto.TreeNameUpgrade - if i == 0 { - upgrades = initialUpgrades(initialStores) - } - writeToWAL(t, db, cs, upgrades) _, err = db.Commit() require.NoError(t, err) } @@ -705,17 +572,12 @@ func TestWALIndexDeltaComputation(t *testing.T) { } require.Equal(t, expectedVersion, db.Version()) - // Note: walIndexDelta is 0 here because it was computed at open time when WAL was empty. - // We'll verify the correct delta after reopening. - require.NoError(t, db.Close()) - require.NoError(t, wal.Close()) // Reopen to verify delta is computed correctly from WAL entries - walReopen := createTestWAL(t, dir) dbReopen, err := OpenDB(logger.NewNopLogger(), 0, Options{ - Dir: dir, - WAL: walReopen, + Dir: dir, + InitialStores: initialStores, }) require.NoError(t, err) @@ -743,14 +605,12 @@ func TestWALIndexDeltaComputation(t *testing.T) { } require.NoError(t, dbReopen.Close()) - require.NoError(t, walReopen.Close()) // Now test rollback with LoadForOverwriting - wal2 := createTestWAL(t, dir) db2, err := OpenDB(logger.NewNopLogger(), tc.rollbackTo, Options{ Dir: dir, + InitialStores: initialStores, LoadForOverwriting: true, - WAL: wal2, }) require.NoError(t, err) @@ -758,25 +618,22 @@ func TestWALIndexDeltaComputation(t *testing.T) { require.Equal(t, tc.rollbackTo, db2.Version(), "Version should be rolled back to %d", tc.rollbackTo) // Verify WAL was truncated correctly - lastIndex, err := wal2.LastOffset() + lastIndex, err := db2.GetWAL().LastOffset() require.NoError(t, err) expectedLastIndex := uint64(tc.rollbackTo - db2.walIndexDelta) require.Equal(t, expectedLastIndex, lastIndex, "WAL should be truncated to index %d", expectedLastIndex) require.NoError(t, db2.Close()) - require.NoError(t, wal2.Close()) // Reopen without LoadForOverwriting to verify persistence - wal3 := createTestWAL(t, dir) db3, err := OpenDB(logger.NewNopLogger(), 0, Options{ - Dir: dir, - WAL: wal3, + Dir: dir, + InitialStores: initialStores, }) require.NoError(t, err) require.Equal(t, tc.rollbackTo, db3.Version(), "Version should persist as %d after reopen", tc.rollbackTo) require.NoError(t, db3.Close()) - require.NoError(t, wal3.Close()) }) } } @@ -788,14 +645,11 @@ func TestWALIndexDeltaWithZeroDelta(t *testing.T) { dir := t.TempDir() initialStores := []string{"test"} - wal := createTestWAL(t, dir) - // Create DB with default initial version (0, so versions start at 1) db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: dir, CreateIfMissing: true, InitialStores: initialStores, - WAL: wal, }) require.NoError(t, err) @@ -812,12 +666,6 @@ func TestWALIndexDeltaWithZeroDelta(t *testing.T) { }, } require.NoError(t, db.ApplyChangeSets(cs)) - - var upgrades []*proto.TreeNameUpgrade - if i == 0 { - upgrades = initialUpgrades(initialStores) - } - writeToWAL(t, db, cs, upgrades) _, err = db.Commit() require.NoError(t, err) } @@ -827,14 +675,12 @@ func TestWALIndexDeltaWithZeroDelta(t *testing.T) { require.Equal(t, int64(0), db.walIndexDelta, "Delta should be 0 when versions start at 1") require.NoError(t, db.Close()) - require.NoError(t, wal.Close()) // Rollback to version 3 - wal2 := createTestWAL(t, dir) db2, err := OpenDB(logger.NewNopLogger(), 3, Options{ Dir: dir, + InitialStores: initialStores, LoadForOverwriting: true, - WAL: wal2, }) require.NoError(t, err) @@ -842,40 +688,32 @@ func TestWALIndexDeltaWithZeroDelta(t *testing.T) { require.Equal(t, int64(3), db2.Version(), "Rollback should work even when delta=0") // Verify WAL truncation - lastIndex, err := wal2.LastOffset() + lastIndex, err := db2.GetWAL().LastOffset() require.NoError(t, err) require.Equal(t, uint64(3), lastIndex, "WAL should be truncated to index 3") require.NoError(t, db2.Close()) - require.NoError(t, wal2.Close()) // Verify rollback persisted after reopen - wal3 := createTestWAL(t, dir) db3, err := OpenDB(logger.NewNopLogger(), 0, Options{ - Dir: dir, - WAL: wal3, + Dir: dir, + InitialStores: initialStores, }) require.NoError(t, err) require.Equal(t, int64(3), db3.Version(), "Rollback should persist after reopen") require.NoError(t, db3.Close()) - require.NoError(t, wal3.Close()) } func TestEmptyValue(t *testing.T) { dir := t.TempDir() initialStores := []string{"test"} - // Create WAL for this test - wal := createTestWAL(t, dir) - defer wal.Close() - db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: dir, InitialStores: initialStores, CreateIfMissing: true, ZeroCopy: true, - WAL: wal, }) require.NoError(t, err) @@ -889,8 +727,6 @@ func TestEmptyValue(t *testing.T) { }}, } require.NoError(t, db.ApplyChangeSets(cs1)) - // First WAL entry must include initial upgrades for replay to work - writeToWAL(t, db, cs1, initialUpgrades(initialStores)) _, err = db.Commit() require.NoError(t, err) @@ -900,14 +736,13 @@ func TestEmptyValue(t *testing.T) { }}, } require.NoError(t, db.ApplyChangeSets(cs2)) - writeToWAL(t, db, cs2, nil) version, err := db.Commit() require.NoError(t, err) require.NoError(t, db.Close()) - // Reopen with the same WAL - db, err = OpenDB(logger.NewNopLogger(), 0, Options{Dir: dir, ZeroCopy: true, WAL: wal}) + // Reopen (MemIAVL will open the changelog from disk) + db, err = OpenDB(logger.NewNopLogger(), 0, Options{Dir: dir, ZeroCopy: true, InitialStores: initialStores}) require.NoError(t, err) defer db.Close() // Close the reopened DB require.Equal(t, version, db.Version()) @@ -1099,19 +934,17 @@ func TestCatchupWithCancelledContext(t *testing.T) { dir := t.TempDir() initialStores := []string{"test"} - // Create WAL for this test - wal := createTestWAL(t, dir) - defer wal.Close() - db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: dir, CreateIfMissing: true, InitialStores: initialStores, - WAL: wal, }) require.NoError(t, err) defer db.Close() + wal := db.GetWAL() + require.NotNil(t, wal) + // Add multiple versions to have changelog entries for i := 0; i < 5; i++ { cs := []*proto.NamedChangeSet{ @@ -1120,12 +953,6 @@ func TestCatchupWithCancelledContext(t *testing.T) { }}, } require.NoError(t, db.ApplyChangeSets(cs)) - // Write to WAL - if i == 0 { - writeToWAL(t, db, cs, initialUpgrades(initialStores)) - } else { - writeToWAL(t, db, cs, nil) - } _, err = db.Commit() require.NoError(t, err) } @@ -1146,8 +973,7 @@ func TestCatchupWithCancelledContext(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() // Cancel immediately - // delta=0 for this test since we're just testing context cancellation - err = mtree.Catchup(ctx, wal, 0, 0) + err = mtree.Catchup(ctx, wal, db.walIndexDelta, 0) // If already caught up, no error; otherwise should get context.Canceled if err != nil { require.Equal(t, context.Canceled, err) diff --git a/sei-db/state_db/sc/memiavl/opts.go b/sei-db/state_db/sc/memiavl/opts.go index c2a714be72..144af6bd6a 100644 --- a/sei-db/state_db/sc/memiavl/opts.go +++ b/sei-db/state_db/sc/memiavl/opts.go @@ -7,8 +7,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/config" - "github.com/sei-protocol/sei-chain/sei-db/proto" - "github.com/sei-protocol/sei-chain/sei-db/wal" ) type Options struct { @@ -49,11 +47,6 @@ type Options struct { // Minimum time interval between snapshots // This prevents excessive snapshot creation during catch-up. Default is 1 hour. SnapshotMinTimeInterval time.Duration - - // WAL is the write-ahead log for changelog persistence and replay. - // If nil, no WAL operations will be performed (read-only mode or tests). - // The WAL is managed by the upper layer (CommitStore) and passed in. - WAL wal.GenericWAL[proto.ChangelogEntry] } func (opts Options) Validate() error { diff --git a/sei-db/state_db/sc/memiavl/snapshot_test.go b/sei-db/state_db/sc/memiavl/snapshot_test.go index 5eb5b380f5..f5bb0015a7 100644 --- a/sei-db/state_db/sc/memiavl/snapshot_test.go +++ b/sei-db/state_db/sc/memiavl/snapshot_test.go @@ -144,20 +144,15 @@ func TestDBSnapshotRestore(t *testing.T) { dir := t.TempDir() initialStores := []string{"test", "test2"} - // Create WAL for this test - wal := createTestWAL(t, dir) - defer wal.Close() - db, err := OpenDB(logger.NewNopLogger(), 0, Options{ Dir: dir, CreateIfMissing: true, InitialStores: initialStores, AsyncCommitBuffer: -1, - WAL: wal, }) require.NoError(t, err) - for i, changes := range ChangeSets { + for _, changes := range ChangeSets { cs := []*proto.NamedChangeSet{ { Name: "test", @@ -169,12 +164,6 @@ func TestDBSnapshotRestore(t *testing.T) { }, } require.NoError(t, db.ApplyChangeSets(cs)) - // First WAL entry must include initial upgrades for replay to work - if i == 0 { - writeToWAL(t, db, cs, initialUpgrades(initialStores)) - } else { - writeToWAL(t, db, cs, nil) - } _, err := db.Commit() require.NoError(t, err) diff --git a/sei-db/state_db/sc/store.go b/sei-db/state_db/sc/store.go index dc7f867e2f..0807c5a0e8 100644 --- a/sei-db/state_db/sc/store.go +++ b/sei-db/state_db/sc/store.go @@ -12,7 +12,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/memiavl" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types" - "github.com/sei-protocol/sei-chain/sei-db/wal" ) var _ types.Committer = (*CommitStore)(nil) @@ -23,12 +22,6 @@ type CommitStore struct { opts memiavl.Options homeDir string cfg config.StateCommitConfig - - // WAL for changelog persistence (owned by CommitStore) - wal wal.GenericWAL[proto.ChangelogEntry] - - // pending changes to be written to WAL on next Commit - pendingLogEntry proto.ChangelogEntry } func NewCommitStore(homeDir string, logger logger.Logger, config config.StateCommitConfig) *CommitStore { @@ -55,32 +48,6 @@ func NewCommitStore(homeDir string, logger logger.Logger, config config.StateCom homeDir: homeDir, cfg: config, } - - // Create WAL once per CommitStore instance. The WAL path is derived from StateCommitConfig - // (via scDir -> committer.db -> changelog). - w, err := wal.NewWAL( - func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, - func(data []byte) (proto.ChangelogEntry, error) { - var e proto.ChangelogEntry - err := e.Unmarshal(data) - return e, err - }, - commitStore.logger, - utils.GetChangelogPath(commitDBPath), - wal.Config{ - DisableFsync: true, - ZeroCopy: true, - WriteBufferSize: commitStore.opts.AsyncCommitBuffer, - }, - ) - if err != nil { - // Keep CommitStore constructible (signature has no error), but fail fast at runtime - // if caller tries to use WAL-dependent features. - commitStore.logger.Error("failed to create WAL", "err", err) - } else { - commitStore.wal = w - commitStore.opts.WAL = w - } return commitStore } @@ -100,7 +67,6 @@ func (cs *CommitStore) Rollback(targetVersion int64) error { options := cs.opts options.LoadForOverwriting = true - options.WAL = cs.wal db, err := memiavl.OpenDB(cs.logger, targetVersion, options) if err != nil { @@ -116,12 +82,11 @@ func (cs *CommitStore) LoadVersion(targetVersion int64, copyExisting bool) (type cs.logger.Info(fmt.Sprintf("SeiDB load target memIAVL version %d, copyExisting = %v\n", targetVersion, copyExisting)) if copyExisting { - // Create a read-only copy via NewCommitStore (WAL creation happens only there) + // Create a read-only copy via NewCommitStore. newCS := NewCommitStore(cs.homeDir, cs.logger, cs.cfg) newCS.opts = cs.opts newCS.opts.ReadOnly = true newCS.opts.CreateIfMissing = false - newCS.opts.WAL = newCS.wal db, err := memiavl.OpenDB(cs.logger, targetVersion, newCS.opts) if err != nil { @@ -135,10 +100,8 @@ func (cs *CommitStore) LoadVersion(targetVersion int64, copyExisting bool) (type if cs.db != nil { _ = cs.db.Close() } - // WAL is created once in NewCommitStore; keep it open and reuse. opts := cs.opts - opts.WAL = cs.wal db, err := memiavl.OpenDB(cs.logger, targetVersion, opts) if err != nil { return nil, err @@ -149,82 +112,7 @@ func (cs *CommitStore) LoadVersion(targetVersion int64, copyExisting bool) (type } func (cs *CommitStore) Commit() (int64, error) { - // Get the next version that will be committed - nextVersion := cs.db.WorkingCommitInfo().Version - - // Write to WAL first (ensures durability before tree commit) - if cs.wal != nil { - cs.pendingLogEntry.Version = nextVersion - if err := cs.wal.Write(cs.pendingLogEntry); err != nil { - return 0, fmt.Errorf("failed to write to WAL: %w", err) - } - // Check for async write errors - if err := cs.wal.CheckError(); err != nil { - return 0, fmt.Errorf("WAL async write error: %w", err) - } - } - - // Clear pending entry - cs.pendingLogEntry = proto.ChangelogEntry{} - - // Now commit to the tree - version, err := cs.db.Commit() - if err != nil { - return 0, err - } - - // Try to truncate WAL after commit (non-blocking, errors are logged) - cs.tryTruncateWAL() - - return version, nil -} - -// tryTruncateWAL checks if WAL can be truncated based on earliest snapshot version. -// This is safe because we only need WAL entries for versions newer than the earliest snapshot. -// Called after each commit to keep WAL size bounded. -func (cs *CommitStore) tryTruncateWAL() { - if cs.wal == nil { - return - } - - // Get WAL's first index - firstWALIndex, err := cs.wal.FirstOffset() - if err != nil { - cs.logger.Error("failed to get WAL first offset", "err", err) - return - } - if firstWALIndex == 0 { - return // empty WAL, nothing to truncate - } - - // Get earliest snapshot version - earliestSnapshotVersion, err := cs.GetEarliestVersion() - if err != nil { - // This can happen if no snapshots exist yet, which is normal - return - } - - // Compute WAL's earliest version using delta - // delta = firstVersion - firstIndex, so firstVersion = firstIndex + delta - walDelta := cs.db.GetWALIndexDelta() - // #nosec G115 -- WAL indices are always much smaller than MaxInt64 in practice - walEarliestVersion := int64(firstWALIndex) + walDelta - - // If WAL's earliest version is less than snapshot's earliest version, - // we can safely truncate those WAL entries - if walEarliestVersion < earliestSnapshotVersion { - // Truncate WAL entries with version < earliestSnapshotVersion - // WAL index for earliestSnapshotVersion = earliestSnapshotVersion - delta - truncateIndex := earliestSnapshotVersion - walDelta - // #nosec G115 -- truncateIndex is guaranteed > firstWALIndex (positive) by the outer check - if truncateIndex > int64(firstWALIndex) && truncateIndex > 0 { - if err := cs.wal.TruncateBefore(uint64(truncateIndex)); err != nil { - cs.logger.Error("failed to truncate WAL", "err", err, "truncateIndex", truncateIndex) - } else { - cs.logger.Debug("truncated WAL", "beforeIndex", truncateIndex, "earliestSnapshotVersion", earliestSnapshotVersion) - } - } - } + return cs.db.Commit() } func (cs *CommitStore) Version() int64 { @@ -244,9 +132,6 @@ func (cs *CommitStore) ApplyChangeSets(changesets []*proto.NamedChangeSet) error return nil } - // Store in pending log entry for WAL - cs.pendingLogEntry.Changesets = changesets - // Apply to tree return cs.db.ApplyChangeSets(changesets) } @@ -256,9 +141,6 @@ func (cs *CommitStore) ApplyUpgrades(upgrades []*proto.TreeNameUpgrade) error { return nil } - // Store in pending log entry for WAL - cs.pendingLogEntry.Upgrades = append(cs.pendingLogEntry.Upgrades, upgrades...) - // Apply to tree return cs.db.ApplyUpgrades(upgrades) } @@ -292,17 +174,10 @@ func (cs *CommitStore) Importer(version int64) (types.Importer, error) { func (cs *CommitStore) Close() error { var errs []error - // Close DB first (it may still reference WAL) if cs.db != nil { errs = append(errs, cs.db.Close()) cs.db = nil } - // Then close WAL - if cs.wal != nil { - errs = append(errs, cs.wal.Close()) - cs.wal = nil - } - return errors.Join(errs...) } diff --git a/sei-db/state_db/sc/store_test.go b/sei-db/state_db/sc/store_test.go index 3f3461ddd3..a016d3aca0 100644 --- a/sei-db/state_db/sc/store_test.go +++ b/sei-db/state_db/sc/store_test.go @@ -12,6 +12,19 @@ import ( iavl "github.com/sei-protocol/sei-chain/sei-iavl" ) +func mustReadLastChangelogEntry(t *testing.T, cs *CommitStore) proto.ChangelogEntry { + t.Helper() + require.NotNil(t, cs.db) + w := cs.db.GetWAL() + require.NotNil(t, w) + last, err := w.LastOffset() + require.NoError(t, err) + require.Greater(t, last, uint64(0)) + e, err := w.ReadAt(last) + require.NoError(t, err) + return e +} + func TestNewCommitStore(t *testing.T) { dir := t.TempDir() cfg := config.StateCommitConfig{ @@ -77,17 +90,14 @@ func TestCommitStoreBasicOperations(t *testing.T) { err = cs.ApplyChangeSets(changesets) require.NoError(t, err) - // Verify pending entry has the changesets - require.Equal(t, changesets, cs.pendingLogEntry.Changesets) - // Commit version, err := cs.Commit() require.NoError(t, err) require.Equal(t, int64(1), version) - // Pending entry should be cleared after commit - require.Nil(t, cs.pendingLogEntry.Changesets) - require.Nil(t, cs.pendingLogEntry.Upgrades) + entry := mustReadLastChangelogEntry(t, cs) + require.Equal(t, int64(1), entry.Version) + require.Equal(t, changesets, entry.Changesets) // Version should be updated require.Equal(t, int64(1), cs.Version()) @@ -105,11 +115,9 @@ func TestApplyChangeSetsEmpty(t *testing.T) { // Empty changesets should be no-op err = cs.ApplyChangeSets(nil) require.NoError(t, err) - require.Nil(t, cs.pendingLogEntry.Changesets) err = cs.ApplyChangeSets([]*proto.NamedChangeSet{}) require.NoError(t, err) - require.Nil(t, cs.pendingLogEntry.Changesets) } func TestApplyUpgrades(t *testing.T) { @@ -129,17 +137,17 @@ func TestApplyUpgrades(t *testing.T) { err = cs.ApplyUpgrades(upgrades) require.NoError(t, err) - // Verify pending entry has the upgrades - require.Equal(t, upgrades, cs.pendingLogEntry.Upgrades) - // Apply more upgrades - should append moreUpgrades := []*proto.TreeNameUpgrade{ {Name: "newstore3"}, } err = cs.ApplyUpgrades(moreUpgrades) require.NoError(t, err) - - require.Len(t, cs.pendingLogEntry.Upgrades, 3) + _, err = cs.Commit() + require.NoError(t, err) + entry := mustReadLastChangelogEntry(t, cs) + // 4 upgrades total: initial store "test" + newstore1, newstore2, newstore3 + require.Len(t, entry.Upgrades, 4) } func TestApplyUpgradesEmpty(t *testing.T) { @@ -154,11 +162,9 @@ func TestApplyUpgradesEmpty(t *testing.T) { // Empty upgrades should be no-op err = cs.ApplyUpgrades(nil) require.NoError(t, err) - require.Nil(t, cs.pendingLogEntry.Upgrades) err = cs.ApplyUpgrades([]*proto.TreeNameUpgrade{}) require.NoError(t, err) - require.Nil(t, cs.pendingLogEntry.Upgrades) } func TestLoadVersionCopyExisting(t *testing.T) { @@ -170,10 +176,6 @@ func TestLoadVersionCopyExisting(t *testing.T) { _, err := cs.LoadVersion(0, false) require.NoError(t, err) - // For the first commit, we need to include initial upgrades in pending entry - // so they are written to WAL for replay - cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} - // Apply and commit some data err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { @@ -320,10 +322,6 @@ func TestRollback(t *testing.T) { // Commit a few versions for i := 0; i < 3; i++ { - // First commit needs initial upgrades for WAL replay - if i == 0 { - cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} - } err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { Name: "test", @@ -375,9 +373,6 @@ func TestMultipleCommits(t *testing.T) { version, err := cs.Commit() require.NoError(t, err) require.Equal(t, int64(i), version) - - // Pending entry should be cleared - require.Nil(t, cs.pendingLogEntry.Changesets) } require.Equal(t, int64(5), cs.Version()) @@ -411,18 +406,14 @@ func TestCommitWithUpgradesAndChangesets(t *testing.T) { }) require.NoError(t, err) - // Both should be in pending entry - require.Len(t, cs.pendingLogEntry.Upgrades, 1) - require.Len(t, cs.pendingLogEntry.Changesets, 1) - // Commit version, err := cs.Commit() require.NoError(t, err) require.Equal(t, int64(1), version) - - // Pending entry should be cleared - require.Nil(t, cs.pendingLogEntry.Changesets) - require.Nil(t, cs.pendingLogEntry.Upgrades) + entry := mustReadLastChangelogEntry(t, cs) + // 2 upgrades total: initial store "test" + "newstore" + require.Len(t, entry.Upgrades, 2) + require.Len(t, entry.Changesets, 1) } func TestSetInitialVersion(t *testing.T) { @@ -449,10 +440,6 @@ func TestGetVersions(t *testing.T) { // Commit a few versions for i := 0; i < 3; i++ { - // First commit needs initial upgrades for WAL replay - if i == 0 { - cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} - } err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { Name: "test", @@ -483,21 +470,12 @@ func TestCreateWAL(t *testing.T) { cs := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) cs.Initialize([]string{"test"}) - // WAL is created during NewCommitStore - require.NotNil(t, cs.wal) - - // WAL should be functional - write an entry - entry := proto.ChangelogEntry{ - Version: 1, - Upgrades: []*proto.TreeNameUpgrade{ - {Name: "test"}, - }, - } - err := cs.wal.Write(entry) + _, err := cs.LoadVersion(0, false) require.NoError(t, err) + defer cs.Close() - // Clean up - require.NoError(t, cs.Close()) + // MemIAVL should have opened its changelog WAL. + require.NotNil(t, cs.db.GetWAL()) } func TestLoadVersionReadOnlyWithWALReplay(t *testing.T) { @@ -509,8 +487,7 @@ func TestLoadVersionReadOnlyWithWALReplay(t *testing.T) { _, err := cs.LoadVersion(0, false) require.NoError(t, err) - // Write data with WAL entries - cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} + // Write data (MemIAVL will persist changelog internally) err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { Name: "test", @@ -551,8 +528,7 @@ func TestLoadVersionReadOnlyWithWALReplay(t *testing.T) { roCommitStore := readOnlyCS.(*CommitStore) require.Equal(t, int64(2), roCommitStore.Version()) - // The read-only copy should have its own WAL - require.NotNil(t, roCommitStore.wal) + require.NotNil(t, roCommitStore.db.GetWAL()) // Clean up require.NoError(t, roCommitStore.Close()) @@ -569,7 +545,6 @@ func TestLoadVersionReadOnlyCreatesOwnWAL(t *testing.T) { require.NoError(t, err) // Commit some data with WAL entries - cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { Name: "test", @@ -596,9 +571,8 @@ func TestLoadVersionReadOnlyCreatesOwnWAL(t *testing.T) { // Each should have its own WAL instance ro1 := readOnly1.(*CommitStore) ro2 := readOnly2.(*CommitStore) - require.NotNil(t, ro1.wal) - require.NotNil(t, ro2.wal) - require.NotSame(t, ro1.wal, ro2.wal) + require.NotNil(t, ro1.db.GetWAL()) + require.NotNil(t, ro2.db.GetWAL()) // Clean up require.NoError(t, ro1.Close()) @@ -617,7 +591,6 @@ func TestWALPersistenceAcrossRestart(t *testing.T) { require.NoError(t, err) // Write and commit - cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { Name: "test", @@ -678,9 +651,6 @@ func TestRollbackWithWAL(t *testing.T) { // Commit multiple versions for i := 0; i < 5; i++ { - if i == 0 { - cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} - } err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { Name: "test", @@ -697,7 +667,7 @@ func TestRollbackWithWAL(t *testing.T) { } require.Equal(t, int64(5), cs.Version()) - require.NotNil(t, cs.wal) + require.NotNil(t, cs.db.GetWAL()) // Rollback to version 3 err = cs.Rollback(3) @@ -705,7 +675,7 @@ func TestRollbackWithWAL(t *testing.T) { require.Equal(t, int64(3), cs.Version()) // WAL should still exist after rollback - require.NotNil(t, cs.wal) + require.NotNil(t, cs.db.GetWAL()) require.NoError(t, cs.Close()) @@ -731,7 +701,6 @@ func TestRollbackCreatesWALIfNeeded(t *testing.T) { _, err := cs.LoadVersion(0, false) require.NoError(t, err) - cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { Name: "test", @@ -752,7 +721,6 @@ func TestRollbackCreatesWALIfNeeded(t *testing.T) { // After Close(), create a new CommitStore (WAL creation happens in NewCommitStore) cs2 := NewCommitStore(dir, logger.NewNopLogger(), config.StateCommitConfig{}) cs2.Initialize([]string{"test"}) - require.NotNil(t, cs2.wal) // Rollback should work require.NoError(t, cs2.Rollback(1)) @@ -767,14 +735,13 @@ func TestCloseReleasesWAL(t *testing.T) { _, err := cs.LoadVersion(0, false) require.NoError(t, err) - // WAL should exist after load - require.NotNil(t, cs.wal) + require.NotNil(t, cs.db) + require.NotNil(t, cs.db.GetWAL()) // Close require.NoError(t, cs.Close()) - // WAL should be nil after close - require.Nil(t, cs.wal) + // DB should be nil after close require.Nil(t, cs.db) } @@ -787,11 +754,9 @@ func TestLoadVersionReusesExistingWAL(t *testing.T) { _, err := cs.LoadVersion(0, false) require.NoError(t, err) - // WAL should be created - require.NotNil(t, cs.wal) + require.NotNil(t, cs.db.GetWAL()) // Commit some data - cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { Name: "test", @@ -810,8 +775,7 @@ func TestLoadVersionReusesExistingWAL(t *testing.T) { _, err = cs.LoadVersion(0, false) require.NoError(t, err) - // WAL should still exist - require.NotNil(t, cs.wal) + require.NotNil(t, cs.db.GetWAL()) // Version should be replayed require.Equal(t, int64(1), cs.Version()) @@ -829,7 +793,6 @@ func TestReadOnlyCopyCannotCommit(t *testing.T) { require.NoError(t, err) // Commit initial data - cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { Name: "test", @@ -880,9 +843,6 @@ func TestWALTruncationOnCommit(t *testing.T) { // Commit multiple versions to trigger snapshot creation and WAL truncation for i := 0; i < 10; i++ { - if i == 0 { - cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} - } err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { Name: "test", @@ -902,7 +862,7 @@ func TestWALTruncationOnCommit(t *testing.T) { require.Equal(t, int64(10), cs.Version()) // Get WAL state - firstWALIndex, err := cs.wal.FirstOffset() + firstWALIndex, err := cs.db.GetWAL().FirstOffset() require.NoError(t, err) // Get earliest snapshot version - may not exist yet if snapshots are async @@ -942,7 +902,6 @@ func TestWALTruncationWithNoSnapshots(t *testing.T) { require.NoError(t, err) // Commit a version - cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { Name: "test", @@ -961,7 +920,7 @@ func TestWALTruncationWithNoSnapshots(t *testing.T) { require.NoError(t, err) // WAL should still have entries - firstIndex, err := cs.wal.FirstOffset() + firstIndex, err := cs.db.GetWAL().FirstOffset() require.NoError(t, err) require.Equal(t, uint64(1), firstIndex, "WAL should not be truncated when no snapshots exist") @@ -989,9 +948,6 @@ func TestWALTruncationDelta(t *testing.T) { // Commit multiple versions for i := 0; i < 10; i++ { - if i == 0 { - cs.pendingLogEntry.Upgrades = []*proto.TreeNameUpgrade{{Name: "test"}} - } err = cs.ApplyChangeSets([]*proto.NamedChangeSet{ { Name: "test", @@ -1015,6 +971,7 @@ func TestWALTruncationDelta(t *testing.T) { // Reopen cs2 := NewCommitStore(dir, logger.NewNopLogger(), cfg) + cs2.Initialize([]string{"test"}) _, err = cs2.LoadVersion(0, false) require.NoError(t, err) @@ -1023,7 +980,7 @@ func TestWALTruncationDelta(t *testing.T) { require.Equal(t, int64(99), walDelta, "Delta should be 99 (firstVersion 100 - firstIndex 1)") // Verify WAL truncation respects delta - firstWALIndex, err := cs2.wal.FirstOffset() + firstWALIndex, err := cs2.db.GetWAL().FirstOffset() require.NoError(t, err) // Get earliest snapshot version - may not exist yet if snapshots are async From 407b3a9a9d6a0920752e596c388f06568709c0c0 Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Tue, 13 Jan 2026 02:27:01 -0800 Subject: [PATCH 20/35] Fix lint --- go.work.sum | 754 ++++++++++++++++++++++++++- sei-db/db_engine/pebbledb/mvcc/db.go | 1 - 2 files changed, 752 insertions(+), 3 deletions(-) diff --git a/go.work.sum b/go.work.sum index d518185b6a..33ad410ee2 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,215 +1,923 @@ +bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512 h1:SRsZGA7aFnCZETmov57jwPrWuTmaZK6+4R4v5FUe1/c= +bitbucket.org/creachadair/shell v0.0.6 h1:reJflDbKqnlnqb4Oo2pQ1/BqmY/eCWcNGHrIUO8qIzc= cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= +cel.dev/expr v0.16.1 h1:NR0+oFYzR1CqLFhTAqg3ql59G9VfN8fKq1TCHJ6gq1g= cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8= cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +cloud.google.com/go/accessapproval v1.8.2 h1:h4u1MypgeYXTGvnNc1luCBLDN4Kb9Re/gw0Atvoi8HE= cloud.google.com/go/accessapproval v1.8.2/go.mod h1:aEJvHZtpjqstffVwF/2mCXXSQmpskyzvw6zKLvLutZM= +cloud.google.com/go/accesscontextmanager v1.9.2 h1:P0uVixQft8aacbZ7VDZStNZdrftF24Hk8JkA3kfvfqI= cloud.google.com/go/accesscontextmanager v1.9.2/go.mod h1:T0Sw/PQPyzctnkw1pdmGAKb7XBA84BqQzH0fSU7wzJU= +cloud.google.com/go/aiplatform v1.69.0 h1:XvBzK8e6/6ufbi/i129Vmn/gVqFwbNPmRQ89K+MGlgc= cloud.google.com/go/aiplatform v1.69.0/go.mod h1:nUsIqzS3khlnWvpjfJbP+2+h+VrFyYsTm7RNCAViiY8= +cloud.google.com/go/analytics v0.25.2 h1:KgJ5Taxtsnro/co7WIhmAHi5pzYAtvxu8LMqenPAlSo= cloud.google.com/go/analytics v0.25.2/go.mod h1:th0DIunqrhI1ZWVlT3PH2Uw/9ANX8YHfFDEPqf/+7xM= +cloud.google.com/go/apigateway v1.7.2 h1:TRB5q0vvbT5Yx4bNSCWlqLJFJnhc7tDlCR9ccpo1vzg= cloud.google.com/go/apigateway v1.7.2/go.mod h1:+weId+9aR9J6GRwDka7jIUSrKEX60XGcikX7dGU8O7M= +cloud.google.com/go/apigeeconnect v1.7.2 h1:GHg0ddEQUZ08C1qC780P5wwY/jaIW8UtxuRQXLLuRXs= cloud.google.com/go/apigeeconnect v1.7.2/go.mod h1:he/SWi3A63fbyxrxD6jb67ak17QTbWjva1TFbT5w8Kw= +cloud.google.com/go/apigeeregistry v0.9.2 h1:fC3ZXEk2QsBxUlZZDZpbBGXC/ZQglCBmHDGgY5aNipg= cloud.google.com/go/apigeeregistry v0.9.2/go.mod h1:A5n/DwpG5NaP2fcLYGiFA9QfzpQhPRFNATO1gie8KM8= +cloud.google.com/go/apikeys v0.6.0 h1:B9CdHFZTFjVti89tmyXXrO+7vSNo2jvZuHG8zD5trdQ= +cloud.google.com/go/appengine v1.9.2 h1:pxAQ//FsyEQsaF9HJduPCOEvj9GV4fvnLARGz1+KDzM= cloud.google.com/go/appengine v1.9.2/go.mod h1:bK4dvmMG6b5Tem2JFZcjvHdxco9g6t1pwd3y/1qr+3s= +cloud.google.com/go/area120 v0.9.2 h1:LODm6TjW27/LJ4z4fBNJHRb+tlvy0gSu6Vb8j2lfluY= cloud.google.com/go/area120 v0.9.2/go.mod h1:Ar/KPx51UbrTWGVGgGzFnT7hFYQuk/0VOXkvHdTbQMI= +cloud.google.com/go/artifactregistry v1.16.0 h1:BZpz0x8HCG7hwTkD+GlUwPQVFGOo9w84t8kxQwwc0DA= cloud.google.com/go/artifactregistry v1.16.0/go.mod h1:LunXo4u2rFtvJjrGjO0JS+Gs9Eco2xbZU6JVJ4+T8Sk= +cloud.google.com/go/asset v1.20.3 h1:/jQBAkZVUbsIczRepDkwaf/K5NcRYvQ6MBiWg5i20fU= cloud.google.com/go/asset v1.20.3/go.mod h1:797WxTDwdnFAJzbjZ5zc+P5iwqXc13yO9DHhmS6wl+o= +cloud.google.com/go/assuredworkloads v1.12.2 h1:6Y6a4V7CD50qtjvayhu7f5o35UFJP8ade7IbHNfdQEc= cloud.google.com/go/assuredworkloads v1.12.2/go.mod h1:/WeRr/q+6EQYgnoYrqCVgw7boMoDfjXZZev3iJxs2Iw= +cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs= cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q= +cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU= cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= +cloud.google.com/go/automl v1.14.2 h1:RzR5Nx78iaF2FNAfaaQ/7o2b4VuQ17YbOaeK/DLYSW4= cloud.google.com/go/automl v1.14.2/go.mod h1:mIat+Mf77W30eWQ/vrhjXsXaRh8Qfu4WiymR0hR6Uxk= +cloud.google.com/go/baremetalsolution v1.3.2 h1:rhawlI+9gy/i1ZQbN/qL6FXHGXusWbfr6UoQdcCpybw= cloud.google.com/go/baremetalsolution v1.3.2/go.mod h1:3+wqVRstRREJV/puwaKAH3Pnn7ByreZG2aFRsavnoBQ= +cloud.google.com/go/batch v1.11.2 h1:OVhgpMMJc+mrFw51R3C06JKC0D6u125RlEBULpg78No= cloud.google.com/go/batch v1.11.2/go.mod h1:ehsVs8Y86Q4K+qhEStxICqQnNqH8cqgpCxx89cmU5h4= +cloud.google.com/go/beyondcorp v1.1.2 h1:hzKZf9ScvqTWqR8xGKVvD35ScQuxbMySELvJ0OW1usI= cloud.google.com/go/beyondcorp v1.1.2/go.mod h1:q6YWSkEsSZTU2WDt1qtz6P5yfv79wgktGtNbd0FJTLI= +cloud.google.com/go/bigquery v1.64.0 h1:vSSZisNyhr2ioJE1OuYBQrnrpB7pIhRQm4jfjc7E/js= cloud.google.com/go/bigquery v1.64.0/go.mod h1:gy8Ooz6HF7QmA+TRtX8tZmXBKH5mCFBwUApGAb3zI7Y= +cloud.google.com/go/bigtable v1.33.0 h1:2BDaWLRAwXO14DJL/u8crbV2oUbMZkIa2eGq8Yao1bk= cloud.google.com/go/bigtable v1.33.0/go.mod h1:HtpnH4g25VT1pejHRtInlFPnN5sjTxbQlsYBjh9t5l0= +cloud.google.com/go/billing v1.19.2 h1:shcyz1UkrUxbPsqHL6L84ZdtBZ7yocaFFCxMInTsrNo= cloud.google.com/go/billing v1.19.2/go.mod h1:AAtih/X2nka5mug6jTAq8jfh1nPye0OjkHbZEZgU59c= +cloud.google.com/go/binaryauthorization v1.9.2 h1:zZX4cvtYSXc5ogOar1w5KA1BLz3j464RPSaR/HhroJ8= cloud.google.com/go/binaryauthorization v1.9.2/go.mod h1:T4nOcRWi2WX4bjfSRXJkUnpliVIqjP38V88Z10OvEv4= +cloud.google.com/go/certificatemanager v1.9.2 h1:/lO1ejN415kRaiO6DNNCHj0UvQujKP714q3l8gp4lsY= cloud.google.com/go/certificatemanager v1.9.2/go.mod h1:PqW+fNSav5Xz8bvUnJpATIRo1aaABP4mUg/7XIeAn6c= +cloud.google.com/go/channel v1.19.1 h1:l4XcnfzJ5UGmqZQls0atcpD6ERDps4PLd5hXSyTWFv0= cloud.google.com/go/channel v1.19.1/go.mod h1:ungpP46l6XUeuefbA/XWpWWnAY3897CSRPXUbDstwUo= +cloud.google.com/go/cloudbuild v1.19.0 h1:Uo0bL251yvyWsNtO3Og9m5Z4S48cgGf3IUX7xzOcl8s= cloud.google.com/go/cloudbuild v1.19.0/go.mod h1:ZGRqbNMrVGhknIIjwASa6MqoRTOpXIVMSI+Ew5DMPuY= +cloud.google.com/go/clouddms v1.8.2 h1:U53ztLRgTkclaxgmBBles+tv+nNcZ5fhbRbw3b2axFw= cloud.google.com/go/clouddms v1.8.2/go.mod h1:pe+JSp12u4mYOkwXpSMouyCCuQHL3a6xvWH2FgOcAt4= +cloud.google.com/go/cloudtasks v1.13.2 h1:x6Qw5JyNbH3reL0arUtlYf77kK6OVjZZ//8JCvUkLro= cloud.google.com/go/cloudtasks v1.13.2/go.mod h1:2pyE4Lhm7xY8GqbZKLnYk7eeuh8L0JwAvXx1ecKxYu8= +cloud.google.com/go/compute v1.29.0 h1:Lph6d8oPi38NHkOr6S55Nus/Pbbcp37m/J0ohgKAefs= cloud.google.com/go/compute v1.29.0/go.mod h1:HFlsDurE5DpQZClAGf/cYh+gxssMhBxBovZDYkEn/Og= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/contactcenterinsights v1.15.1 h1:cR/gQMweaG8RIWAlS5Jo1ARi8LUVQJ51t84EUefHeZ8= cloud.google.com/go/contactcenterinsights v1.15.1/go.mod h1:cFGxDVm/OwEVAHbU9UO4xQCtQFn0RZSrSUcF/oJ0Bbs= +cloud.google.com/go/container v1.42.0 h1:sH9Hj9SoLeP+uKvLXc/04nWyWDiMo4Q85xfb1Nl5sAg= cloud.google.com/go/container v1.42.0/go.mod h1:YL6lDgCUi3frIWNIFU9qrmF7/6K1EYrtspmFTyyqJ+k= +cloud.google.com/go/containeranalysis v0.13.2 h1:AG2gOcfZJFRiz+3SZCPnxU+gwbzKe++QSX/ej71Lom8= cloud.google.com/go/containeranalysis v0.13.2/go.mod h1:AiKvXJkc3HiqkHzVIt6s5M81wk+q7SNffc6ZlkTDgiE= +cloud.google.com/go/datacatalog v1.23.0 h1:9F2zIbWNNmtrSkPIyGRQNsIugG5VgVVFip6+tXSdWLg= cloud.google.com/go/datacatalog v1.23.0/go.mod h1:9Wamq8TDfL2680Sav7q3zEhBJSPBrDxJU8WtPJ25dBM= +cloud.google.com/go/dataflow v0.10.2 h1:o9P5/zR2mOYJmCnfp9/7RprKFZCwmSu3TvemQSmCaFM= cloud.google.com/go/dataflow v0.10.2/go.mod h1:+HIb4HJxDCZYuCqDGnBHZEglh5I0edi/mLgVbxDf0Ag= +cloud.google.com/go/dataform v0.10.2 h1:t16DoejuOHoxJR88qrpdmFFlCXA9+x5PKrqI9qiDYz0= cloud.google.com/go/dataform v0.10.2/go.mod h1:oZHwMBxG6jGZCVZqqMx+XWXK+dA/ooyYiyeRbUxI15M= +cloud.google.com/go/datafusion v1.8.2 h1:RPoHvIeXexXwlWhEU6DNgrYCh+C+FR2EXbrnMs2ptpI= cloud.google.com/go/datafusion v1.8.2/go.mod h1:XernijudKtVG/VEvxtLv08COyVuiYPraSxm+8hd4zXA= +cloud.google.com/go/datalabeling v0.9.2 h1:UesbU2kYIUWhHUcnFS86ANPbugEq98X9k1whTNcenlc= cloud.google.com/go/datalabeling v0.9.2/go.mod h1:8me7cCxwV/mZgYWtRAd3oRVGFD6UyT7hjMi+4GRyPpg= +cloud.google.com/go/dataplex v1.19.2 h1:R2xnsZnuWpHi2NmBR0e43GZk2IZcQ1AFEAo1fUI0xsw= cloud.google.com/go/dataplex v1.19.2/go.mod h1:vsxxdF5dgk3hX8Ens9m2/pMNhQZklUhSgqTghZtF1v4= +cloud.google.com/go/dataproc v1.12.0 h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU= +cloud.google.com/go/dataproc/v2 v2.10.0 h1:B0b7eLRXzFTzb4UaxkGGidIF23l/Xpyce28m1Q0cHmU= cloud.google.com/go/dataproc/v2 v2.10.0/go.mod h1:HD16lk4rv2zHFhbm8gGOtrRaFohMDr9f0lAUMLmg1PM= +cloud.google.com/go/dataqna v0.9.2 h1:hrEcid5jK5fEdlYZ0eS8HJoq+ZCTRWSV7Av42V/G994= cloud.google.com/go/dataqna v0.9.2/go.mod h1:WCJ7pwD0Mi+4pIzFQ+b2Zqy5DcExycNKHuB+VURPPgs= +cloud.google.com/go/datastore v1.20.0 h1:NNpXoyEqIJmZFc0ACcwBEaXnmscUpcG4NkKnbCePmiM= cloud.google.com/go/datastore v1.20.0/go.mod h1:uFo3e+aEpRfHgtp5pp0+6M0o147KoPaYNaPAKpfh8Ew= +cloud.google.com/go/datastream v1.11.2 h1:vgtrwwPfY7JFEDD0VARJK4qyiApnFnPkFRQVuczYb/w= cloud.google.com/go/datastream v1.11.2/go.mod h1:RnFWa5zwR5SzHxeZGJOlQ4HKBQPcjGfD219Qy0qfh2k= +cloud.google.com/go/deploy v1.25.0 h1:nYLFG2TSsYMJuengVru5P8iWnA5mNA4rKFV5YoOWQ3M= cloud.google.com/go/deploy v1.25.0/go.mod h1:h9uVCWxSDanXUereI5WR+vlZdbPJ6XGy+gcfC25v5rM= +cloud.google.com/go/dialogflow v1.60.0 h1:H+Q1SUeVU2La0Y0ZGEaKkhEXg3bj9Ceg5YKcMbyNOEc= cloud.google.com/go/dialogflow v1.60.0/go.mod h1:PjsrI+d2FI4BlGThxL0+Rua/g9vLI+2A1KL7s/Vo3pY= +cloud.google.com/go/dlp v1.20.0 h1:Wwz1FoZp3pyrTNkS5fncaAccP/AbqzLQuN5WMi3aVYQ= cloud.google.com/go/dlp v1.20.0/go.mod h1:nrGsA3r8s7wh2Ct9FWu69UjBObiLldNyQda2RCHgdaY= +cloud.google.com/go/documentai v1.35.0 h1:DO4ut86a+Xa0gBq7j3FZJPavnKBNoznrg44csnobqIY= cloud.google.com/go/documentai v1.35.0/go.mod h1:ZotiWUlDE8qXSUqkJsGMQqVmfTMYATwJEYqbPXTR9kk= +cloud.google.com/go/domains v0.10.2 h1:ekJCkuzbciXyPKkwPwvI+2Ov1GcGJtMXj/fbgilPFqg= cloud.google.com/go/domains v0.10.2/go.mod h1:oL0Wsda9KdJvvGNsykdalHxQv4Ri0yfdDkIi3bzTUwk= +cloud.google.com/go/edgecontainer v1.4.0 h1:vpKTEkQPpkl55d6aUU2rzDFvTkMUATvBXfZSlI2KMR0= cloud.google.com/go/edgecontainer v1.4.0/go.mod h1:Hxj5saJT8LMREmAI9tbNTaBpW5loYiWFyisCjDhzu88= +cloud.google.com/go/errorreporting v0.3.1 h1:E/gLk+rL7u5JZB9oq72iL1bnhVlLrnfslrgcptjJEUE= cloud.google.com/go/errorreporting v0.3.1/go.mod h1:6xVQXU1UuntfAf+bVkFk6nld41+CPyF2NSPCyXE3Ztk= +cloud.google.com/go/essentialcontacts v1.7.2 h1:a/reGTn7WblM5DgieiLbX6CswHgTneWrA4ZNS5E+1Bg= cloud.google.com/go/essentialcontacts v1.7.2/go.mod h1:NoCBlOIVteJFJU+HG9dIG/Cc9kt1K9ys9mbOaGPUmPc= +cloud.google.com/go/eventarc v1.15.0 h1:IVU2EOR8P2f6N8eneuwspN122LR87v9G54B+7ihd1TY= cloud.google.com/go/eventarc v1.15.0/go.mod h1:PAd/pPIZdJtJQFJI1yDEUms1mqohdNuM1BFEVHHlVFg= +cloud.google.com/go/filestore v1.9.2 h1:DYwMNAcF5bELHHMxRdkIWWZ3XicKp+ZpEBy+c6Gt4uY= cloud.google.com/go/filestore v1.9.2/go.mod h1:I9pM7Hoetq9a7djC1xtmtOeHSUYocna09ZP6x+PG1Xw= +cloud.google.com/go/firestore v1.17.0 h1:iEd1LBbkDZTFsLw3sTH50eyg4qe8eoG6CjocmEXO9aQ= cloud.google.com/go/firestore v1.17.0/go.mod h1:69uPx1papBsY8ZETooc71fOhoKkD70Q1DwMrtKuOT/Y= +cloud.google.com/go/functions v1.19.2 h1:Cu2Gj1JBBJv9gi89r8LrZNsJhGwePnhttn4Blqw/EYI= cloud.google.com/go/functions v1.19.2/go.mod h1:SBzWwWuaFDLnUyStDAMEysVN1oA5ECLbP3/PfJ9Uk7Y= +cloud.google.com/go/gaming v1.9.0 h1:7vEhFnZmd931Mo7sZ6pJy7uQPDxF7m7v8xtBheG08tc= +cloud.google.com/go/gkebackup v1.6.2 h1:lWaSgjSonOXe41UhwQjts6lhDZdr5e882LNUTtnjZS0= cloud.google.com/go/gkebackup v1.6.2/go.mod h1:WsTSWqKJkGan1pkp5dS30oxb+Eaa6cLvxEUxKTUALwk= +cloud.google.com/go/gkeconnect v0.12.0 h1:MuA3/aIuncXkXuUDGdbT7OLnIp7xpFhciuHAnQaoQz4= cloud.google.com/go/gkeconnect v0.12.0/go.mod h1:zn37LsFiNZxPN4iO7YbUk8l/E14pAJ7KxpoXoxt7Ly0= +cloud.google.com/go/gkehub v0.15.2 h1:CR5MPEP/Ogk5IahCq3O2fKS6TJZQi8mrnrysGHCs0g8= cloud.google.com/go/gkehub v0.15.2/go.mod h1:8YziTOpwbM8LM3r9cHaOMy2rNgJHXZCrrmGgcau9zbQ= +cloud.google.com/go/gkemulticloud v1.4.1 h1:SvVD2nJTGScEDYygIQ5dI14oFYhgtJx8HazkT3aufEI= cloud.google.com/go/gkemulticloud v1.4.1/go.mod h1:KRvPYcx53bztNwNInrezdfNF+wwUom8Y3FuJBwhvFpQ= +cloud.google.com/go/grafeas v0.3.11 h1:CobnwnyeY1j1Defi5vbEircI+jfrk3ci5m004ZjiFP4= +cloud.google.com/go/grafeas v0.3.11/go.mod h1:dcQyG2+T4tBgG0MvJAh7g2wl/xHV2w+RZIqivwuLjNg= +cloud.google.com/go/gsuiteaddons v1.7.2 h1:Rma+a2tCB2PV0Rm87Ywr4P96dCwGIm8vw8gF23ZlYoY= cloud.google.com/go/gsuiteaddons v1.7.2/go.mod h1:GD32J2rN/4APilqZw4JKmwV84+jowYYMkEVwQEYuAWc= +cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA= cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= +cloud.google.com/go/iap v1.10.2 h1:rvM+FNIF2wIbwUU8299FhhVGak2f7oOvbW8J/I5oflE= cloud.google.com/go/iap v1.10.2/go.mod h1:cClgtI09VIfazEK6VMJr6bX8KQfuQ/D3xqX+d0wrUlI= +cloud.google.com/go/ids v1.5.2 h1:EDYZQraE+Eq6BewUQxVRY8b3VUUo/MnjMfzSh1NGjx8= cloud.google.com/go/ids v1.5.2/go.mod h1:P+ccDD96joXlomfonEdCnyrHvE68uLonc7sJBPVM5T0= +cloud.google.com/go/iot v1.8.2 h1:KMN0wujrPV7q0yfs4rt5CUl9Di8sQhJ0uohJn1h6yaI= cloud.google.com/go/iot v1.8.2/go.mod h1:UDwVXvRD44JIcMZr8pzpF3o4iPsmOO6fmbaIYCAg1ww= +cloud.google.com/go/kms v1.20.1 h1:og29Wv59uf2FVaZlesaiDAqHFzHaoUyHI3HYp9VUHVg= cloud.google.com/go/kms v1.20.1/go.mod h1:LywpNiVCvzYNJWS9JUcGJSVTNSwPwi0vBAotzDqn2nc= +cloud.google.com/go/language v1.14.2 h1:rwrIOwcAgPTYbigOaiMSjKCvBy0xHZJbRc7HB/xMECA= cloud.google.com/go/language v1.14.2/go.mod h1:dviAbkxT9art+2ioL9AM05t+3Ql6UPfMpwq1cDsF+rg= +cloud.google.com/go/lifesciences v0.10.2 h1:eZSaRgBwbnb/oXwCj1SGE0Kp534DuXpg55iYBWgN024= cloud.google.com/go/lifesciences v0.10.2/go.mod h1:vXDa34nz0T/ibUNoeHnhqI+Pn0OazUTdxemd0OLkyoY= +cloud.google.com/go/logging v1.12.0 h1:ex1igYcGFd4S/RZWOCU51StlIEuey5bjqwH9ZYjHibk= cloud.google.com/go/logging v1.12.0/go.mod h1:wwYBt5HlYP1InnrtYI0wtwttpVU1rifnMT7RejksUAM= +cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc= cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= +cloud.google.com/go/managedidentities v1.7.2 h1:oWxuIhIwQC1Vfs1SZi1x389W2TV9uyPsAyZMJgZDND4= cloud.google.com/go/managedidentities v1.7.2/go.mod h1:t0WKYzagOoD3FNtJWSWcU8zpWZz2i9cw2sKa9RiPx5I= +cloud.google.com/go/maps v1.15.0 h1:bmFHlO6BL/smC6GD45r5j0ChjsyyevuJCSARdOL62TI= cloud.google.com/go/maps v1.15.0/go.mod h1:ZFqZS04ucwFiHSNU8TBYDUr3wYhj5iBFJk24Ibvpf3o= +cloud.google.com/go/mediatranslation v0.9.2 h1:p37R/k9+L33bUMO87gFyv93MwJ+9nuzVhXM5X+6ULwA= cloud.google.com/go/mediatranslation v0.9.2/go.mod h1:1xyRoDYN32THzy+QaU62vIMciX0CFexplju9t30XwUc= +cloud.google.com/go/memcache v1.11.2 h1:GGgC2A9AClJN8VLbMUAPUxj/dNMFwz6Lj01gDxPw7os= cloud.google.com/go/memcache v1.11.2/go.mod h1:jIzHn79b0m5wbkax2SdlW5vNSbpaEk0yWHbeLpMIYZE= +cloud.google.com/go/metastore v1.14.2 h1:Euc9kLTKS8T6M1JVqQavwDFHu9UtT1//lGXSKjpO3/0= cloud.google.com/go/metastore v1.14.2/go.mod h1:dk4zOBhZIy3TFOQlI8sbOa+ef0FjAcCHEnd8dO2J+LE= +cloud.google.com/go/monitoring v1.21.2 h1:FChwVtClH19E7pJ+e0xUhJPGksctZNVOk2UhMmblmdU= cloud.google.com/go/monitoring v1.21.2/go.mod h1:hS3pXvaG8KgWTSz+dAdyzPrGUYmi2Q+WFX8g2hqVEZU= +cloud.google.com/go/networkconnectivity v1.15.2 h1:CuBLrRKhPbzXkFGADopQUpMcdY+SSfoy/3RqsMH2pq4= cloud.google.com/go/networkconnectivity v1.15.2/go.mod h1:N1O01bEk5z9bkkWwXLKcN2T53QN49m/pSpjfUvlHDQY= +cloud.google.com/go/networkmanagement v1.16.0 h1:oT7c2Oo9NT54XjnP4GMNj/HEywrFnBz0u6QLJ2iu8NE= cloud.google.com/go/networkmanagement v1.16.0/go.mod h1:Yc905R9U5jik5YMt76QWdG5WqzPU4ZsdI/mLnVa62/Q= +cloud.google.com/go/networksecurity v0.10.2 h1://zFZM8XZZs+3Y6QKuLqwD5tZ+B/17KUo/rJpGW2tJs= cloud.google.com/go/networksecurity v0.10.2/go.mod h1:puU3Gwchd6Y/VTyMkL50GI2RSRMS3KXhcDBY1HSOcck= +cloud.google.com/go/notebooks v1.12.2 h1:BHIH9kf/02wSCcLAVttEXHSFAgSotgRg2y1YjR7VDCc= cloud.google.com/go/notebooks v1.12.2/go.mod h1:EkLwv8zwr8DUXnvzl944+sRBG+b73HEKzV632YYAGNI= +cloud.google.com/go/optimization v1.7.2 h1:yM4teRB60qyIm8cV4VRW4wepmHbXCoqv3QKGfKzylEQ= cloud.google.com/go/optimization v1.7.2/go.mod h1:msYgDIh1SGSfq6/KiWJQ/uxMkWq8LekPyn1LAZ7ifNE= +cloud.google.com/go/orchestration v1.11.1 h1:uZOwdQoAamx8+X0UdMqY/lro3/h/Zhb7SnfArufNVcc= cloud.google.com/go/orchestration v1.11.1/go.mod h1:RFHf4g88Lbx6oKhwFstYiId2avwb6oswGeAQ7Tjjtfw= +cloud.google.com/go/orgpolicy v1.14.1 h1:c1QLoM5v8/aDKgYVCUaC039lD3GPvqAhTVOwsGhIoZQ= cloud.google.com/go/orgpolicy v1.14.1/go.mod h1:1z08Hsu1mkoH839X7C8JmnrqOkp2IZRSxiDw7W/Xpg4= +cloud.google.com/go/osconfig v1.14.2 h1:iBN87PQc+EGh5QqijM3CuxcibvDWmF+9k0eOJT27FO4= cloud.google.com/go/osconfig v1.14.2/go.mod h1:kHtsm0/j8ubyuzGciBsRxFlbWVjc4c7KdrwJw0+g+pQ= +cloud.google.com/go/oslogin v1.14.2 h1:6ehIKkALrLe9zUHwEmfXRVuSPm3HiUmEnnDRr7yLIo8= cloud.google.com/go/oslogin v1.14.2/go.mod h1:M7tAefCr6e9LFTrdWRQRrmMeKHbkvc4D9g6tHIjHySA= +cloud.google.com/go/phishingprotection v0.9.2 h1:SaW0IPf/1fflnzomjy7+9EMtReXuxkYpUAf/77m5xL8= cloud.google.com/go/phishingprotection v0.9.2/go.mod h1:mSCiq3tD8fTJAuXq5QBHFKZqMUy8SfWsbUM9NpzJIRQ= +cloud.google.com/go/policytroubleshooter v1.11.2 h1:sTIH5AQ8tcgmnqrqlZfYWymjMhPh4ZEt4CvQGgG+kzc= cloud.google.com/go/policytroubleshooter v1.11.2/go.mod h1:1TdeCRv8Qsjcz2qC3wFltg/Mjga4HSpv8Tyr5rzvPsw= +cloud.google.com/go/privatecatalog v0.10.2 h1:01RPfn8IL2//8UHAmImRraTFYM/3gAEiIxudWLWrp+0= cloud.google.com/go/privatecatalog v0.10.2/go.mod h1:o124dHoxdbO50ImR3T4+x3GRwBSTf4XTn6AatP8MgsQ= +cloud.google.com/go/pubsub v1.45.1 h1:ZC/UzYcrmK12THWn1P72z+Pnp2vu/zCZRXyhAfP1hJY= cloud.google.com/go/pubsub v1.45.1/go.mod h1:3bn7fTmzZFwaUjllitv1WlsNMkqBgGUb3UdMhI54eCc= +cloud.google.com/go/pubsublite v1.8.2 h1:jLQozsEVr+c6tOU13vDugtnaBSUy/PD5zK6mhm+uF1Y= cloud.google.com/go/pubsublite v1.8.2/go.mod h1:4r8GSa9NznExjuLPEJlF1VjOPOpgf3IT6k8x/YgaOPI= +cloud.google.com/go/recaptchaenterprise v1.3.1 h1:u6EznTGzIdsyOsvm+Xkw0aSuKFXQlyjGE9a4exk6iNQ= +cloud.google.com/go/recaptchaenterprise/v2 v2.19.0 h1:J/J7ZeVOX+sqn0hxzkOBfnQfBAzPZt8KaAuQoarQWQM= cloud.google.com/go/recaptchaenterprise/v2 v2.19.0/go.mod h1:vnbA2SpVPPwKeoFrCQxR+5a0JFRRytwBBG69Zj9pGfk= +cloud.google.com/go/recommendationengine v0.9.2 h1:RHVdmoNBdzgRJXI/3SV+GB5TTv/umsVguiaEvmKOh98= cloud.google.com/go/recommendationengine v0.9.2/go.mod h1:DjGfWZJ68ZF5ZuNgoTVXgajFAG0yLt4CJOpC0aMK3yw= +cloud.google.com/go/recommender v1.13.2 h1:xDFzlFk5Xp5MXnac468eicKM3MUo6UNdxoYuBMOF1mE= cloud.google.com/go/recommender v1.13.2/go.mod h1:XJau4M5Re8F4BM+fzF3fqSjxNJuM66fwF68VCy/ngGE= +cloud.google.com/go/redis v1.17.2 h1:QbW264RBH+NSVEQqlDoHfoxcreXK8QRRByTOR2CFbJs= cloud.google.com/go/redis v1.17.2/go.mod h1:h071xkcTMnJgQnU/zRMOVKNj5J6AttG16RDo+VndoNo= +cloud.google.com/go/resourcemanager v1.10.2 h1:LpqZZGM0uJiu1YWM878AA8zZ/qOQ/Ngno60Q8RAraAI= cloud.google.com/go/resourcemanager v1.10.2/go.mod h1:5f+4zTM/ZOTDm6MmPOp6BQAhR0fi8qFPnvVGSoWszcc= +cloud.google.com/go/resourcesettings v1.8.2 h1:ISRX2HZHNS17F/EuIwzPrQwEyIyUJayGuLrS51yt6Wk= cloud.google.com/go/resourcesettings v1.8.2/go.mod h1:uEgtPiMA+xuBUM4Exu+ZkNpMYP0BLlYeJbyNHfrc+U0= +cloud.google.com/go/retail v1.19.1 h1:FVzvA+VuEdNoMz2WzWZ5KwfG+CX+jSv+SOspyQPLuRs= cloud.google.com/go/retail v1.19.1/go.mod h1:W48zg0zmt2JMqmJKCuzx0/0XDLtovwzGAeJjmv6VPaE= +cloud.google.com/go/run v1.7.0 h1:GJtHWUgi8CK+YPhmTR3tKBAmDmU9RRMYqiGKCmIgFG8= cloud.google.com/go/run v1.7.0/go.mod h1:IvJOg2TBb/5a0Qkc6crn5yTy5nkjcgSWQLhgO8QL8PQ= +cloud.google.com/go/scheduler v1.11.2 h1:PfkvJP1qKu9NvFB65Ja/s918bPZWMBcYkg35Ljdw1Oc= cloud.google.com/go/scheduler v1.11.2/go.mod h1:GZSv76T+KTssX2I9WukIYQuQRf7jk1WI+LOcIEHUUHk= +cloud.google.com/go/secretmanager v1.14.2 h1:2XscWCfy//l/qF96YE18/oUaNJynAx749Jg3u0CjQr8= cloud.google.com/go/secretmanager v1.14.2/go.mod h1:Q18wAPMM6RXLC/zVpWTlqq2IBSbbm7pKBlM3lCKsmjw= +cloud.google.com/go/security v1.18.2 h1:9Nzp9LGjiDvHqy7X7Q9GrS5lIHN0bI8RvDjkrl4ILO0= cloud.google.com/go/security v1.18.2/go.mod h1:3EwTcYw8554iEtgK8VxAjZaq2unFehcsgFIF9nOvQmU= +cloud.google.com/go/securitycenter v1.35.2 h1:XkkE+IRE5/88drGPIuvETCSN7dAnWoqJahZzDbP5Hog= cloud.google.com/go/securitycenter v1.35.2/go.mod h1:AVM2V9CJvaWGZRHf3eG+LeSTSissbufD27AVBI91C8s= +cloud.google.com/go/servicecontrol v1.11.1 h1:d0uV7Qegtfaa7Z2ClDzr9HJmnbJW7jn0WhZ7wOX6hLE= +cloud.google.com/go/servicedirectory v1.12.2 h1:W/oZmTUzlWbeSTujRbmG9v7HZyHcorj608tkcD3vVYE= cloud.google.com/go/servicedirectory v1.12.2/go.mod h1:F0TJdFjqqotiZRlMXgIOzszaplk4ZAmUV8ovHo08M2U= +cloud.google.com/go/servicemanagement v1.8.0 h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkvi+gzu+NjywY= +cloud.google.com/go/serviceusage v1.6.0 h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4= +cloud.google.com/go/shell v1.8.2 h1:lSfdEng3n7zZHzC40BJ4trEMyme3CGnLLnA09MlLQdQ= cloud.google.com/go/shell v1.8.2/go.mod h1:QQR12T6j/eKvqAQLv6R3ozeoqwJ0euaFSz2qLqG93Bs= +cloud.google.com/go/spanner v1.73.0 h1:0bab8QDn6MNj9lNK6XyGAVFhMlhMU2waePPa6GZNoi8= cloud.google.com/go/spanner v1.73.0/go.mod h1:mw98ua5ggQXVWwp83yjwggqEmW9t8rjs9Po1ohcUGW4= +cloud.google.com/go/speech v1.25.2 h1:rKOXU9LAZTOYHhRNB4gZDekNjJx21TktQpetBa5IzOk= cloud.google.com/go/speech v1.25.2/go.mod h1:KPFirZlLL8SqPaTtG6l+HHIFHPipjbemv4iFg7rTlYs= +cloud.google.com/go/storage v1.49.0 h1:zenOPBOWHCnojRd9aJZAyQXBYqkJkdQS42dxL55CIMw= cloud.google.com/go/storage v1.49.0/go.mod h1:k1eHhhpLvrPjVGfo0mOUPEJ4Y2+a/Hv5PiwehZI9qGU= +cloud.google.com/go/storagetransfer v1.11.2 h1:hMcP8ECmxedXjPxr2j3Ca45ro/TKEF+1YYjq2p5LMTI= cloud.google.com/go/storagetransfer v1.11.2/go.mod h1:FcM29aY4EyZ3yVPmW5SxhqUdhjgPBUOFyy4rqiQbias= +cloud.google.com/go/talent v1.7.2 h1:KONR7KX/EXI3pO2cbSIDOBqhBzvgDS71vaMz8k4qRCg= cloud.google.com/go/talent v1.7.2/go.mod h1:k1sqlDgS9gbc0gMTRuRQpX6C6VB7bGUxSPcoTRWJod8= +cloud.google.com/go/texttospeech v1.10.0 h1:icRAxYDtq3zO1T0YBT/fe8C/7pXoIqfkY4iYr5zG39I= cloud.google.com/go/texttospeech v1.10.0/go.mod h1:215FpCOyRxxrS7DSb2t7f4ylMz8dXsQg8+Vdup5IhP4= +cloud.google.com/go/tpu v1.7.2 h1:xPBJd7xZgtl3CgrZoaUf7zFPVVj68jmzzGTSzkcsOtQ= cloud.google.com/go/tpu v1.7.2/go.mod h1:0Y7dUo2LIbDUx0yQ/vnLC6e18FK6NrDfAhYS9wZ/2vs= +cloud.google.com/go/trace v1.11.2 h1:4ZmaBdL8Ng/ajrgKqY5jfvzqMXbrDcBsUGXOT9aqTtI= cloud.google.com/go/trace v1.11.2/go.mod h1:bn7OwXd4pd5rFuAnTrzBuoZ4ax2XQeG3qNgYmfCy0Io= +cloud.google.com/go/translate v1.12.2 h1:qECivi8O+jFI/vnvN9elK6CME+WAWy56GIBszF+/rNc= cloud.google.com/go/translate v1.12.2/go.mod h1:jjLVf2SVH2uD+BNM40DYvRRKSsuyKxVvs3YjTW/XSWY= +cloud.google.com/go/video v1.23.2 h1:CGAPOXTJMoZm9PeHkohBlMTy8lqN6VWCNDjp5VODfy8= cloud.google.com/go/video v1.23.2/go.mod h1:rNOr2pPHWeCbW0QsOwJRIe0ZiuwHpHtumK0xbiYB1Ew= +cloud.google.com/go/videointelligence v1.12.2 h1:ZLElysepw9vfQGAKWfnxdnSnHSKbEn/nU/tmBnCJLfA= cloud.google.com/go/videointelligence v1.12.2/go.mod h1:8xKGlq0lNVyT8JgTkkCUCpyNJnYYEJVWGdqzv+UcwR8= +cloud.google.com/go/vision v1.2.0 h1:/CsSTkbmO9HC8iQpxbK8ATms3OQaX3YQUeTMGCxlaK4= +cloud.google.com/go/vision/v2 v2.9.2 h1:u4pu3gKps88oUe76WwVPeX9dgWVyyYopZ1s05FwsKEk= cloud.google.com/go/vision/v2 v2.9.2/go.mod h1:WuxjVQdAy4j4WZqY5Rr655EdAgi8B707Vdb5T8c90uo= +cloud.google.com/go/vmmigration v1.8.2 h1:Hpqv3fZ3Ri1OMhTNVJgxxsTou2ZlRzKbnc1dSybTP5Y= cloud.google.com/go/vmmigration v1.8.2/go.mod h1:FBejrsr8ZHmJb949BSOyr3D+/yCp9z9Hk0WtsTiHc1Q= +cloud.google.com/go/vmwareengine v1.3.2 h1:LmkojgSLvsRwU1+c0iiY2XoBkXYKzpArElHC9IDWakg= cloud.google.com/go/vmwareengine v1.3.2/go.mod h1:JsheEadzT0nfXOGkdnwtS1FhFAnj4g8qhi4rKeLi/AU= +cloud.google.com/go/vpcaccess v1.8.2 h1:nvrkqAjS2sorOu4YGCIXWz+Kk+5aAAdnaMD2tnsqeFg= cloud.google.com/go/vpcaccess v1.8.2/go.mod h1:4yvYKNjlNjvk/ffgZ0PuEhpzNJb8HybSM1otG2aDxnY= +cloud.google.com/go/webrisk v1.10.2 h1:X7zSwS1mX2bxoZ30Ozh6lqiSLezl7RMBWwp5a3Mkxp4= cloud.google.com/go/webrisk v1.10.2/go.mod h1:c0ODT2+CuKCYjaeHO7b0ni4CUrJ95ScP5UFl9061Qq8= +cloud.google.com/go/websecurityscanner v1.7.2 h1:8/4rfJXcyxozbfzI0lDFPcPShRE6bJ4HQwgDAG9J4oQ= cloud.google.com/go/websecurityscanner v1.7.2/go.mod h1:728wF9yz2VCErfBaACA5px2XSYHQgkK812NmHcUsDXA= +cloud.google.com/go/workflows v1.13.2 h1:jYIxrDOVCGvTBHIAVhqQ+P8fhE0trm+Hf2hgL1YzmK0= cloud.google.com/go/workflows v1.13.2/go.mod h1:l5Wj2Eibqba4BsADIRzPLaevLmIuYF2W+wfFBkRG3vU= +contrib.go.opencensus.io/exporter/stackdriver v0.13.4 h1:ksUxwH3OD5sxkjzEqGxNTl+Xjsmu3BnC/300MhSVTSc= +crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c h1:wvzox0eLO6CKQAMcOqz7oH3UFqMpMmK7kwmwV+22HIs= +crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= +gioui.org v0.0.0-20210308172011-57750fc8a0a6 h1:K72hopUosKG3ntOPNG4OzzbuhxGuVf06fa2la1/H/Ho= +git.sr.ht/~sbinet/gg v0.3.1 h1:LNhjNn8DerC8f9DHLz6lS0YYul/b602DUxDgGkd/Aik= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 h1:8q4SaHjFsClSvuVne0ID/5Ka8u3fcIHyqkLjcFpNRHQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.0.0 h1:lMW1lD/17LUA5z1XTURo7LcVG2ICBPlyMHjIUrcFZNQ= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0 h1:Ma67P/GGprNwsslzEH6+Kb8nybI8jpDTm4Wmzu2ReK8= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0 h1:gggzg0SUMs6SQbEw+3LoSsYf9YMjkupeAnHMX8O9mmY= +github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= +github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME= +github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= +github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20190129172621-c8b1d7a94ddf h1:8F6fjL5iQP6sArGtPuXh0l6hggdcIpAm4ChjVJE4oTs= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0 h1:oVLqHXhnYtUwM89y9T1fXGaK9wTkXHgNp8/ZNMQzUxE= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 h1:UQ0AhxogsIRZDkElkblfnwjc3IaltCm2HUMvezQaL7s= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1 h1:oTX4vsorBZo/Zdum6OKPA4o7544hm6smoRv1QjpTwGo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1/go.mod h1:0wEl7vrAD8mehJyohS9HZy+WyEOaQO2mJx86Cvh93kM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 h1:8nn+rsCvTq9axyEh382S0PFLBeaFwNsT43IrPWzctRU= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1/go.mod h1:viRWSEhtMZqz1rhwmOVKkWl6SwmVowfL9O2YR5gI2PE= +github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= +github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= +github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= +github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/RoaringBitmap/roaring v1.2.2 h1:RT+1qfb7a8rkOIxPnyJdvU4G8Ynmhc2YYP6MvzqEtwk= +github.com/RoaringBitmap/roaring v1.2.2/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE= +github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0= +github.com/Shopify/sarama v1.19.0 h1:9oksLxC6uxVPHPVYUmq6xhr1BOF/hHobWH2UzO67z1s= +github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= +github.com/VictoriaMetrics/metrics v1.23.1 h1:/j8DzeJBxSpL2qSIdqnRFLvQQhbJyJbbEi22yMm7oL0= +github.com/VictoriaMetrics/metrics v1.23.1/go.mod h1:rAr/llLpEnAdTehiNlUxKgnjcOuROSzpw0GvjpEbvFc= +github.com/Zilliqa/gozilliqa-sdk v1.2.1-0.20201201074141-dd0ecada1be6 h1:1d9pzdbkth4D9AX6ndKSl7of3UTV0RYl3z64U2dXMGo= +github.com/aclements/go-gg v0.0.0-20170118225347-6dbb4e4fefb0 h1:E5Dzlk3akC+T2Zj1LBHgfPK1y8YWgLDnNDRmG+tpSKw= +github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 h1:xlwdaKcTNVW4PtpQb8aKA4Pjy0CdJHEqvFbAnvR5m2g= +github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9 h1:7kQgkwGRoLzC9K0oyXdJo7nve/bynv/KwUsxbiTlzAM= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19 h1:iXUgAaqDcIUGbRoy2TdeofRG/j1zpGRSEmNK05T+bi8= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= +github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0= +github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0/go.mod h1:q37NoqncT41qKc048STsifIt69LfUJ8SrWWcz/yam5k= +github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8= +github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI= +github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/anacrolix/chansync v0.3.0 h1:lRu9tbeuw3wl+PhMu/r+JJCRu5ArFXIluOgdF0ao6/U= +github.com/anacrolix/chansync v0.3.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k= +github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 h1:8V0K09lrGoeT2KRJNOtspA7q+OMxGwQqK/Ug0IiaaRE= +github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444/go.mod h1:MctKM1HS5YYDb3F30NGJxLE+QPuqWoT5ReW/4jt8xew= +github.com/anacrolix/envpprof v1.2.1 h1:25TJe6t/i0AfzzldiGFKCpD+s+dk8lONBcacJZB2rdE= +github.com/anacrolix/envpprof v1.2.1/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4= +github.com/anacrolix/generics v0.0.0-20220618083756-f99e35403a60 h1:k4/h2B1gGF+PJGyGHxs8nmHHt1pzWXZWBj6jn4OBlRc= +github.com/anacrolix/generics v0.0.0-20220618083756-f99e35403a60/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8= +github.com/anacrolix/go-libutp v1.2.0 h1:sjxoB+/ARiKUR7IK/6wLWyADIBqGmu1fm0xo+8Yy7u0= +github.com/anacrolix/go-libutp v1.2.0/go.mod h1:RrJ3KcaDcf9Jqp33YL5V/5CBEc6xMc7aJL8wXfuWL50= +github.com/anacrolix/log v0.13.2-0.20221123232138-02e2764801c3 h1:qDcPnH18SanNZMeMuEjzKpB3NQGR1ahytV08KOhZhNo= +github.com/anacrolix/log v0.13.2-0.20221123232138-02e2764801c3/go.mod h1:MD4fn2pYcyhUAQg9SxoGOpTnV/VIdiKVYKZdCbDC97k= +github.com/anacrolix/missinggo v1.3.0 h1:06HlMsudotL7BAELRZs0yDZ4yVXsHXGi323QBjAVASw= +github.com/anacrolix/missinggo v1.3.0/go.mod h1:bqHm8cE8xr+15uVfMG3BFui/TxyB6//H5fwlq/TeqMc= +github.com/anacrolix/missinggo/perf v1.0.0 h1:7ZOGYziGEBytW49+KmYGTaNfnwUqP1HBsy6BqESAJVw= +github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ= +github.com/anacrolix/missinggo/v2 v2.7.0 h1:4fzOAAn/VCvfWGviLmh64MPMttrlYew81JdPO7nSHvI= +github.com/anacrolix/missinggo/v2 v2.7.0/go.mod h1:2IZIvmRTizALNYFYXsPR7ofXPzJgyBpKZ4kMqMEICkI= +github.com/anacrolix/mmsg v1.0.0 h1:btC7YLjOn29aTUAExJiVUhQOuf/8rhm+/nWCMAnL3Hg= +github.com/anacrolix/mmsg v1.0.0/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc= +github.com/anacrolix/multiless v0.3.0 h1:5Bu0DZncjE4e06b9r1Ap2tUY4Au0NToBP5RpuEngSis= +github.com/anacrolix/multiless v0.3.0/go.mod h1:TrCLEZfIDbMVfLoQt5tOoiBS/uq4y8+ojuEVVvTNPX4= +github.com/anacrolix/stm v0.4.0 h1:tOGvuFwaBjeu1u9X1eIh9TX8OEedEiEQ1se1FjhFnXY= +github.com/anacrolix/stm v0.4.0/go.mod h1:GCkwqWoAsP7RfLW+jw+Z0ovrt2OO7wRzcTtFYMYY5t8= +github.com/anacrolix/sync v0.4.0 h1:T+MdO/u87ir/ijWsTFsPYw5jVm0SMm4kVpg8t4KF38o= +github.com/anacrolix/sync v0.4.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g= +github.com/anacrolix/torrent v1.48.0 h1:OQe1aQb8WnhDzpcI7r3yWoHzHWKyPbfhXGfO9Q/pvbY= +github.com/anacrolix/torrent v1.48.0/go.mod h1:3UtkJ8BnxXDRwvk+eT+uwiZalfFJ8YzAhvxe4QRPSJI= +github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 h1:QAVZ3pN/J4/UziniAhJR2OZ9Ox5kOY2053tBbbqUPYA= +github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96/go.mod h1:Wa6n8cYIdaG35x15aH3Zy6d03f7P728QfdcDeD/IEOs= +github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4= +github.com/anacrolix/utp v0.1.0/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNaBJ2Dk= github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= +github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg= +github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI= +github.com/apache/arrow/go/v11 v11.0.0 h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM= +github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= +github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= +github.com/apache/thrift v0.16.0 h1:qEy6UW60iVOlUy+b9ZR0d5WzUWYGOo4HfopoyBaNmoY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a h1:pv34s756C4pEXnjgPfGYgdhg/ZdajGhyOvzx8k+23nw= +github.com/aws/aws-lambda-go v1.13.3 h1:SuCy7H3NLyp+1Mrfp+m80jcbi9KYWAs9/BXwppwRDzY= +github.com/aws/aws-sdk-go-v2 v1.21.2 h1:+LXZ0sgo8quN9UOKXXzAWRT3FWd4NxeXWOZom9pE7GA= +github.com/aws/aws-sdk-go-v2/config v1.18.45 h1:Aka9bI7n8ysuwPeFdm77nfbyHCAKQ3z9ghB3S/38zes= +github.com/aws/aws-sdk-go-v2/credentials v1.13.43 h1:LU8vo40zBlo3R7bAvBVy/ku4nxGEyZe9N8MqAeFTzF8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 h1:PIktER+hwIG286DqXyvVENjgLTAwGgoeriLDD5C+YlQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 h1:nFBQlGtkbPzp/NjZLuFxRqmT91rLJkgvsEQs68h962Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 h1:JRVhO25+r3ar2mKGP7E0LDl8K9/G36gjlqca5iQbaqc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 h1:hze8YsjSh8Wl1rYa1CJpRmXP21BvOBuc76YhW0HsuQ4= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1 h1:w/fPGB0t5rWwA43mux4e9ozFSH5zF1moQemlA131PWc= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 h1:WWZA/I2K4ptBS1kg0kV1JbBtG/umed0vwHRrmcr9z7k= +github.com/aws/aws-sdk-go-v2/service/route53 v1.30.2 h1:/RPQNjh1sDIezpXaFIkZb7MlXnSyAqjVdAwcJuGYTqg= +github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 h1:JuPGc7IkOP4AaqcZSIcyqLpFSqBWK32rM9+a1g6u73k= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 h1:HFiiRkf1SdaAmV3/BHOFZ9DjFynPHj8G/UIO1lQS+fk= +github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 h1:0BkLfgeDjfZnZ+MhB3ONb01u9pwFYTCZVhlsSSBvlbU= +github.com/aws/smithy-go v1.15.0 h1:PS/durmlzvAFpQHDs4wi4sNNP9ExsqZh6IlfdHXgKK8= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/bazelbuild/rules_go v0.49.0 h1:5vCbuvy8Q11g41lseGJDc5vxhDjJtfxr6nM/IC4VmqM= github.com/bazelbuild/rules_go v0.49.0/go.mod h1:Dhcz716Kqg1RHNWos+N6MlXNkjNP2EwZQ0LukRKJfMs= github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bketelsen/crypt v0.0.4 h1:w/jqZtC9YD4DS/Vp9GhWfWcCpuAL58oTnLoI8vE9YHU= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= +github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8= +github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og= +github.com/btcsuite/btcd/btcutil v1.1.0 h1:MO4klnGY+EWJdoWF12Wkuf4AWDBPMpZNeN/jRLrklUU= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= +github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk= +github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b h1:6+ZFm0flnudZzdSE0JxlhR2hKnGPcNB35BjQf4RYQDY= +github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= +github.com/casbin/casbin/v2 v2.37.0 h1:/poEwPSovi4bTOcP752/CsTQiRz2xycyVKFG7GUhbDw= +github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= +github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= +github.com/checkpoint-restore/go-criu/v5 v5.0.0 h1:TW8f/UvntYoVDMN1K2HlT82qH1rb0sOjpGw3m6Ym+i4= +github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764= +github.com/chzyer/logex v1.2.0 h1:+eqR0HfOetur4tgnC8ftU5imRnhi4te+BadWS95c5AM= +github.com/chzyer/readline v1.5.0 h1:lSwwFrbNviGePhkewF1az4oLmcwqCZijQ2/Wi3BGHAI= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23 h1:dZ0/VyGgQdVGAss6Ju0dt5P0QltE0SFY5Woh6hbIfiQ= +github.com/cilium/ebpf v0.6.2 h1:iHsfF/t4aW4heW2YKfeHrVPGdtYTL4C4KocpM8KTSnI= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyYMm1aztcyj/J5ckgJm2zwdDajFbx1NY= +github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= +github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= +github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec h1:EdRZT3IeKQmfCSrgo8SZ8V3MEnskuJP0wCYNpe+aiXo= +github.com/cloudflare/cloudflare-go v0.114.0 h1:ucoti4/7Exo0XQ+rzpn1H+IfVVe++zgiM+tyKtf0HUA= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= +github.com/consensys/bavard v0.1.31-0.20250406004941-2db259e4b582 h1:dTlIwEdFQmldzFf5F6bbTcYWhvnAgZai2g8eq3Wwxqg= github.com/consensys/bavard v0.1.31-0.20250406004941-2db259e4b582/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= +github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn9dE= +github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s= +github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= +github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a h1:W8b4lQ4tFF21aspRGoBuCNV6V2fFJBF+pm1J6OY8Lys= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= +github.com/crate-crypto/go-eth-kzg v1.3.0 h1:05GrhASN9kDAidaFJOda6A4BEvgvuXbazXg/0E3OOdI= +github.com/crate-crypto/go-eth-kzg v1.3.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/creachadair/command v0.0.0-20220426235536-a748effdf6a1 h1:r626P+s8TKpQaHwIaLmA2nGetfIVYEhtqcs3g2U1dC8= +github.com/creachadair/command v0.0.0-20220426235536-a748effdf6a1/go.mod h1:bAM+qFQb/KwWyCc9MLC4U1jvn3XyakqP5QRkds5T6cY= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= +github.com/cristalhq/acmd v0.8.1 h1:mtFp/cbeJNY5jokF9zPz5mRllGHropRrOkOVxeGS6FI= +github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c h1:/ovYnF02fwL0kvspmy9AuyKg1JhdTRUgPw4nUxd9oZM= +github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg= +github.com/decred/dcrd/lru v1.0.0 h1:Kbsb1SFDsIlaupWPwsPp+dkxiBY1frcS07PCPgotKz8= +github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= +github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954 h1:RMLoZVzv4GliuWafOuPuQDKSm1SJph7uCRnnS61JAn4= +github.com/djherbis/atime v1.1.0 h1:rgwVbP/5by8BvvjBNrbh64Qz33idKT3pSnMSJsxhi0g= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= +github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 h1:C7t6eeMaEQVy6e8CarIhscYQlNmw5e3G36y7l7Y21Ao= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d h1:W1n4DvpzZGOISgp7wWNtraLcHtnmnTwBlJidqtMIuwQ= +github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= +github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o= github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= +github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE= github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= +github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= +github.com/ethereum/c-kzg-4844/v2 v2.1.1 h1:KhzBVjmURsfr1+S3k/VE35T02+AW2qU9t9gr4R6YpSo= +github.com/ethereum/c-kzg-4844/v2 v2.1.1/go.mod h1:TC48kOKjJKPbN7C++qIgt0TJzZ70QznYR7Ob+WXl57E= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/ferranbt/fastssz v0.1.2 h1:Dky6dXlngF6Qjc+EfDipAkE83N5I5DE68bY6O0VLNPk= +github.com/fjl/gencodec v0.1.0 h1:B3K0xPfc52cw52BBgUbSPxYo+HlLfAgWMVKRWXUXBcs= +github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= +github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db h1:gb2Z18BhTPJPpLQWj4T+rfKHYCHxRHCtRxhKKjRidVw= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 h1:a9ENSRDFBUPkJ5lCgVZh26+ZbGyoVJG7yb5SSzF5H54= +github.com/fullstorydev/grpcurl v1.6.0 h1:p8BB6VZF8O7w6MxGr3KJ9E6EVKaswCevSALK6FBtMzA= +github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 h1:IZqZOB2fydHte3kUgxrzK5E1fW7RQGeDwE8F/ZZnUYc= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/getkin/kin-openapi v0.53.0 h1:7WzP+MZRRe7YQz2Kc74Ley3dukJmXDvifVbElGmQfoA= +github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9 h1:r5GgOLGbza2wVHRzK7aAj6lWZjfbAwiu/RDCVOKjRyM= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/go-chi/chi/v5 v5.0.0 h1:DBPx88FjZJH3FsICfDAfIfnb7XxKIYVGG6lOPlhENAg= +github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ= +github.com/go-fonts/latin-modern v0.2.0 h1:5/Tv1Ek/QCr20C6ZOz15vw3g7GELYL98KWr8Hgo+3vk= +github.com/go-fonts/liberation v0.2.0 h1:jAkAWJP4S+OsrPLZM4/eC9iW7CtHy+HBXrEwZXWo5VM= +github.com/go-fonts/stix v0.1.0 h1:UlZlgrvvmT/58o573ot7NFw0vZasZ5I6bcIft/oMdgg= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2YY30qV3gDcVBGtFgVsV3+/i+mKQ= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8= +github.com/go-redis/redis v6.15.8+incompatible h1:BKZuG6mCnRj5AOaWJXoCgf6rqTYnYJLe4en2hxT7r9o= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-zookeeper/zk v1.0.2 h1:4mx0EYENAdX/B/rbunjlt5+4RTA/a9SMHBRuSKdGxPM= +github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= +github.com/gogo/status v1.1.0 h1:+eIkrewn5q6b30y+g/BJINVVdi2xH7je5MPJ3ZPK3JA= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/gonum/blas v0.0.0-20181208220705-f22b278b28ac h1:Q0Jsdxl5jbxouNs1TQYt0gxesYMU4VXRbsTlgDloZ50= +github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82 h1:EvokxLQsaaQjcWVWSV38221VAK7qc2zhaO17bKys/18= +github.com/gonum/internal v0.0.0-20181124074243-f884aa714029 h1:8jtTdc+Nfj9AR+0soOeia9UZSvYBvETVHZrugUowJ7M= +github.com/gonum/lapack v0.0.0-20181123203213-e4cdc5a0bff9 h1:7qnwS9+oeSiOIsiUMajT+0R7HR6hw5NegnKPmn/94oI= +github.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9 h1:V2IgdyerlBa/MxaEFRbV5juy/C3MGdj4ePi+g6ePIp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/certificate-transparency-go v1.1.1 h1:6JHXZhXEvilMcTjR4MGZn5KV0IRkcFl4CJx5iHVhjFE= +github.com/google/go-pkcs11 v0.3.0 h1:PVRnTgtArZ3QQqTGtbtjtnIkzl2iY2kt24yqbrf7td8= +github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= +github.com/google/safehtml v0.0.2 h1:ZOt2VXg4x24bW0m2jtzAOkhoXV0iM8vNKc0paByCZqM= +github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= +github.com/google/trillian v1.3.11 h1:pPzJPkK06mvXId1LHEAJxIegGgHzzp/FUnycPYfoCMI= +github.com/googleapis/cloud-bigtable-clients-test v0.0.2 h1:S+sCHWAiAc+urcEnvg5JYJUOdlQEm/SEzQ/c/IdAH5M= +github.com/googleapis/cloud-bigtable-clients-test v0.0.2/go.mod h1:mk3CrkrouRgtnhID6UZQDK3DrFFa7cYCAJcEmNsHYrY= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/gax-go v0.0.0-20161107002406-da06d194a00e h1:CYRpN206UTHUinz3VJoLaBdy1gEGeJNsqT0mvswDcMw= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4= +github.com/gookit/color v1.5.1 h1:Vjg2VEcdHpwq+oY63s/ksHrgJYCTo0bwWvmmYWdE9fQ= github.com/gookit/color v1.5.1/go.mod h1:wZFzea4X8qN6vHOSP2apMb4/+w/orMznEzYsIHPaqKM= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75 h1:f0n1xnMSmBLzVfsMMvriDyA75NB/oBgILX2GcHXIQzY= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gostaticanalysis/analysisutil v0.4.1/go.mod h1:18U/DLpRgIUd459wGxVHE0fRgmo1UgHDcbw7F5idXu0= +github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/guptarohit/asciigraph v0.5.5 h1:ccFnUF8xYIOUPPY3tmdvRyHqmn1MYI9iv1pLKX+/ZkQ= github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/api v1.12.0 h1:k3y1FYv6nuKyNTqj6w9gXOx5r5CfLj/k/euUeBXj1OY= +github.com/hashicorp/consul/sdk v0.8.0 h1:OJtKBtEjboEZvG6AOUdh4Z1Zbyu0WcxQ0qatRrZHTVU= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= +github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= +github.com/hashicorp/go-retryablehttp v0.5.3 h1:QlWt0KvWT0lq8MFppF9tsJGF+ynG7ztc2KIPhzRGk7s= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= +github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= +github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/mdns v1.0.4 h1:sY0CMhFmjIPDMlTB+HfymFHCaYLhgifZ0QhjaYKD/UQ= github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.3.0 h1:8+567mCcFDnS5ADl7lrpxPMWiFCElyUEeW0gtj34fMA= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hashicorp/serf v0.9.7 h1:hkdgbqizGQHuU5IPqYM1JdSMV8nKfpuOnZYXssk9muY= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/hudl/fargo v1.4.0 h1:ZDDILMbB37UlAVLlWcJ2Iz1XuahZZTDZfdCKeclfq2s= +github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= +github.com/hydrogen18/memlistener v1.0.0 h1:JR7eDj8HD6eXrc5fWLbSUnfcQFL06PYvCc0DKQnWfaU= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2 h1:rcanfLhLDA8nozr/K289V1zcntHr3V+SHlXwzz1ZI2g= +github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= +github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= +github.com/iris-contrib/go.uuid v2.0.0+incompatible h1:XZubAYg61/JwnJNbZilGjf3b3pB80+OQg2qf6c8BfWE= +github.com/iris-contrib/httpexpect/v2 v2.12.1 h1:3cTZSyBBen/kfjCtgNFoUKi1u0FVXNaAjyRJOo6AVS4= +github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= +github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 h1:TMtDYDHKYY15rFihtRfck/bfFqNfvcabqvXAFQfAUpY= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +github.com/jhump/gopoet v0.1.0 h1:gYjOPnzHd2nzB37xYQZxj4EIQNpBrBskRqQQ3q4ZgSg= +github.com/jhump/goprotoc v0.5.0 h1:Y1UgUX+txUznfqcGdDef8ZOVlyQvnV0pKWZH08RmZuo= +github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= +github.com/jonboulle/clockwork v0.2.0 h1:J2SLSdy7HgElq8ekSl2Mxh6vrRNFxqbXGenYH2I02Vs= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/txtarfs v0.0.0-20210218200122-0702f000015a h1:8NZHLa6Gp0hW6xJ0c3F1Kse7dJw30fOcDzHuF9sLbnE= github.com/josharian/txtarfs v0.0.0-20210218200122-0702f000015a/go.mod h1:izVPOvVRsHiKkeGCT6tYBNWyDVuzj9wAaBb5R9qamfw= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5 h1:PJr+ZMXIecYc1Ey2zucXdR73SMBtgjPgwa31099IMv0= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= +github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52 h1:msKODTL1m0wigztaqILOtla9HeW1ciscYG4xjLtvk5I= +github.com/kataras/blocks v0.0.7 h1:cF3RDY/vxnSRezc7vLFlQFTYXG/yAr1o7WImJuZbzC4= +github.com/kataras/golog v0.1.8 h1:isP8th4PJH2SrbkciKnylaND9xoTtfxv++NB+DF0l9g= +github.com/kataras/iris/v12 v12.2.0 h1:WzDY5nGuW/LgVaFS5BtTkW3crdSKJ/FEgWnxPnIVVLI= +github.com/kataras/jwt v0.1.8 h1:u71baOsYD22HWeSOg32tCHbczPjdCk7V4MMeJqTtmGk= +github.com/kataras/neffos v0.0.21 h1:UwN/F44jlqdtgFI29y3VhA7IlJ4JbK3UjCbTDg1pYoo= +github.com/kataras/pio v0.0.11 h1:kqreJ5KOEXGMwHAWHDwIl+mjfNCPhAwZPa8gK7MKlyw= +github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= +github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kilic/bls12-381 v0.1.0 h1:encrdjqKMEvabVQ7qYOKu1OvhqpK4s47wDYtNiPtlp4= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfMuZT83xIwfPDxEI2OHu6xUmJMFE= +github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +github.com/labstack/echo/v4 v4.10.0 h1:5CiyngihEO4HXsz3vVsJn7f8xAlWwRr3aY6Ih280ZKA= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= +github.com/ledgerwatch/interfaces v0.0.0-20230210062155-539b8171d9f0 h1:AbXB1b7L84dXxc0em6As+KkjbjEdIebsF07WVLemEXI= +github.com/ledgerwatch/interfaces v0.0.0-20230210062155-539b8171d9f0/go.mod h1:ugQv1QllJzBny3cKZKxUrSnykkjkBgm27eQM6dnGAcc= +github.com/ledgerwatch/log/v3 v3.7.0 h1:aFPEZdwZx4jzA3+/Pf8wNDN5tCI0cIolq/kfvgcM+og= +github.com/ledgerwatch/log/v3 v3.7.0/go.mod h1:J2Jl6zV/58LeA6LTaVVnCGyf1/cYYSEOOLHY4ZN8S2A= +github.com/ledgerwatch/secp256k1 v1.0.0 h1:Usvz87YoTG0uePIV8woOof5cQnLXGYa162rFf3YnwaQ= +github.com/ledgerwatch/secp256k1 v1.0.0/go.mod h1:SPmqJFciiF/Q0mPt2jVs2dTr/1TZBTIA+kPMmKgBAak= +github.com/ledgerwatch/trackerslist v1.0.0 h1:6gnQu93WCTL4jPcdmc8UEmw56Cb8IFQHLGnevfIeLwo= +github.com/ledgerwatch/trackerslist v1.0.0/go.mod h1:pCC+eEw8izNcnBBiSwvIq8kKsxDLInAafSW275jqFrg= +github.com/letsencrypt/pkcs11key/v4 v4.0.0 h1:qLc/OznH7xMr5ARJgkZCCWk+EomQkiNTOoOF5LAgagc= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 h1:143Bb8f8DuGWck/xpNUOckBVYfFbBTnLevfRZ1aVVqo= +github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk= +github.com/lispad/go-generics-tools v1.1.0 h1:mbSgcxdFVmpoyso1X/MJHXbSbSL3dD+qhRryyxk+/XY= +github.com/lispad/go-generics-tools v1.1.0/go.mod h1:2csd1EJljo/gy5qG4khXol7ivCPptNjG5Uv2X8MgK84= +github.com/lucasjones/reggen v0.0.0-20180717132126-cdb49ff09d77 h1:6xiz3+ZczT3M4+I+JLpcPGG1bQKm8067HktB17EDWEE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= +github.com/lyft/protoc-gen-star v0.6.1 h1:erE0rdztuaDq3bpGifD95wfoPrSZc95nGA6tbiNYh6M= +github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4 h1:sIXJOMrYnQZJu7OB7ANSF4MYri2fTEGIsRLz6LwI4xE= github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= +github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= +github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/matryer/moq v0.3.0 h1:4j0goF/XK3pMTc7fJB3fveuTJoQNdavRX/78vlK3Xb4= +github.com/matryer/moq v0.3.0/go.mod h1:RJ75ZZZD71hejp39j4crZLsEDszGk6iH4v4YsWFKH4s= +github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2 h1:JAEbJn3j/FrhdWA9jW8B5ajsLIjeuEHLi8xE4fk997o= +github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/mediocregopher/radix/v3 v3.8.1 h1:rOkHflVuulFKlwsLY01/M2cM2tWCjDoETcMqKbAWu1M= +github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 h1:zpIH83+oKzcpryru8ceC6BxnoG8TBrhgAvRg8obzup0= +github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY= +github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= +github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= +github.com/miekg/pkcs11 v1.0.3 h1:iMwmD7I5225wv84WxIG/bmxz9AXjWvTWIbM/TYHvWtw= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= +github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/mitchellh/cli v1.1.0 h1:tEElEatulEHDeedTxwckzyYMA5c86fbmNIUL1hBIiTg= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= +github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= +github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= +github.com/mmcloughlin/profile v0.1.1 h1:jhDmAqPyebOsVDOCICJoINoLb/AnLBaUw58nFzxWS2w= +github.com/moby/sys/mountinfo v0.4.1 h1:1O+1cHA1aujwEwwVMa2Xm2l+gIpUHyd3+D+d7LZh1kM= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= +github.com/mozilla/scribe v0.0.0-20180711195314-fb71baf557c1 h1:29NKShH4TWd3lxCDUhS4Xe16EWMA753dtIxYtwddklU= +github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5 h1:0KqC6/sLy7fDpBdybhVkkv4Yz+PmB7c9Dz9z3dLW804= +github.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6Z4= +github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= +github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= +github.com/mwitkow/go-proto-validators v0.2.0 h1:F6LFfmgVnfULfaRsQWBbe7F7ocuHCr9+7m+GAeDzNbQ= +github.com/mwitkow/grpc-proxy v0.0.0-20181017164139-0f1106ef9c76 h1:0xuRacu/Zr+jX+KyLLPPktbwXqyOvnOPUQmMLzX1jxU= +github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks= +github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 h1:shk/vn9oCoOTmwcouEdwIeOtOGA/ELRUw/GwvxwfT+0= +github.com/nats-io/jwt v0.3.2 h1:+RB5hMpXUUA2dfxuhBTEkMOrYmM+gKIZYS1KjSostMI= +github.com/nats-io/jwt/v2 v2.3.0 h1:z2mA1a7tIf5ShggOFlR1oBPgd6hGqcDYsISxZByUzdI= +github.com/nats-io/nats-server/v2 v2.9.11 h1:4y5SwWvWI59V5mcqtuoqKq6L9NDUydOP3Ekwuwl8cZI= +github.com/nats-io/nats.go v1.23.0 h1:lR28r7IX44WjYgdiKz9GmUeW0uh/m33uD3yEjLZ2cOE= +github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 h1:D6paGObi5Wud7xg83MaEFyjxQB1W5bz5d0IFppr+ymk= +github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c h1:bY6ktFuJkt+ZXkX0RChQch2FtHpWQLVS8Qo1YasiIVk= +github.com/neilotoole/errgroup v0.1.5 h1:DxEGoIfFm5ooGicidR+okiHjoOaGRKFaSxDPVZuuu2I= +github.com/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc= +github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68= github.com/onsi/gomega v1.20.0/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 h1:3snG66yBm59tKhhSPQrQ/0bCrv1LQbKt40LnUPiUxdc= +github.com/opencontainers/selinux v1.8.2 h1:c4ca10UMgRcvZ6h0K4HtS15UaVSBEaE+iln2LVpAuGc= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 h1:lM6RxxfUMrYL/f8bWEUqdXrANWtrL7Nndbm9iFN0DlU= +github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 h1:ZCnq+JUrvXcDVhX/xRolRBZifmabN1HcS1wrPSvxhrU= +github.com/openzipkin/zipkin-go v0.2.5 h1:UwtQQx2pyPIgWYHRg+epgdx1/HnBQTgN3/oIYEJTQzU= +github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE= +github.com/otiai10/curr v1.0.0 h1:TJIWdbX0B+kpNagQrjgq8bCMrbhiuX73M2XwgtDMoOI= +github.com/otiai10/mint v1.3.1 h1:BCmzIS3n71sGfHB5NMNDB3lHYPz8fWSkCAErHed//qc= +github.com/pact-foundation/pact-go v1.0.4 h1:OYkFijGHoZAYbOIb1LWXrwKQbMMRUv1oQ89blD2Mh2Q= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= +github.com/performancecopilot/speed v3.0.0+incompatible h1:2WnRzIquHa5QxaJKShDkLM+sc0JPuwhXzK8OYOyt3Vg= +github.com/performancecopilot/speed/v4 v4.0.0 h1:VxEDCmdkfbQYDlcr/GC9YoN9PQ6p8ulk9xVsepYy9ZY= +github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= +github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ= +github.com/phpdave11/gofpdi v1.0.13 h1:o61duiW8M9sMlkVXWlvP92sZJtGKENvW3VExs6dZukQ= +github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= +github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= +github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E= +github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ= +github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig= +github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE= +github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs= +github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8= +github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw= +github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U= +github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo= +github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA= +github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= +github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA= +github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s= +github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU= +github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= +github.com/pion/srtp/v2 v2.0.9 h1:JJq3jClmDFBPX/F5roEb0U19jSU7eUhyDqR/NZ34EKQ= +github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4= github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= +github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA= +github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg= +github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw= +github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw= +github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= +github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= +github.com/pion/webrtc/v3 v3.1.42 h1:wJEQFIXVanptnQcHOLTuIo4AtGB2+mG2x4OhIhnITOA= +github.com/pion/webrtc/v3 v3.1.42/go.mod h1:ffD9DulDrPxyWvDPUIPAOSAWx9GUlOExiJPf7cCcMLA= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= +github.com/pkg/sftp v1.13.7 h1:uv+I3nNJvlKZIQGSr8JVQLNHFU9YhhNpvC14Y6KgmSM= github.com/pkg/sftp v1.13.7/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA= +github.com/protolambda/bls12-381-util v0.1.0 h1:05DU2wJN7DTU7z28+Q+zejXkIsA/MF8JZQGhtBZZiWk= +github.com/protolambda/messagediff v1.4.0 h1:fk6gxK7WybJCaeOFK1yuh2Ldplx7qYMLibiMwWFcSZY= +github.com/protolambda/zrnt v0.34.1 h1:qW55rnhZJDnOb3TwFiFRJZi3yTXFrJdGOFQM7vCwYGg= +github.com/protolambda/ztyp v0.2.2 h1:rVcL3vBu9W/aV646zF6caLS/dyn9BN8NYiuJzicLNyY= +github.com/prysmaticlabs/gohashtree v0.0.1-alpha.0.20220714111606-acbb2962fb48 h1:cSo6/vk8YpvkLbk9v3FO97cakNmUoxwi2KMP8hd5WIw= +github.com/pseudomuto/protoc-gen-doc v1.3.2 h1:61vWZuxYa8D7Rn4h+2dgoTNqnluBmJya2MgbqO32z6g= +github.com/pseudomuto/protokit v0.2.0 h1:hlnBDcy3YEDXH7kc9gV+NLaN0cDzhDvD1s7Y6FZ8RpM= +github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= +github.com/quasilyte/go-ruleguard/rules v0.0.0-20211022131956-028d6511ab71 h1:CNooiryw5aisadVfzneSZPswRWvnVW8hF1bS/vo8ReI= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/go-dbus v0.0.0-20121104212943-b7232d34b1d5 h1:CvqZS4QYHBRvx7AeFdimd16HCbLlYsvQMcKDACpJW/c= +github.com/remyoudompheng/go-dbus v0.0.0-20121104212943-b7232d34b1d5/go.mod h1:+u151txRmLpwxBmpYn9z3d1sdJdjRPQpsXuYeY9jNls= +github.com/remyoudompheng/go-liblzma v0.0.0-20190506200333-81bf2d431b96 h1:J8J/cgLDRuqXJnwIrRDBvtl+LLsdg7De74znW/BRRq4= +github.com/remyoudompheng/go-liblzma v0.0.0-20190506200333-81bf2d431b96/go.mod h1:90HvCY7+oHHUKkbeMCiHt1WuFR2/hPJ9QrljDG+v6ls= +github.com/remyoudompheng/go-misc v0.0.0-20190427085024-2d6ac652a50e h1:eTWZyPUnHcuGRDiryS/l2I7FfKjbU3IBx3IjqHPxuKU= +github.com/remyoudompheng/go-misc v0.0.0-20190427085024-2d6ac652a50e/go.mod h1:80FQABjoFzZ2M5uEa6FUaJYEmqU2UOKojlFVak1UAwI= +github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs= +github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f h1:UFr9zpz4xgTnIE5yIMtWAMngCdZ9p/+q6lTbgelo80M= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= +github.com/sagikazarmark/crypt v0.6.0 h1:REOEXCs/NFY/1jOCEouMuT4zEniE5YoXbvpC5X/TLF8= +github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU= +github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= +github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/seccomp/libseccomp-golang v0.9.1 h1:NJjM5DNFOs0s3kYE1WUOr6G8V97sdt46rlXTMfXGWBo= +github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= github.com/sei-protocol/go-ethereum v1.15.7-sei-12 h1:3Wj5nU7X0+mKcDho6mwf0leVytQWmNEq6xFv9Wr+HOs= github.com/sei-protocol/go-ethereum v1.15.7-sei-12/go.mod h1:+S9k+jFzlyVTNcYGvqFhzN/SFhI6vA+aOY4T5tLSPL0= -github.com/sei-protocol/go-ethereum v1.15.7-sei-14 h1:yJ272fLard1CqL3YvDEB3MFpp9zJ37j3ky4ojqq7NL0= -github.com/sei-protocol/go-ethereum v1.15.7-sei-14/go.mod h1:+S9k+jFzlyVTNcYGvqFhzN/SFhI6vA+aOY4T5tLSPL0= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/shirou/gopsutil/v3 v3.22.9/go.mod h1:bBYl1kjgEJpWpxeHmLI+dVHWtyAwfcmSBLDsp2TNT8A= +github.com/shirou/gopsutil/v3 v3.23.2 h1:PAWSuiAszn7IhPMBtXsbSCafej7PqUOvY6YywlQUExU= +github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 h1:aSISeOcal5irEhJd1M+IrApc0PdcN7e7Aj4yuEnOrfQ= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc= +github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 h1:pXY9qYc/MP5zdvqWEUH6SjNiu7VhSjuVFTFiTcphaLU= +github.com/sirkon/gitlab v0.0.5 h1:FXi1K3yE8x/3Bc9/AXmWyCRcWGwfED6hEXEV2XrDNl8= +github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= +github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ= github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= +github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= +github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= +github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e h1:mOtuXaRAbVZsxAHVdPR3IjfmN8T1h2iczJLynhLybf8= +github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= +github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 h1:34icjjmqJ2HPjrSuJYEkdZ+0ItmGQAQ75cRHIiftIyE= +github.com/tdewolff/minify/v2 v2.12.4 h1:kejsHQMM17n6/gwdw53qsi6lg0TGddZADVyQOz1KMdE= +github.com/tdewolff/parse/v2 v2.6.4 h1:KCkDvNUMof10e3QExio9OPZJT8SbdKojLBumw8YZycQ= +github.com/tdewolff/test v1.0.7 h1:8Vs0142DmPFW/bQeHRP3MV19m1gvndjUb1sn8yy74LM= +github.com/tidwall/sjson v1.1.4 h1:bTSsPLdAYF5QNLSwYsKfBKKTnlGbIuhqL3CpRsjzGhg= github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= +github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966 h1:j6JEOq5QWFker+d7mFQYOhjTZonQ7YkLTHm56dbn+yM= +github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc= +github.com/torquem-ch/mdbx-go v0.27.5 h1:bbhXQGFCmoxbRDXKYEJwxSOOTeBKwoD4pFBUpK9+V1g= +github.com/torquem-ch/mdbx-go v0.27.5/go.mod h1:T2fsoJDVppxfAPTLd1svUgH1kpPmeXdPESmroSHcL1E= +github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ= +github.com/tyler-smith/go-bip39 v1.0.2 h1:+t3w+KwLXO6154GNJY+qUtIxLTmFjfUmpguQT1OlOT8= github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY= +github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= +github.com/valyala/fasthttp v1.40.0 h1:CRq/00MfruPGFLTQKY8b+8SfdK60TxNztjRMnH0t1Yc= +github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= +github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= +github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= +github.com/valyala/quicktemplate v1.7.0 h1:LUPTJmlVcb46OOUY3IeD9DojFpAVbsG+5WFTcjMJzCM= github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8 h1:EVObHAr8DqpoJCVv6KYTle8FEImKhtkfcZetNqxDoJQ= +github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= +github.com/ybbus/jsonrpc v2.1.2+incompatible h1:V4mkE9qhbDQ92/MLMIhlhMSbz8jNXdagC3xBR5NDwaQ= +github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zenazn/goji v0.9.0 h1:RSQQAbXGArQ0dIDEq+PI6WqN6if+5KHu6x2Cx/GXLTQ= +go.einride.tech/aip v0.68.0 h1:4seM66oLzTpz50u4K1zlJyOXQ3tCzcJN7I22tKkjipw= +go.einride.tech/aip v0.68.0/go.mod h1:7y9FF8VtPWqpxuAxl0KQWqaULxW4zFIesD6zF5RIHHg= +go.etcd.io/etcd v0.0.0-20200513171258-e048e166ab9c h1:/RwRVN9EdXAVtdHxP7Ndn/tfmM9/goiwU0QTnLBgS4w= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.4 h1:lrneYvz923dvC14R54XcA7FXoZ3mlGZAgmwhfm7HqOg= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= +go.etcd.io/etcd/client/v2 v2.305.4 h1:Dcx3/MYyfKcPNLpR4VVQUP5KgYrBeJtktBwEKkw08Ao= +go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= +go.etcd.io/etcd/client/v3 v3.5.4 h1:p83BUL3tAYS0OT/r0qglgc3M1JjhM0diV8DSWAhVXv4= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= +go.etcd.io/gofail v0.1.0 h1:XItAMIhOojXFQMgrxjnd2EIIHun/d5qL0Pf7FzVTkFg= +go.mozilla.org/mozlog v0.0.0-20170222151521-4bb13139d403 h1:rKyWXYDfrVOpMFBion4Pmx5sJbQreQNXycHvm4KwJSg= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/detectors/gcp v1.29.0 h1:TiaiXB4DpGD3sdzNlYQxruQngn5Apwzi1X0DRhuGvDQ= go.opentelemetry.io/contrib/detectors/gcp v1.29.0/go.mod h1:GW2aWZNwR2ZxDLdv8OyC2G8zkRoQBuURgV7RPQgcPoU= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/exp/typeparams v0.0.0-20220613132600-b0d781184e0d/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -219,7 +927,9 @@ golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/perf v0.0.0-20230113213139-801c7ef9e5c5 h1:ObuXPmIgI4ZMyQLIz48cJYgSyWdjUXc2SZAdyJMwEAU= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -227,6 +937,7 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= @@ -235,22 +946,61 @@ golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f/go.mod h1:SgwaegtQh8clI golang.org/x/tools v0.1.12-0.20220628192153-7743d1d949f1/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= +gonum.org/v1/plot v0.10.1 h1:dnifSs43YJuNMDzB7v8wV64O4ABBHReuAVAoBxqBqS4= google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= +google.golang.org/api v0.215.0 h1:jdYF4qnyczlEz2ReWIsosNLDuzXyvFHJtI5gcr0J7t0= google.golang.org/api v0.215.0/go.mod h1:fta3CVtuJYOEdugLNWm6WodzOS8KdFckABwN4I40hzY= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f/go.mod h1:Yo94eF2nj7igQt+TiJ49KxjIH8ndLYPZMIRSiRcEbg0= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20241223144023-3abc09e42ca8 h1:qlXhWiX84AGgaN7LuORWBEQCCTqj3szNbh2am45O3W8= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:bLYPejkLzwgJuAHlIk1gdPOlx9CUYXLZi2rZxL/ursM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +modernc.org/cc/v3 v3.36.3 h1:uISP3F66UlixxWEcKuIWERa4TwrZENHSL8tWxZz8bHg= +modernc.org/ccgo/v3 v3.16.9 h1:AXquSwg7GuMk11pIdw7fmO1Y/ybgazVkMhsZWCV0mHM= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/libc v1.17.1 h1:Q8/Cpi36V/QBfuQaFVeisEBs3WqoGAJprZzmf7TfEYI= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/memory v1.2.1 h1:dkRh86wgmq/bJu2cAS2oqBCz/KsMZU7TUM4CibQ7eBs= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/sqlite v1.18.1 h1:ko32eKt3jf7eqIkCgPAeHMBXw3riNSLhl2f3loEF7o8= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao= +modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= +modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM= +moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= +rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= +rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0 h1:ucqkfpjg9WzSUubAO62csmucvxl4/JeW3F4I4909XkM= diff --git a/sei-db/db_engine/pebbledb/mvcc/db.go b/sei-db/db_engine/pebbledb/mvcc/db.go index 6cacf910e6..5f0df086f2 100644 --- a/sei-db/db_engine/pebbledb/mvcc/db.go +++ b/sei-db/db_engine/pebbledb/mvcc/db.go @@ -25,7 +25,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-db/config" "github.com/sei-protocol/sei-chain/sei-db/proto" "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/types" - "github.com/sei-protocol/sei-chain/sei-db/state_db/ss/util" "github.com/sei-protocol/sei-chain/sei-db/wal" ) From 38f5dc5b9579705e58f360e860d03500cc101e0c Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Tue, 13 Jan 2026 02:40:33 -0800 Subject: [PATCH 21/35] Fix go lint and remove unneeded code --- sei-db/state_db/sc/memiavl/db.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/sei-db/state_db/sc/memiavl/db.go b/sei-db/state_db/sc/memiavl/db.go index 177cad0076..cdc97b4e3f 100644 --- a/sei-db/state_db/sc/memiavl/db.go +++ b/sei-db/state_db/sc/memiavl/db.go @@ -207,15 +207,6 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * // Even in read-only mode we may need WAL replay to reconstruct non-snapshot versions. var walHandler wal.GenericWAL[proto.ChangelogEntry] changelogDir := utils.GetChangelogPath(opts.Dir) - if st, err := os.Stat(changelogDir); err == nil && !st.IsDir() { - return nil, fmt.Errorf("changelog path exists but is not a directory: %s", changelogDir) - } else if err != nil && os.IsNotExist(err) { - // Create empty WAL directory if missing. This preserves the invariant that db.GetWAL() is non-nil. - // In read-only mode this is still safe: we won't write entries unless Commit() is called (which is disallowed). - if err := os.MkdirAll(changelogDir, 0o755); err != nil { - return nil, fmt.Errorf("failed to create changelog directory: %w", err) - } - } writeBuf := 0 if !opts.ReadOnly { From d6005aef4ae4e1febff115a37347eea90817158d Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Tue, 13 Jan 2026 15:11:15 -0800 Subject: [PATCH 22/35] Add changelog back and upgrade tidywal version --- go.mod | 2 +- go.sum | 2 + sei-db/db_engine/pebbledb/mvcc/db.go | 24 ++--- sei-db/db_engine/rocksdb/mvcc/db.go | 24 ++--- sei-db/state_db/sc/memiavl/db.go | 20 ++--- sei-db/state_db/ss/store.go | 12 +-- .../cmd/seidb/operations/replay_changelog.go | 12 +-- sei-db/wal/changelog.go | 25 ++++++ sei-db/wal/processor.go | 87 ------------------- 9 files changed, 49 insertions(+), 159 deletions(-) create mode 100644 sei-db/wal/changelog.go delete mode 100644 sei-db/wal/processor.go diff --git a/go.mod b/go.mod index 6c1a950803..b9cee513b6 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,7 @@ require ( github.com/tendermint/tm-db v0.6.8-0.20220519162814-e24b96538a12 github.com/tidwall/btree v1.6.0 github.com/tidwall/gjson v1.10.2 - github.com/tidwall/wal v1.1.7 + github.com/tidwall/wal v1.2.1 github.com/zbiljic/go-filelock v0.0.0-20170914061330-1dbf7103ab7d github.com/zeebo/blake3 v0.2.4 go.opentelemetry.io/otel v1.38.0 diff --git a/go.sum b/go.sum index b73c7cda7f..cf11ba8a77 100644 --- a/go.sum +++ b/go.sum @@ -2085,6 +2085,8 @@ github.com/tidwall/tinylru v1.1.0 h1:XY6IUfzVTU9rpwdhKUF6nQdChgCdGjkMfLzbWyiau6I github.com/tidwall/tinylru v1.1.0/go.mod h1:3+bX+TJ2baOLMWTnlyNWHh4QMnFyARg2TLTQ6OFbzw8= github.com/tidwall/wal v1.1.7 h1:emc1TRjIVsdKKSnpwGBAcsAGg0767SvUk8+ygx7Bb+4= github.com/tidwall/wal v1.1.7/go.mod h1:r6lR1j27W9EPalgHiB7zLJDYu3mzW5BQP5KrzBpYY/E= +github.com/tidwall/wal v1.2.1 h1:xQvwnRF3e+xBC4NvFvl1mPGJHU0aH5zNzlUKnKGIImA= +github.com/tidwall/wal v1.2.1/go.mod h1:r6lR1j27W9EPalgHiB7zLJDYu3mzW5BQP5KrzBpYY/E= github.com/timakin/bodyclose v0.0.0-20210704033933-f49887972144 h1:kl4KhGNsJIbDHS9/4U9yQo1UcPQM0kOMJHn29EoH/Ro= github.com/timakin/bodyclose v0.0.0-20210704033933-f49887972144/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= github.com/timonwong/loggercheck v0.9.3 h1:ecACo9fNiHxX4/Bc02rW2+kaJIAMAes7qJ7JKxt0EZI= diff --git a/sei-db/db_engine/pebbledb/mvcc/db.go b/sei-db/db_engine/pebbledb/mvcc/db.go index 5f0df086f2..af572debf7 100644 --- a/sei-db/db_engine/pebbledb/mvcc/db.go +++ b/sei-db/db_engine/pebbledb/mvcc/db.go @@ -66,7 +66,7 @@ type Database struct { storeKeyDirty sync.Map // Changelog used to support async write - streamHandler *wal.WAL[proto.ChangelogEntry] + streamHandler wal.ChangelogWAL // Pending changes to be written to the DB pendingChanges chan VersionedChangesets @@ -151,22 +151,12 @@ func OpenDB(dataDir string, config config.StateStoreConfig) (*Database, error) { _ = db.Close() return nil, errors.New("KeepRecent must be non-negative") } - streamHandler, err := wal.NewWAL( - func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, - func(data []byte) (proto.ChangelogEntry, error) { - var e proto.ChangelogEntry - err := e.Unmarshal(data) - return e, err - }, - logger.NewNopLogger(), - utils.GetChangelogPath(dataDir), - wal.Config{ - DisableFsync: true, - ZeroCopy: true, - KeepRecent: uint64(config.KeepRecent), - PruneInterval: time.Duration(config.PruneIntervalSeconds) * time.Second, - }, - ) + streamHandler, err := wal.NewChangelogWAL(logger.NewNopLogger(), utils.GetChangelogPath(dataDir), wal.Config{ + DisableFsync: true, + ZeroCopy: true, + KeepRecent: uint64(config.KeepRecent), + PruneInterval: time.Duration(config.PruneIntervalSeconds) * time.Second, + }) if err != nil { panic(err) } diff --git a/sei-db/db_engine/rocksdb/mvcc/db.go b/sei-db/db_engine/rocksdb/mvcc/db.go index 62ffd3c9cf..e3a1fd8227 100644 --- a/sei-db/db_engine/rocksdb/mvcc/db.go +++ b/sei-db/db_engine/rocksdb/mvcc/db.go @@ -66,7 +66,7 @@ type Database struct { asyncWriteWG sync.WaitGroup // Changelog used to support async write - streamHandler *wal.WAL[proto.ChangelogEntry] + streamHandler wal.ChangelogWAL // Pending changes to be written to the DB pendingChanges chan VersionedChangesets @@ -113,22 +113,12 @@ func OpenDB(dataDir string, config config.StateStoreConfig) (*Database, error) { } database.latestVersion.Store(latestVersion) - streamHandler, err := wal.NewWAL( - func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, - func(data []byte) (proto.ChangelogEntry, error) { - var e proto.ChangelogEntry - err := e.Unmarshal(data) - return e, err - }, - logger.NewNopLogger(), - utils.GetChangelogPath(dataDir), - wal.Config{ - DisableFsync: true, - ZeroCopy: true, - KeepRecent: uint64(config.KeepRecent), - PruneInterval: time.Duration(config.PruneIntervalSeconds) * time.Second, - }, - ) + streamHandler, err := wal.NewChangelogWAL(logger.NewNopLogger(), utils.GetChangelogPath(dataDir), wal.Config{ + DisableFsync: true, + ZeroCopy: true, + KeepRecent: uint64(config.KeepRecent), + PruneInterval: time.Duration(config.PruneIntervalSeconds) * time.Second, + }) if err != nil { panic(err) } diff --git a/sei-db/state_db/sc/memiavl/db.go b/sei-db/state_db/sc/memiavl/db.go index cdc97b4e3f..648912b182 100644 --- a/sei-db/state_db/sc/memiavl/db.go +++ b/sei-db/state_db/sc/memiavl/db.go @@ -212,21 +212,11 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * if !opts.ReadOnly { writeBuf = opts.AsyncCommitBuffer } - w, err := wal.NewWAL( - func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, - func(data []byte) (proto.ChangelogEntry, error) { - var e proto.ChangelogEntry - err := e.Unmarshal(data) - return e, err - }, - logger, - changelogDir, - wal.Config{ - DisableFsync: true, - ZeroCopy: true, - WriteBufferSize: writeBuf, - }, - ) + w, err := wal.NewChangelogWAL(logger, changelogDir, wal.Config{ + DisableFsync: true, + ZeroCopy: true, + WriteBufferSize: writeBuf, + }) if err != nil { return nil, fmt.Errorf("failed to open changelog WAL: %w", err) } diff --git a/sei-db/state_db/ss/store.go b/sei-db/state_db/ss/store.go index d65523b1d1..83d3f7eda1 100644 --- a/sei-db/state_db/ss/store.go +++ b/sei-db/state_db/ss/store.go @@ -61,17 +61,7 @@ func NewStateStore(logger logger.Logger, homeDir string, ssConfig config.StateSt func RecoverStateStore(logger logger.Logger, changelogPath string, stateStore types.StateStore) error { ssLatestVersion := stateStore.GetLatestVersion() logger.Info(fmt.Sprintf("Recovering from changelog %s with latest SS version %d", changelogPath, ssLatestVersion)) - streamHandler, err := wal.NewWAL( - func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, - func(data []byte) (proto.ChangelogEntry, error) { - var e proto.ChangelogEntry - err := e.Unmarshal(data) - return e, err - }, - logger, - changelogPath, - wal.Config{}, - ) + streamHandler, err := wal.NewChangelogWAL(logger, changelogPath, wal.Config{}) if err != nil { return err } diff --git a/sei-db/tools/cmd/seidb/operations/replay_changelog.go b/sei-db/tools/cmd/seidb/operations/replay_changelog.go index 5ba6c02c8d..d9f4526c91 100644 --- a/sei-db/tools/cmd/seidb/operations/replay_changelog.go +++ b/sei-db/tools/cmd/seidb/operations/replay_changelog.go @@ -42,17 +42,7 @@ func executeReplayChangelog(cmd *cobra.Command, _ []string) { } logDir := filepath.Join(dbDir, "changelog") - stream, err := wal.NewWAL( - func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, - func(data []byte) (proto.ChangelogEntry, error) { - var e proto.ChangelogEntry - err := e.Unmarshal(data) - return e, err - }, - logger.NewNopLogger(), - logDir, - wal.Config{}, - ) + stream, err := wal.NewChangelogWAL(logger.NewNopLogger(), logDir, wal.Config{}) if err != nil { panic(err) } diff --git a/sei-db/wal/changelog.go b/sei-db/wal/changelog.go new file mode 100644 index 0000000000..b9a44e6e58 --- /dev/null +++ b/sei-db/wal/changelog.go @@ -0,0 +1,25 @@ +package wal + +import ( + "github.com/sei-protocol/sei-chain/sei-db/common/logger" + "github.com/sei-protocol/sei-chain/sei-db/proto" +) + +// ChangelogWAL is a type alias for a WAL specialized for ChangelogEntry. +type ChangelogWAL = GenericWAL[proto.ChangelogEntry] + +// NewChangelogWAL creates a new WAL for ChangelogEntry. +// This is a convenience wrapper that handles serialization automatically. +func NewChangelogWAL(logger logger.Logger, dir string, config Config) (ChangelogWAL, error) { + return NewWAL( + func(e proto.ChangelogEntry) ([]byte, error) { return e.Marshal() }, + func(data []byte) (proto.ChangelogEntry, error) { + var e proto.ChangelogEntry + err := e.Unmarshal(data) + return e, err + }, + logger, + dir, + config, + ) +} diff --git a/sei-db/wal/processor.go b/sei-db/wal/processor.go deleted file mode 100644 index cbb999bc87..0000000000 --- a/sei-db/wal/processor.go +++ /dev/null @@ -1,87 +0,0 @@ -package wal - -import ( - "fmt" - - "github.com/sei-protocol/sei-chain/sei-db/proto" -) - -var _ GenericWALProcessor[proto.ChangelogEntry] = (*Processor)(nil) - -type Processor struct { - maxPendingSize int - chPendingEntries chan proto.ChangelogEntry - errSignal chan error - stopSignal chan struct{} - processFn func(entry proto.ChangelogEntry) error -} - -func NewSubscriber( - maxPendingSize int, - processFn func(entry proto.ChangelogEntry) error, -) *Processor { - subscriber := &Processor{ - maxPendingSize: maxPendingSize, - processFn: processFn, - } - - return subscriber -} - -func (s *Processor) Start() { - if s.maxPendingSize > 0 { - s.startAsyncProcessing() - } -} - -func (s *Processor) ProcessEntry(entry proto.ChangelogEntry) error { - if s.maxPendingSize <= 0 { - return s.processFn(entry) - } - s.chPendingEntries <- entry - return s.CheckError() -} - -func (s *Processor) startAsyncProcessing() { - if s.chPendingEntries == nil { - s.chPendingEntries = make(chan proto.ChangelogEntry, s.maxPendingSize) - s.errSignal = make(chan error) - s.stopSignal = make(chan struct{}) - go func() { - defer close(s.errSignal) - for { - select { - case entry := <-s.chPendingEntries: - if err := s.processFn(entry); err != nil { - s.errSignal <- err - } - case <-s.stopSignal: - return - } - } - }() - } -} - -func (s *Processor) Close() error { - if s.chPendingEntries == nil { - return nil - } - s.stopSignal <- struct{}{} - close(s.chPendingEntries) - err := s.CheckError() - s.chPendingEntries = nil - s.errSignal = nil - s.stopSignal = nil - return err -} - -func (s *Processor) CheckError() error { - select { - case err := <-s.errSignal: - // async wal writing failed, we need to abort the state machine - return fmt.Errorf("subscriber failed unexpectedly: %w", err) - default: - } - return nil -} From 9f3cf3f812c318463fbf78ee0994b4bd6bc7786e Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Tue, 13 Jan 2026 16:56:36 -0800 Subject: [PATCH 23/35] Fix go mod --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index cf11ba8a77..8c4483202a 100644 --- a/go.sum +++ b/go.sum @@ -2083,8 +2083,6 @@ github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhso github.com/tidwall/sjson v1.1.4/go.mod h1:wXpKXu8CtDjKAZ+3DrKY5ROCorDFahq8l0tey/Lx1fg= github.com/tidwall/tinylru v1.1.0 h1:XY6IUfzVTU9rpwdhKUF6nQdChgCdGjkMfLzbWyiau6I= github.com/tidwall/tinylru v1.1.0/go.mod h1:3+bX+TJ2baOLMWTnlyNWHh4QMnFyARg2TLTQ6OFbzw8= -github.com/tidwall/wal v1.1.7 h1:emc1TRjIVsdKKSnpwGBAcsAGg0767SvUk8+ygx7Bb+4= -github.com/tidwall/wal v1.1.7/go.mod h1:r6lR1j27W9EPalgHiB7zLJDYu3mzW5BQP5KrzBpYY/E= github.com/tidwall/wal v1.2.1 h1:xQvwnRF3e+xBC4NvFvl1mPGJHU0aH5zNzlUKnKGIImA= github.com/tidwall/wal v1.2.1/go.mod h1:r6lR1j27W9EPalgHiB7zLJDYu3mzW5BQP5KrzBpYY/E= github.com/timakin/bodyclose v0.0.0-20210704033933-f49887972144 h1:kl4KhGNsJIbDHS9/4U9yQo1UcPQM0kOMJHn29EoH/Ro= From f2d11979a1c0d013d3e048f574925d0b3ccc9bb9 Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Tue, 13 Jan 2026 17:54:01 -0800 Subject: [PATCH 24/35] remove checkerror in WAL --- sei-db/state_db/sc/memiavl/db.go | 64 +++++++++++-------------- sei-db/state_db/sc/memiavl/multitree.go | 8 ---- sei-db/wal/types.go | 3 -- sei-db/wal/wal.go | 50 +++++++++---------- sei-db/wal/wal_test.go | 4 -- 5 files changed, 55 insertions(+), 74 deletions(-) diff --git a/sei-db/state_db/sc/memiavl/db.go b/sei-db/state_db/sc/memiavl/db.go index 648912b182..acf9ffd210 100644 --- a/sei-db/state_db/sc/memiavl/db.go +++ b/sei-db/state_db/sc/memiavl/db.go @@ -205,27 +205,19 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * // MemIAVL owns changelog lifecycle: always open the WAL here. // Even in read-only mode we may need WAL replay to reconstruct non-snapshot versions. - var walHandler wal.GenericWAL[proto.ChangelogEntry] - changelogDir := utils.GetChangelogPath(opts.Dir) - - writeBuf := 0 - if !opts.ReadOnly { - writeBuf = opts.AsyncCommitBuffer - } - w, err := wal.NewChangelogWAL(logger, changelogDir, wal.Config{ + streamHandler, err := wal.NewChangelogWAL(logger, utils.GetChangelogPath(opts.Dir), wal.Config{ DisableFsync: true, ZeroCopy: true, - WriteBufferSize: writeBuf, + WriteBufferSize: opts.AsyncCommitBuffer, }) if err != nil { return nil, fmt.Errorf("failed to open changelog WAL: %w", err) } - walHandler = w // Compute WAL index delta (only needed once per DB open) var walIndexDelta int64 var walHasEntries bool - walIndexDelta, walHasEntries, err = computeWALIndexDelta(walHandler) + walIndexDelta, walHasEntries, err = computeWALIndexDelta(streamHandler) if err != nil { return nil, fmt.Errorf("failed to compute WAL index delta: %w", err) } @@ -237,7 +229,7 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * // Replay WAL to catch up to target version (if WAL has entries) if walHasEntries && (targetVersion == 0 || targetVersion > mtree.Version()) { logger.Info("Start catching up and replaying the MemIAVL changelog file") - if err := mtree.Catchup(context.Background(), walHandler, walIndexDelta, targetVersion); err != nil { + if err := mtree.Catchup(context.Background(), streamHandler, walIndexDelta, targetVersion); err != nil { return nil, err } logger.Info(fmt.Sprintf("Finished the replay and caught up to version %d", targetVersion)) @@ -263,7 +255,7 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * // Use O(1) conversion: walIndex = version - delta truncateIndex := targetVersion - walIndexDelta if truncateIndex > 0 { - if err := walHandler.TruncateAfter(uint64(truncateIndex)); err != nil { + if err := streamHandler.TruncateAfter(uint64(truncateIndex)); err != nil { return nil, fmt.Errorf("fail to truncate rlog file: %w", err) } } @@ -301,7 +293,7 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * fileLock: fileLock, readOnly: opts.ReadOnly, walIndexDelta: walIndexDelta, - streamHandler: walHandler, + streamHandler: streamHandler, snapshotKeepRecent: opts.SnapshotKeepRecent, snapshotInterval: opts.SnapshotInterval, snapshotMinTimeInterval: opts.SnapshotMinTimeInterval, @@ -448,11 +440,7 @@ func (db *DB) ApplyChangeSet(name string, changeSet iavl.ChangeSet) error { // checkAsyncTasks checks the status of background tasks non-blocking-ly and process the result func (db *DB) checkAsyncTasks() error { - var walErr error - if wal := db.GetWAL(); wal != nil { - walErr = wal.CheckError() - } - return errorutils.Join(walErr, db.checkBackgroundSnapshotRewrite()) + return db.checkBackgroundSnapshotRewrite() } // CommittedVersion returns the current version of the MultiTree. @@ -470,7 +458,7 @@ func (db *DB) CommittedVersion() int64 { db.logger.Error("WAL last offset overflows int64", "lastOffset", lastOffset) return math.MaxInt64 } - return int64(lastOffset) + db.walIndexDelta + return db.walIndexToVersion(lastOffset) } } return db.MultiTree.Version() @@ -631,24 +619,31 @@ func (db *DB) Commit() (version int64, _err error) { return 0, errReadOnly } - nextVersion := db.MultiTree.WorkingCommitInfo().Version + // Commit the in-memory tree state FIRST. + // MemIAVL is purely in-memory; SaveVersion() doesn't persist anything. + // The changelog WAL is our persistence layer. + v, err := db.MultiTree.SaveVersion(true) + if err != nil { + return 0, err + } + + // Write to WAL AFTER successful SaveVersion. + // Rationale: If SaveVersion fails but we already wrote to WAL, we'd have + // a WAL entry for a version that was never committed. On replay, this would + // corrupt state. By writing WAL after SaveVersion succeeds, we ensure WAL + // only contains valid committed versions. If WAL write fails after SaveVersion, + // we lose this version on crash (rollback to prior state), but remain consistent. + // + // Note: Write() automatically checks for any previous async write errors. if wal := db.GetWAL(); wal != nil { entry := db.pendingLogEntry - entry.Version = nextVersion + entry.Version = v if err := wal.Write(entry); err != nil { return 0, fmt.Errorf("failed to write changelog WAL: %w", err) } - if err := wal.CheckError(); err != nil { - return 0, fmt.Errorf("changelog WAL async write error: %w", err) - } } db.pendingLogEntry = proto.ChangelogEntry{} - v, err := db.MultiTree.SaveVersion(true) - if err != nil { - return 0, err - } - if err := db.checkAsyncTasks(); err != nil { return 0, err } @@ -677,16 +672,15 @@ func (db *DB) tryTruncateWAL() { db.logger.Error("WAL first offset overflows int64; skipping truncation", "firstWALIndex", firstWALIndex) return } - walEarliestVersion := int64(firstWALIndex) + db.walIndexDelta + walEarliestVersion := db.walIndexToVersion(firstWALIndex) if walEarliestVersion >= earliestSnapshotVersion { return } - truncateIndex := earliestSnapshotVersion - db.walIndexDelta - if truncateIndex <= 0 || truncateIndex <= int64(firstWALIndex) { + truncateIndex := db.versionToWALIndex(earliestSnapshotVersion) + if truncateIndex == 0 || truncateIndex <= firstWALIndex { return } - // #nosec G115 -- truncateIndex is checked to be positive and <= MaxInt64 above. - if err := db.streamHandler.TruncateBefore(uint64(truncateIndex)); err != nil { + if err := db.streamHandler.TruncateBefore(truncateIndex); err != nil { db.logger.Error("failed to truncate changelog WAL", "err", err, "truncateIndex", truncateIndex) } } diff --git a/sei-db/state_db/sc/memiavl/multitree.go b/sei-db/state_db/sc/memiavl/multitree.go index 235c014b60..4d726aa820 100644 --- a/sei-db/state_db/sc/memiavl/multitree.go +++ b/sei-db/state_db/sc/memiavl/multitree.go @@ -264,14 +264,6 @@ func (t *MultiTree) ApplyUpgrades(upgrades []*proto.TreeNameUpgrade) error { t.trees[i].Name = upgrade.Name default: // add tree - // Idempotency: adding an existing tree should be a no-op. This can happen - // if callers apply InitialStores up front and WAL replay also contains the - // same "add tree" upgrades. - if slices.IndexFunc(t.trees, func(entry NamedTree) bool { - return entry.Name == upgrade.Name - }) >= 0 { - continue - } v := utils.NextVersion(t.Version(), t.initialVersion.Load()) if v < 0 || v > math.MaxUint32 { return fmt.Errorf("version overflows uint32: %d", v) diff --git a/sei-db/wal/types.go b/sei-db/wal/types.go index 5292e35694..be5584a6b2 100644 --- a/sei-db/wal/types.go +++ b/sei-db/wal/types.go @@ -11,9 +11,6 @@ type GenericWAL[T any] interface { // Write will append a new entry to the end of the log. Write(entry T) error - // CheckError check the error signal of async writes - CheckError() error - // TruncateBefore will remove all entries that are before the provided `offset` TruncateBefore(offset uint64) error diff --git a/sei-db/wal/wal.go b/sei-db/wal/wal.go index 5ff87e3ad7..83fdfd09c0 100644 --- a/sei-db/wal/wal.go +++ b/sei-db/wal/wal.go @@ -24,7 +24,7 @@ type WAL[T any] struct { marshal MarshalFn[T] unmarshal UnmarshalFn[T] writeChannel chan T - errSignal chan error + asyncErr atomic.Pointer[error] // stores async write error, checked on each Write() nextOffset uint64 isClosed atomic.Bool closeCh chan struct{} // signals shutdown to background goroutines @@ -96,7 +96,13 @@ func NewWAL[T any]( // Write will append a new entry to the end of the log. // Whether the writes is in blocking or async manner depends on the buffer size. +// For async writes, this also checks for any previous async write errors. func (walLog *WAL[T]) Write(entry T) error { + // Check for any previous async write error + if errPtr := walLog.asyncErr.Load(); errPtr != nil { + return fmt.Errorf("async WAL write failed previously: %w", *errPtr) + } + channelBufferSize := walLog.config.WriteBufferSize if channelBufferSize > 0 { if walLog.writeChannel == nil { @@ -124,12 +130,12 @@ func (walLog *WAL[T]) Write(entry T) error { // This should only be called on initialization if async write is enabled func (walLog *WAL[T]) startWriteGoroutine() { walLog.writeChannel = make(chan T, walLog.config.WriteBufferSize) - walLog.errSignal = make(chan error) // Capture the starting offset for the goroutine writeOffset := walLog.nextOffset + walLog.wg.Add(1) go func() { + defer walLog.wg.Done() batch := wal.Batch{} - defer close(walLog.errSignal) for { entries := channelBatchRecv(walLog.writeChannel) if len(entries) == 0 { @@ -140,7 +146,7 @@ func (walLog *WAL[T]) startWriteGoroutine() { for _, entry := range entries { bz, err := walLog.marshal(entry) if err != nil { - walLog.errSignal <- err + walLog.asyncErr.Store(&err) return } batch.Write(writeOffset, bz) @@ -148,7 +154,7 @@ func (walLog *WAL[T]) startWriteGoroutine() { } if err := walLog.log.WriteBatch(&batch); err != nil { - walLog.errSignal <- err + walLog.asyncErr.Store(&err) return } batch.Clear() @@ -173,17 +179,6 @@ func (walLog *WAL[T]) TruncateBefore(index uint64) error { return walLog.log.TruncateFront(index) } -// CheckError check if there's any failed async writes or not -func (walLog *WAL[T]) CheckError() error { - select { - case err := <-walLog.errSignal: - // async wal writing failed, we need to abort the state machine - return fmt.Errorf("async wal writing goroutine quit unexpectedly: %w", err) - default: - } - return nil -} - func (walLog *WAL[T]) FirstOffset() (index uint64, err error) { return walLog.log.FirstIndex() } @@ -258,18 +253,25 @@ func (walLog *WAL[T]) Close() error { // Signal background goroutines to stop close(walLog.closeCh) - // Wait for background goroutines (pruning) to finish before closing resources - walLog.wg.Wait() - - var err error + // Close write channel to signal the write goroutine to exit. + // Don't set to nil yet - goroutine still needs to read from it. if walLog.writeChannel != nil { close(walLog.writeChannel) - err = <-walLog.errSignal - walLog.writeChannel = nil - walLog.errSignal = nil + } + + // Wait for all background goroutines (pruning + write) to finish + walLog.wg.Wait() + + // Now safe to nil out + walLog.writeChannel = nil + + // Check for any async write error that occurred + var asyncErr error + if errPtr := walLog.asyncErr.Load(); errPtr != nil { + asyncErr = *errPtr } errClose := walLog.log.Close() - return errorutils.Join(err, errClose) + return errorutils.Join(asyncErr, errClose) } // open opens the replay log, try to truncate the corrupted tail if there's any diff --git a/sei-db/wal/wal_test.go b/sei-db/wal/wal_test.go index 869b61ae1b..701c370229 100644 --- a/sei-db/wal/wal_test.go +++ b/sei-db/wal/wal_test.go @@ -365,10 +365,6 @@ func TestCheckErrorNoError(t *testing.T) { err = changelog.Write(*entry) require.NoError(t, err) - // CheckError should return nil when no errors - err = changelog.CheckError() - require.NoError(t, err) - err = changelog.Close() require.NoError(t, err) } From 79324e2e7ce5fe9fc52a45c349fdbc59d2f18c5b Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Wed, 14 Jan 2026 08:17:05 -0800 Subject: [PATCH 25/35] Address comments to make integration test simpler and db not panic --- .github/workflows/integration-test.yml | 37 ++++---------------------- sei-db/db_engine/pebbledb/mvcc/db.go | 2 +- sei-db/db_engine/rocksdb/mvcc/db.go | 2 +- 3 files changed, 7 insertions(+), 34 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 354773e4b4..4010586d47 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -175,31 +175,7 @@ jobs: done unset IFS # revert the internal field separator back to default - # The integration test runner executes commands inside docker containers (see integration_test/scripts/runner.py). - # Logs written to build/generated/logs therefore live inside the containers, not on the GitHub runner host. - - name: Collect docker node logs (container filesystem) - if: ${{ always() }} - run: | - set -euo pipefail - mkdir -p artifacts/container-logs - for c in sei-node-0 sei-node-1 sei-node-2 sei-node-3 sei-rpc-node; do - if ! docker ps --format '{{.Names}}' | grep -q "^${c}$"; then - echo "Container ${c} not running; skipping" - continue - fi - echo "Locating build/generated/logs in ${c}..." - LOG_DIR=$(docker exec "${c}" /bin/bash -lc 'set -e; for root in /root /workspace /; do d=$(find "$root" -maxdepth 6 -type d -path "*/build/generated/logs" 2>/dev/null | head -n1 || true); if [ -n "$d" ]; then echo "$d"; exit 0; fi; done; exit 0') - if [ -z "${LOG_DIR}" ]; then - echo "No build/generated/logs found in ${c}" - continue - fi - echo "Found ${LOG_DIR} in ${c}" - mkdir -p "artifacts/container-logs/${c}" - # Copy logs out for artifact upload - docker cp "${c}:${LOG_DIR}" "artifacts/container-logs/${c}/" || true - done - - - name: Print last 200 lines of node logs on failure + - name: Print node logs on failure if: ${{ failure() }} run: | set -euo pipefail @@ -207,9 +183,10 @@ jobs: echo "==================== ${c} (docker logs tail) ====================" docker logs --tail 200 "${c}" || true echo "==================== ${c} (seid log file tail) ====================" - # Try to print the per-node log file if present in the copied artifacts - if [ -f "artifacts/container-logs/${c}/logs/seid-${c#sei-node-}.log" ]; then - tail -200 "artifacts/container-logs/${c}/logs/seid-${c#sei-node-}.log" || true + # Logs are accessible on host since build/generated is mounted in containers + NODE_ID=${c#sei-node-} + if [ -f "build/generated/logs/seid-${NODE_ID}.log" ]; then + tail -200 "build/generated/logs/seid-${NODE_ID}.log" || true fi done @@ -234,10 +211,6 @@ jobs: else echo "No logs directory found" fi - # Also include container logs (these are the real node logs for most integration tests) - if [ -d artifacts/container-logs ]; then - cp -r artifacts/container-logs "$LOG_ROOT/" - fi - name: Upload logs directory if: ${{ always() }} diff --git a/sei-db/db_engine/pebbledb/mvcc/db.go b/sei-db/db_engine/pebbledb/mvcc/db.go index af572debf7..201fc279c3 100644 --- a/sei-db/db_engine/pebbledb/mvcc/db.go +++ b/sei-db/db_engine/pebbledb/mvcc/db.go @@ -158,7 +158,7 @@ func OpenDB(dataDir string, config config.StateStoreConfig) (*Database, error) { PruneInterval: time.Duration(config.PruneIntervalSeconds) * time.Second, }) if err != nil { - panic(err) + return nil, err } database.streamHandler = streamHandler database.asyncWriteWG.Add(1) diff --git a/sei-db/db_engine/rocksdb/mvcc/db.go b/sei-db/db_engine/rocksdb/mvcc/db.go index e3a1fd8227..42567007e6 100644 --- a/sei-db/db_engine/rocksdb/mvcc/db.go +++ b/sei-db/db_engine/rocksdb/mvcc/db.go @@ -120,7 +120,7 @@ func OpenDB(dataDir string, config config.StateStoreConfig) (*Database, error) { PruneInterval: time.Duration(config.PruneIntervalSeconds) * time.Second, }) if err != nil { - panic(err) + return nil, err } database.streamHandler = streamHandler go database.writeAsyncInBackground() From 12d563ab0166596ffdca07bb0e8bdbcb74b61bff Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Wed, 14 Jan 2026 08:21:36 -0800 Subject: [PATCH 26/35] Remove unnecessary config overrides and fix resource leak --- sei-db/db_engine/pebbledb/mvcc/db.go | 2 -- sei-db/db_engine/rocksdb/mvcc/db.go | 2 -- sei-db/state_db/sc/memiavl/db.go | 2 -- sei-db/wal/wal.go | 7 +++---- 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/sei-db/db_engine/pebbledb/mvcc/db.go b/sei-db/db_engine/pebbledb/mvcc/db.go index 201fc279c3..c8236022f3 100644 --- a/sei-db/db_engine/pebbledb/mvcc/db.go +++ b/sei-db/db_engine/pebbledb/mvcc/db.go @@ -152,8 +152,6 @@ func OpenDB(dataDir string, config config.StateStoreConfig) (*Database, error) { return nil, errors.New("KeepRecent must be non-negative") } streamHandler, err := wal.NewChangelogWAL(logger.NewNopLogger(), utils.GetChangelogPath(dataDir), wal.Config{ - DisableFsync: true, - ZeroCopy: true, KeepRecent: uint64(config.KeepRecent), PruneInterval: time.Duration(config.PruneIntervalSeconds) * time.Second, }) diff --git a/sei-db/db_engine/rocksdb/mvcc/db.go b/sei-db/db_engine/rocksdb/mvcc/db.go index 42567007e6..53f1a67a79 100644 --- a/sei-db/db_engine/rocksdb/mvcc/db.go +++ b/sei-db/db_engine/rocksdb/mvcc/db.go @@ -114,8 +114,6 @@ func OpenDB(dataDir string, config config.StateStoreConfig) (*Database, error) { database.latestVersion.Store(latestVersion) streamHandler, err := wal.NewChangelogWAL(logger.NewNopLogger(), utils.GetChangelogPath(dataDir), wal.Config{ - DisableFsync: true, - ZeroCopy: true, KeepRecent: uint64(config.KeepRecent), PruneInterval: time.Duration(config.PruneIntervalSeconds) * time.Second, }) diff --git a/sei-db/state_db/sc/memiavl/db.go b/sei-db/state_db/sc/memiavl/db.go index acf9ffd210..2038f6c96e 100644 --- a/sei-db/state_db/sc/memiavl/db.go +++ b/sei-db/state_db/sc/memiavl/db.go @@ -206,8 +206,6 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * // MemIAVL owns changelog lifecycle: always open the WAL here. // Even in read-only mode we may need WAL replay to reconstruct non-snapshot versions. streamHandler, err := wal.NewChangelogWAL(logger, utils.GetChangelogPath(opts.Dir), wal.Config{ - DisableFsync: true, - ZeroCopy: true, WriteBufferSize: opts.AsyncCommitBuffer, }) if err != nil { diff --git a/sei-db/wal/wal.go b/sei-db/wal/wal.go index 83fdfd09c0..622620c2d4 100644 --- a/sei-db/wal/wal.go +++ b/sei-db/wal/wal.go @@ -32,8 +32,6 @@ type WAL[T any] struct { } type Config struct { - DisableFsync bool - ZeroCopy bool WriteBufferSize int KeepRecent uint64 PruneInterval time.Duration @@ -60,8 +58,8 @@ func NewWAL[T any]( config Config, ) (*WAL[T], error) { log, err := open(dir, &wal.Options{ - NoSync: config.DisableFsync, - NoCopy: config.ZeroCopy, + NoSync: true, + NoCopy: true, }) if err != nil { return nil, err @@ -79,6 +77,7 @@ func NewWAL[T any]( // Finding the nextOffset to write lastIndex, err := log.LastIndex() if err != nil { + _ = log.Close() // Close the opened log to avoid resource leak return nil, err } w.nextOffset = lastIndex + 1 From 6a5dc4a46db943e604359b4fddfddcb8163e8e0f Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Wed, 14 Jan 2026 08:28:41 -0800 Subject: [PATCH 27/35] Fix type and add error check for last and first index --- sei-db/state_db/sc/memiavl/db.go | 6 +++--- sei-db/state_db/sc/memiavl/multitree.go | 2 +- sei-db/wal/wal.go | 12 ++++++++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/sei-db/state_db/sc/memiavl/db.go b/sei-db/state_db/sc/memiavl/db.go index 2038f6c96e..a3b18cb332 100644 --- a/sei-db/state_db/sc/memiavl/db.go +++ b/sei-db/state_db/sc/memiavl/db.go @@ -55,7 +55,7 @@ type DB struct { // streamHandler is the changelog WAL owned by MemIAVL. // It is opened during OpenDB (if present / allowed) and closed in DB.Close(). - streamHandler wal.GenericWAL[proto.ChangelogEntry] + streamHandler wal.ChangelogWAL // pendingLogEntry accumulates changes (changesets + upgrades) to be written // into the changelog WAL on the next Commit(). pendingLogEntry proto.ChangelogEntry @@ -318,7 +318,7 @@ func OpenDB(logger logger.Logger, targetVersion int64, opts Options) (database * } // GetWAL returns the WAL handler for changelog operations. -func (db *DB) GetWAL() wal.GenericWAL[proto.ChangelogEntry] { +func (db *DB) GetWAL() wal.ChangelogWAL { return db.streamHandler } @@ -555,7 +555,7 @@ func (db *DB) pruneSnapshots() { // computeWALIndexDelta computes the constant delta between version and WAL index. // Since both are strictly contiguous, we only need to read one entry. // Returns (delta, hasEntries, error). hasEntries is false if WAL is empty. -func computeWALIndexDelta(stream wal.GenericWAL[proto.ChangelogEntry]) (int64, bool, error) { +func computeWALIndexDelta(stream wal.ChangelogWAL) (int64, bool, error) { firstIndex, err := stream.FirstOffset() if err != nil { return 0, false, err diff --git a/sei-db/state_db/sc/memiavl/multitree.go b/sei-db/state_db/sc/memiavl/multitree.go index 4d726aa820..7a266d1ce7 100644 --- a/sei-db/state_db/sc/memiavl/multitree.go +++ b/sei-db/state_db/sc/memiavl/multitree.go @@ -361,7 +361,7 @@ func (t *MultiTree) UpdateCommitInfo() { // Catchup replays WAL entries to catch up the tree to the target or latest version. // delta is the difference between version and WAL index (version = walIndex + delta). // endVersion specifies the target version (0 means catch up to latest). -func (t *MultiTree) Catchup(ctx context.Context, stream wal.GenericWAL[proto.ChangelogEntry], delta int64, endVersion int64) error { +func (t *MultiTree) Catchup(ctx context.Context, stream wal.ChangelogWAL, delta int64, endVersion int64) error { startTime := time.Now() // Get actual WAL index range diff --git a/sei-db/wal/wal.go b/sei-db/wal/wal.go index 622620c2d4..a9391e43f0 100644 --- a/sei-db/wal/wal.go +++ b/sei-db/wal/wal.go @@ -234,8 +234,16 @@ func (walLog *WAL[T]) StartPruning(keepRecent uint64, pruneInterval time.Duratio case <-walLog.closeCh: return case <-ticker.C: - lastIndex, _ := walLog.log.LastIndex() - firstIndex, _ := walLog.log.FirstIndex() + lastIndex, err := walLog.log.LastIndex() + if err != nil { + walLog.logger.Error("failed to get last index for pruning", "err", err) + continue + } + firstIndex, err := walLog.log.FirstIndex() + if err != nil { + walLog.logger.Error("failed to get first index for pruning", "err", err) + continue + } if lastIndex > keepRecent && (lastIndex-keepRecent) > firstIndex { prunePos := lastIndex - keepRecent if err := walLog.TruncateBefore(prunePos); err != nil { From 136ffa8c6c56deea3b08597e179997069af218c5 Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Wed, 14 Jan 2026 13:17:34 -0800 Subject: [PATCH 28/35] Fix locking issue and thread safety issue for WAL --- sei-db/wal/wal.go | 248 +++++++++++++++++++++-------------------- sei-db/wal/wal_test.go | 5 +- 2 files changed, 134 insertions(+), 119 deletions(-) diff --git a/sei-db/wal/wal.go b/sei-db/wal/wal.go index a9391e43f0..b843cbd922 100644 --- a/sei-db/wal/wal.go +++ b/sei-db/wal/wal.go @@ -11,24 +11,23 @@ import ( "github.com/tidwall/wal" - errorutils "github.com/sei-protocol/sei-chain/sei-db/common/errors" "github.com/sei-protocol/sei-chain/sei-db/common/logger" ) // WAL is a generic write-ahead log implementation. type WAL[T any] struct { - dir string - log *wal.Log - config Config - logger logger.Logger - marshal MarshalFn[T] - unmarshal UnmarshalFn[T] - writeChannel chan T - asyncErr atomic.Pointer[error] // stores async write error, checked on each Write() - nextOffset uint64 - isClosed atomic.Bool - closeCh chan struct{} // signals shutdown to background goroutines - wg sync.WaitGroup // tracks background goroutines (pruning) + dir string + log *wal.Log + config Config + logger logger.Logger + marshal MarshalFn[T] + unmarshal UnmarshalFn[T] + writeChannel chan T + mtx sync.RWMutex + asyncWriteErrCh chan error // buffered=1; async writer reports first error non-blocking + isClosed atomic.Bool + closeCh chan struct{} // signals shutdown to background goroutines + wg sync.WaitGroup // tracks background goroutines (pruning) } type Config struct { @@ -65,29 +64,20 @@ func NewWAL[T any]( return nil, err } w := &WAL[T]{ - dir: dir, - log: log, - config: config, - logger: logger, - marshal: marshal, - unmarshal: unmarshal, - closeCh: make(chan struct{}), - // isClosed is zero-initialized to false (atomic.Bool) + dir: dir, + log: log, + config: config, + logger: logger, + marshal: marshal, + unmarshal: unmarshal, + closeCh: make(chan struct{}), + asyncWriteErrCh: make(chan error, 1), + isClosed: atomic.Bool{}, } - // Finding the nextOffset to write - lastIndex, err := log.LastIndex() - if err != nil { - _ = log.Close() // Close the opened log to avoid resource leak - return nil, err - } - w.nextOffset = lastIndex + 1 + // Start the auto pruning goroutine - if config.KeepRecent > 0 { - w.wg.Add(1) - go func() { - defer w.wg.Done() - w.StartPruning(config.KeepRecent, config.PruneInterval) - }() + if config.KeepRecent > 0 && config.PruneInterval > 0 { + w.StartPruning(config.KeepRecent, config.PruneInterval) } return w, nil @@ -97,66 +87,65 @@ func NewWAL[T any]( // Whether the writes is in blocking or async manner depends on the buffer size. // For async writes, this also checks for any previous async write errors. func (walLog *WAL[T]) Write(entry T) error { - // Check for any previous async write error - if errPtr := walLog.asyncErr.Load(); errPtr != nil { - return fmt.Errorf("async WAL write failed previously: %w", *errPtr) - } + walLog.mtx.Lock() + defer walLog.mtx.Unlock() - channelBufferSize := walLog.config.WriteBufferSize - if channelBufferSize > 0 { + if walLog.isClosed.Load() { + return errors.New("wal is closed") + } + if err := walLog.getAsyncWriteErrLocked(); err != nil { + return fmt.Errorf("async WAL write failed previously: %w", err) + } + writeBufferSize := walLog.config.WriteBufferSize + if writeBufferSize > 0 { + // async write if walLog.writeChannel == nil { - walLog.logger.Info(fmt.Sprintf("async write is enabled with buffer size %d", channelBufferSize)) - walLog.startWriteGoroutine() + walLog.writeChannel = make(chan T, writeBufferSize) + walLog.startAsyncWriteGoroutine() + walLog.logger.Info(fmt.Sprintf("WAL async write is enabled with buffer size %d", writeBufferSize)) } - // async write walLog.writeChannel <- entry - walLog.nextOffset++ } else { // synchronous write bz, err := walLog.marshal(entry) if err != nil { return err } - if err := walLog.log.Write(walLog.nextOffset, bz); err != nil { + lastOffset, err := walLog.log.LastIndex() + if err != nil { + return err + } + if err := walLog.log.Write(lastOffset+1, bz); err != nil { return err } - walLog.nextOffset++ } return nil } // startWriteGoroutine will start a goroutine to write entries to the log. // This should only be called on initialization if async write is enabled -func (walLog *WAL[T]) startWriteGoroutine() { - walLog.writeChannel = make(chan T, walLog.config.WriteBufferSize) - // Capture the starting offset for the goroutine - writeOffset := walLog.nextOffset +func (walLog *WAL[T]) startAsyncWriteGoroutine() { walLog.wg.Add(1) + ch := walLog.writeChannel go func() { defer walLog.wg.Done() - batch := wal.Batch{} - for { - entries := channelBatchRecv(walLog.writeChannel) - if len(entries) == 0 { - // channel is closed - break + for entry := range ch { + bz, err := walLog.marshal(entry) + if err != nil { + walLog.recordAsyncWriteErr(err) + return } - - for _, entry := range entries { - bz, err := walLog.marshal(entry) - if err != nil { - walLog.asyncErr.Store(&err) - return - } - batch.Write(writeOffset, bz) - writeOffset++ + nextOffset, err := walLog.NextOffset() + if err != nil { + walLog.recordAsyncWriteErr(err) + return } - - if err := walLog.log.WriteBatch(&batch); err != nil { - walLog.asyncErr.Store(&err) + err = walLog.log.Write(nextOffset, bz) + if err != nil { + walLog.recordAsyncWriteErr(err) return } - batch.Clear() + } }() } @@ -167,13 +156,12 @@ func (walLog *WAL[T]) TruncateAfter(index uint64) error { if err := walLog.log.TruncateBack(index); err != nil { return err } - // Update nextOffset to reflect the new end of log - walLog.nextOffset = index + 1 return nil } // TruncateBefore will remove all entries that are before the provided `index`. // In other words the entry at `index` becomes the first entry in the log. +// Need to add write lock because this would change the next write offset func (walLog *WAL[T]) TruncateBefore(index uint64) error { return walLog.log.TruncateFront(index) } @@ -187,6 +175,14 @@ func (walLog *WAL[T]) LastOffset() (index uint64, err error) { return walLog.log.LastIndex() } +func (walLog *WAL[T]) NextOffset() (index uint64, err error) { + lastOffset, err := walLog.log.LastIndex() + if err != nil { + return 0, err + } + return lastOffset + 1, nil +} + // ReadAt will read the log entry at the provided index func (walLog *WAL[T]) ReadAt(index uint64) (T, error) { var zero T @@ -221,64 +217,80 @@ func (walLog *WAL[T]) Replay(start uint64, end uint64, processFn func(index uint } func (walLog *WAL[T]) StartPruning(keepRecent uint64, pruneInterval time.Duration) { - // Use a minimum interval to avoid tight loops - if pruneInterval <= 0 { - pruneInterval = time.Minute // Default to 1 minute if not specified - } - - ticker := time.NewTicker(pruneInterval) - defer ticker.Stop() - - for { - select { - case <-walLog.closeCh: - return - case <-ticker.C: - lastIndex, err := walLog.log.LastIndex() - if err != nil { - walLog.logger.Error("failed to get last index for pruning", "err", err) - continue - } - firstIndex, err := walLog.log.FirstIndex() - if err != nil { - walLog.logger.Error("failed to get first index for pruning", "err", err) - continue - } - if lastIndex > keepRecent && (lastIndex-keepRecent) > firstIndex { - prunePos := lastIndex - keepRecent - if err := walLog.TruncateBefore(prunePos); err != nil { - walLog.logger.Error(fmt.Sprintf("failed to prune changelog till index %d", prunePos), "err", err) + walLog.wg.Add(1) + go func() { + defer walLog.wg.Done() + ticker := time.NewTicker(pruneInterval) + defer ticker.Stop() + for { + select { + case <-walLog.closeCh: + return + case <-ticker.C: + lastIndex, err := walLog.log.LastIndex() + if err != nil { + walLog.logger.Error("failed to get last index for pruning", "err", err) + continue + } + firstIndex, err := walLog.log.FirstIndex() + if err != nil { + walLog.logger.Error("failed to get first index for pruning", "err", err) + continue + } + if lastIndex > keepRecent && (lastIndex-keepRecent) > firstIndex { + prunePos := lastIndex - keepRecent + if err := walLog.TruncateBefore(prunePos); err != nil { + walLog.logger.Error(fmt.Sprintf("failed to prune changelog till index %d", prunePos), "err", err) + } } } } - } + }() } func (walLog *WAL[T]) Close() error { - walLog.isClosed.Store(true) - - // Signal background goroutines to stop - close(walLog.closeCh) - - // Close write channel to signal the write goroutine to exit. - // Don't set to nil yet - goroutine still needs to read from it. - if walLog.writeChannel != nil { + // Close should only be called once + if walLog.isClosed.CompareAndSwap(false, true) { + // Signal background goroutines to stop. + close(walLog.closeCh) close(walLog.writeChannel) + // Wait for all background goroutines (pruning + async write) to finish + walLog.wg.Wait() + walLog.writeChannel = nil + return walLog.log.Close() } + return nil +} - // Wait for all background goroutines (pruning + write) to finish - walLog.wg.Wait() - - // Now safe to nil out - walLog.writeChannel = nil +// recordAsyncWriteErr records the first async write error (non-blocking). +func (walLog *WAL[T]) recordAsyncWriteErr(err error) { + if err == nil { + return + } + select { + case walLog.asyncWriteErrCh <- err: + default: + // already recorded + } +} - // Check for any async write error that occurred - var asyncErr error - if errPtr := walLog.asyncErr.Load(); errPtr != nil { - asyncErr = *errPtr +// getAsyncWriteErrLocked returns the async write error if present. +// To keep the error "sticky" without an extra cached field, we implement +// a "peek" by reading once and then non-blocking re-inserting the same +// error back into the buffered channel. +// Caller must hold walLog.mtx (read lock is sufficient). +func (walLog *WAL[T]) getAsyncWriteErrLocked() error { + select { + case err := <-walLog.asyncWriteErrCh: + // Put it back so subsequent callers still observe it. + select { + case walLog.asyncWriteErrCh <- err: + default: + } + return err + default: + return nil } - errClose := walLog.log.Close() - return errorutils.Join(asyncErr, errClose) } // open opens the replay log, try to truncate the corrupted tail if there's any diff --git a/sei-db/wal/wal_test.go b/sei-db/wal/wal_test.go index 701c370229..b8a2b1fa2e 100644 --- a/sei-db/wal/wal_test.go +++ b/sei-db/wal/wal_test.go @@ -261,7 +261,10 @@ func TestCloseSyncMode(t *testing.T) { require.NoError(t, err) // Verify isClosed is set - require.True(t, changelog.isClosed.Load()) + changelog.mtx.RLock() + isClosed := changelog.isClosed + changelog.mtx.RUnlock() + require.True(t, isClosed) // Reopen and verify data persisted changelog2, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) From 65bceb29e049e899d091143668fbed417da7d92a Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Wed, 14 Jan 2026 13:18:46 -0800 Subject: [PATCH 29/35] Fix tests --- sei-db/wal/wal_test.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sei-db/wal/wal_test.go b/sei-db/wal/wal_test.go index b8a2b1fa2e..701c370229 100644 --- a/sei-db/wal/wal_test.go +++ b/sei-db/wal/wal_test.go @@ -261,10 +261,7 @@ func TestCloseSyncMode(t *testing.T) { require.NoError(t, err) // Verify isClosed is set - changelog.mtx.RLock() - isClosed := changelog.isClosed - changelog.mtx.RUnlock() - require.True(t, isClosed) + require.True(t, changelog.isClosed.Load()) // Reopen and verify data persisted changelog2, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) From 07859a0fde5a4d55307977689145f7fe6b3a035d Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Wed, 14 Jan 2026 13:49:17 -0800 Subject: [PATCH 30/35] Fix close logic --- sei-db/wal/wal.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sei-db/wal/wal.go b/sei-db/wal/wal.go index b843cbd922..5987d071b3 100644 --- a/sei-db/wal/wal.go +++ b/sei-db/wal/wal.go @@ -249,11 +249,13 @@ func (walLog *WAL[T]) StartPruning(keepRecent uint64, pruneInterval time.Duratio } func (walLog *WAL[T]) Close() error { - // Close should only be called once + // Close should only be called once and run for once if walLog.isClosed.CompareAndSwap(false, true) { // Signal background goroutines to stop. close(walLog.closeCh) - close(walLog.writeChannel) + if walLog.writeChannel != nil { + close(walLog.writeChannel) + } // Wait for all background goroutines (pruning + async write) to finish walLog.wg.Wait() walLog.writeChannel = nil From 2c3d509293a0ee980ea914892596b8fe8b4821d4 Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Wed, 14 Jan 2026 14:22:56 -0800 Subject: [PATCH 31/35] Require error checking for close --- sei-db/state_db/sc/memiavl/db.go | 30 ++++------ sei-db/state_db/sc/memiavl/db_test.go | 19 +++---- .../sc/memiavl/snapshot_pipeline_test.go | 4 +- sei-db/state_db/sc/store_test.go | 55 +++++++++++++++---- sei-db/wal/wal_test.go | 27 +++++---- 5 files changed, 82 insertions(+), 53 deletions(-) diff --git a/sei-db/state_db/sc/memiavl/db.go b/sei-db/state_db/sc/memiavl/db.go index a3b18cb332..14586895f4 100644 --- a/sei-db/state_db/sc/memiavl/db.go +++ b/sei-db/state_db/sc/memiavl/db.go @@ -442,24 +442,15 @@ func (db *DB) checkAsyncTasks() error { } // CommittedVersion returns the current version of the MultiTree. -func (db *DB) CommittedVersion() int64 { - // Prefer the WAL's last offset, converted via walIndexDelta, to avoid relying - // on potentially stale tree metadata. Fall back to the tree version on error - // or when WAL is absent/empty. - if wal := db.GetWAL(); wal != nil { - lastOffset, err := wal.LastOffset() - if err != nil { - db.logger.Error("failed to read WAL last offset for committed version", "err", err) - } else if lastOffset > 0 { - // Defensive bound check: uint64 -> int64 conversion can overflow in theory. - if lastOffset > uint64(math.MaxInt64) { - db.logger.Error("WAL last offset overflows int64", "lastOffset", lastOffset) - return math.MaxInt64 - } - return db.walIndexToVersion(lastOffset) - } +func (db *DB) CommittedVersion() (int64, error) { + lastOffset, err := db.GetWAL().LastOffset() + if err != nil { + return 0, err } - return db.MultiTree.Version() + if lastOffset == 0 { + return db.SnapshotVersion(), nil + } + return db.walIndexToVersion(lastOffset), nil } // checkBackgroundSnapshotRewrite check the result of background snapshot rewrite, cleans up the old snapshots and switches to a new multitree @@ -483,7 +474,10 @@ func (db *DB) checkBackgroundSnapshotRewrite() error { // wait for potential pending writes to finish, to make sure we catch up to latest state. // in real world, block execution should be slower than tree updates, so this should not block for long. for { - committedVersion := db.CommittedVersion() + committedVersion, err := db.CommittedVersion() + if err != nil { + return fmt.Errorf("get committed version failed: %w", err) + } if db.lastCommitInfo.Version == committedVersion { break } diff --git a/sei-db/state_db/sc/memiavl/db_test.go b/sei-db/state_db/sc/memiavl/db_test.go index 647014f2ea..a0d822fd03 100644 --- a/sei-db/state_db/sc/memiavl/db_test.go +++ b/sei-db/state_db/sc/memiavl/db_test.go @@ -27,7 +27,7 @@ func TestRewriteSnapshot(t *testing.T) { InitialStores: []string{"test"}, }) require.NoError(t, err) - defer db.Close() // Ensure DB cleanup + t.Cleanup(func() { require.NoError(t, db.Close()) }) // Ensure DB cleanup for i, changes := range ChangeSets { cs := []*proto.NamedChangeSet{ @@ -51,7 +51,6 @@ func TestRewriteSnapshot(t *testing.T) { func TestRemoveSnapshotDir(t *testing.T) { dbDir := t.TempDir() - defer os.RemoveAll(dbDir) snapshotDir := filepath.Join(dbDir, snapshotName(0)) tmpDir := snapshotDir + "-tmp" @@ -101,7 +100,7 @@ func TestRewriteSnapshotBackground(t *testing.T) { SnapshotKeepRecent: 0, // only a single snapshot is kept }) require.NoError(t, err) - defer db.Close() // Ensure DB cleanup and goroutine termination + t.Cleanup(func() { require.NoError(t, db.Close()) }) // Ensure DB cleanup and goroutine termination // spin up goroutine to keep querying the tree stopCh := make(chan struct{}) @@ -272,7 +271,7 @@ func TestRlog(t *testing.T) { // Reopen (MemIAVL will open the changelog from disk) db, err = OpenDB(logger.NewNopLogger(), 0, Options{Dir: dir, InitialStores: initialStores}) require.NoError(t, err) - defer db.Close() // Close the reopened DB + t.Cleanup(func() { require.NoError(t, db.Close()) }) // Close the reopened DB require.Equal(t, "newtest", db.lastCommitInfo.StoreInfos[0].Name) require.Equal(t, 1, len(db.lastCommitInfo.StoreInfos)) @@ -514,19 +513,19 @@ func TestWALIndexDeltaComputation(t *testing.T) { rollbackTo int64 }{ { - name: "delta=0 (version starts at 1)", + name: "Test wal delta=0 and version = 1", initialVersion: 0, numVersions: 5, rollbackTo: 3, }, { - name: "delta=9 (version starts at 10)", + name: "Test wal delta=9 and version = 10", initialVersion: 10, numVersions: 5, rollbackTo: 12, }, { - name: "delta=99 (version starts at 100)", + name: "Test wal delta=99 and version = 100", initialVersion: 100, numVersions: 5, rollbackTo: 102, @@ -744,7 +743,7 @@ func TestEmptyValue(t *testing.T) { // Reopen (MemIAVL will open the changelog from disk) db, err = OpenDB(logger.NewNopLogger(), 0, Options{Dir: dir, ZeroCopy: true, InitialStores: initialStores}) require.NoError(t, err) - defer db.Close() // Close the reopened DB + t.Cleanup(func() { require.NoError(t, db.Close()) }) // Close the reopened DB require.Equal(t, version, db.Version()) } @@ -940,7 +939,7 @@ func TestCatchupWithCancelledContext(t *testing.T) { InitialStores: initialStores, }) require.NoError(t, err) - defer db.Close() + t.Cleanup(func() { require.NoError(t, db.Close()) }) wal := db.GetWAL() require.NotNil(t, wal) @@ -967,7 +966,7 @@ func TestCatchupWithCancelledContext(t *testing.T) { Logger: logger.NewNopLogger(), }) require.NoError(t, err) - defer mtree.Close() + t.Cleanup(func() { require.NoError(t, mtree.Close()) }) // Catchup with cancelled context should return error ctx, cancel := context.WithCancel(context.Background()) diff --git a/sei-db/state_db/sc/memiavl/snapshot_pipeline_test.go b/sei-db/state_db/sc/memiavl/snapshot_pipeline_test.go index 5e18fb5805..8b5cfbf4e1 100644 --- a/sei-db/state_db/sc/memiavl/snapshot_pipeline_test.go +++ b/sei-db/state_db/sc/memiavl/snapshot_pipeline_test.go @@ -30,7 +30,7 @@ func TestSnapshotWriterPipeline(t *testing.T) { opts.FillDefaults() snapshot, err := OpenSnapshot(snapshotDir, opts) require.NoError(t, err) - defer snapshot.Close() + t.Cleanup(func() { require.NoError(t, snapshot.Close()) }) require.Equal(t, uint32(tree.Version()), snapshot.Version()) require.Equal(t, tree.RootHash(), snapshot.RootHash()) @@ -222,7 +222,7 @@ func TestEmptySnapshotWrite(t *testing.T) { opts.FillDefaults() snapshot, err := OpenSnapshot(snapshotDir, opts) require.NoError(t, err) - defer snapshot.Close() + t.Cleanup(func() { require.NoError(t, snapshot.Close()) }) require.True(t, snapshot.IsEmpty()) require.Equal(t, uint32(0), snapshot.Version()) diff --git a/sei-db/state_db/sc/store_test.go b/sei-db/state_db/sc/store_test.go index a016d3aca0..812e8f58c6 100644 --- a/sei-db/state_db/sc/store_test.go +++ b/sei-db/state_db/sc/store_test.go @@ -70,7 +70,10 @@ func TestCommitStoreBasicOperations(t *testing.T) { // Load version 0 to initialize the DB _, err := cs.LoadVersion(0, false) require.NoError(t, err) - defer cs.Close() + defer func() { + err := cs.Close() + require.NoError(t, err) + }() // Initial version should be 0 require.Equal(t, int64(0), cs.Version()) @@ -110,7 +113,10 @@ func TestApplyChangeSetsEmpty(t *testing.T) { _, err := cs.LoadVersion(0, false) require.NoError(t, err) - defer cs.Close() + defer func() { + err := cs.Close() + require.NoError(t, err) + }() // Empty changesets should be no-op err = cs.ApplyChangeSets(nil) @@ -127,7 +133,10 @@ func TestApplyUpgrades(t *testing.T) { _, err := cs.LoadVersion(0, false) require.NoError(t, err) - defer cs.Close() + defer func() { + err := cs.Close() + require.NoError(t, err) + }() // Apply upgrades upgrades := []*proto.TreeNameUpgrade{ @@ -157,7 +166,10 @@ func TestApplyUpgradesEmpty(t *testing.T) { _, err := cs.LoadVersion(0, false) require.NoError(t, err) - defer cs.Close() + defer func() { + err := cs.Close() + require.NoError(t, err) + }() // Empty upgrades should be no-op err = cs.ApplyUpgrades(nil) @@ -212,7 +224,10 @@ func TestCommitInfo(t *testing.T) { _, err := cs.LoadVersion(0, false) require.NoError(t, err) - defer cs.Close() + defer func() { + err := cs.Close() + require.NoError(t, err) + }() // WorkingCommitInfo before any commit workingInfo := cs.WorkingCommitInfo() @@ -246,7 +261,10 @@ func TestGetModuleByName(t *testing.T) { _, err := cs.LoadVersion(0, false) require.NoError(t, err) - defer cs.Close() + defer func() { + err := cs.Close() + require.NoError(t, err) + }() // Get existing module module := cs.GetModuleByName("test") @@ -264,7 +282,10 @@ func TestExporterVersionValidation(t *testing.T) { _, err := cs.LoadVersion(0, false) require.NoError(t, err) - defer cs.Close() + defer func() { + err := cs.Close() + require.NoError(t, err) + }() // Negative version should fail _, err = cs.Exporter(-1) @@ -354,7 +375,10 @@ func TestMultipleCommits(t *testing.T) { _, err := cs.LoadVersion(0, false) require.NoError(t, err) - defer cs.Close() + defer func() { + err := cs.Close() + require.NoError(t, err) + }() // Multiple commits for i := 1; i <= 5; i++ { @@ -385,7 +409,10 @@ func TestCommitWithUpgradesAndChangesets(t *testing.T) { _, err := cs.LoadVersion(0, false) require.NoError(t, err) - defer cs.Close() + defer func() { + err := cs.Close() + require.NoError(t, err) + }() // Apply upgrades first err = cs.ApplyUpgrades([]*proto.TreeNameUpgrade{ @@ -423,7 +450,10 @@ func TestSetInitialVersion(t *testing.T) { _, err := cs.LoadVersion(0, false) require.NoError(t, err) - defer cs.Close() + defer func() { + err := cs.Close() + require.NoError(t, err) + }() // Set initial version err = cs.SetInitialVersion(100) @@ -472,7 +502,10 @@ func TestCreateWAL(t *testing.T) { _, err := cs.LoadVersion(0, false) require.NoError(t, err) - defer cs.Close() + defer func() { + err := cs.Close() + require.NoError(t, err) + }() // MemIAVL should have opened its changelog WAL. require.NotNil(t, cs.db.GetWAL()) diff --git a/sei-db/wal/wal_test.go b/sei-db/wal/wal_test.go index 701c370229..952d860f67 100644 --- a/sei-db/wal/wal_test.go +++ b/sei-db/wal/wal_test.go @@ -50,9 +50,10 @@ func TestOpenAndCorruptedTail(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - os.WriteFile(filepath.Join(dir, "00000000000000000001"), tc.logs, 0o600) + err := os.WriteFile(filepath.Join(dir, "00000000000000000001"), tc.logs, 0o600) + require.NoError(t, err) - _, err := wal.Open(dir, opts) + _, err = wal.Open(dir, opts) require.Equal(t, wal.ErrCorrupt, err) log, err := open(dir, opts) @@ -188,7 +189,8 @@ func TestOpenWithNilOptions(t *testing.T) { func TestTruncateAfter(t *testing.T) { changelog := prepareTestData(t) - defer changelog.Close() + err := changelog.Close() + require.NoError(t, err) // Verify we have 3 entries lastIndex, err := changelog.LastOffset() @@ -217,7 +219,8 @@ func TestTruncateAfter(t *testing.T) { func TestTruncateBefore(t *testing.T) { changelog := prepareTestData(t) - defer changelog.Close() + err := changelog.Close() + require.NoError(t, err) // Verify we have 3 entries starting at 1 firstIndex, err := changelog.FirstOffset() @@ -266,7 +269,7 @@ func TestCloseSyncMode(t *testing.T) { // Reopen and verify data persisted changelog2, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) require.NoError(t, err) - defer changelog2.Close() + t.Cleanup(func() { require.NoError(t, changelog2.Close()) }) lastIndex, err := changelog2.LastOffset() require.NoError(t, err) @@ -275,7 +278,7 @@ func TestCloseSyncMode(t *testing.T) { func TestReadAtNonExistent(t *testing.T) { changelog := prepareTestData(t) - defer changelog.Close() + t.Cleanup(func() { require.NoError(t, changelog.Close()) }) // Try to read an entry that doesn't exist _, err := changelog.ReadAt(100) @@ -284,7 +287,7 @@ func TestReadAtNonExistent(t *testing.T) { func TestReplayWithError(t *testing.T) { changelog := prepareTestData(t) - defer changelog.Close() + t.Cleanup(func() { require.NoError(t, changelog.Close()) }) // Replay with a function that returns an error expectedErr := fmt.Errorf("test error") @@ -342,7 +345,7 @@ func TestEmptyLog(t *testing.T) { dir := t.TempDir() changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) require.NoError(t, err) - defer changelog.Close() + t.Cleanup(func() { require.NoError(t, changelog.Close()) }) // Empty log should have 0 for both first and last index firstIndex, err := changelog.FirstOffset() @@ -371,7 +374,7 @@ func TestCheckErrorNoError(t *testing.T) { func TestFirstAndLastOffset(t *testing.T) { changelog := prepareTestData(t) - defer changelog.Close() + t.Cleanup(func() { require.NoError(t, changelog.Close()) }) firstIndex, err := changelog.FirstOffset() require.NoError(t, err) @@ -417,7 +420,7 @@ func TestAsyncWriteReopenAndContinue(t *testing.T) { // Reopen and verify all 6 entries changelog3, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) require.NoError(t, err) - defer changelog3.Close() + t.Cleanup(func() { require.NoError(t, changelog3.Close()) }) lastIndex, err := changelog3.LastOffset() require.NoError(t, err) @@ -426,7 +429,7 @@ func TestAsyncWriteReopenAndContinue(t *testing.T) { func TestReplaySingleEntry(t *testing.T) { changelog := prepareTestData(t) - defer changelog.Close() + t.Cleanup(func() { require.NoError(t, changelog.Close()) }) var count int err := changelog.Replay(2, 2, func(index uint64, entry proto.ChangelogEntry) error { @@ -442,7 +445,7 @@ func TestWriteMultipleChangesets(t *testing.T) { dir := t.TempDir() changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) require.NoError(t, err) - defer changelog.Close() + t.Cleanup(func() { require.NoError(t, changelog.Close()) }) // Write entry with multiple changesets entry := &proto.ChangelogEntry{ From 06dee5f8a717dca86dec1c16d3c1aa49bf2b7dd4 Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Wed, 14 Jan 2026 14:49:50 -0800 Subject: [PATCH 32/35] Add unit test for concurrency and deadlock test --- sei-db/db_engine/pebbledb/db_test.go | 33 +- sei-db/state_db/sc/memiavl/benchmark_test.go | 2 +- sei-db/state_db/sc/memiavl/db_rewrite_test.go | 2 +- sei-db/state_db/sc/memiavl/proof_test.go | 2 +- .../sc/memiavl/snapshot_methods_test.go | 8 +- sei-db/state_db/sc/memiavl/tree_test.go | 2 +- sei-db/wal/wal.go | 28 +- sei-db/wal/wal_test.go | 285 +++++++++++++++++- 8 files changed, 321 insertions(+), 41 deletions(-) diff --git a/sei-db/db_engine/pebbledb/db_test.go b/sei-db/db_engine/pebbledb/db_test.go index f29c61c75c..0a1ce5fdfe 100644 --- a/sei-db/db_engine/pebbledb/db_test.go +++ b/sei-db/db_engine/pebbledb/db_test.go @@ -6,6 +6,7 @@ import ( "github.com/cockroachdb/pebble" "github.com/sei-protocol/sei-chain/sei-db/db_engine" + "github.com/stretchr/testify/require" ) func TestDBGetSetDelete(t *testing.T) { @@ -14,7 +15,7 @@ func TestDBGetSetDelete(t *testing.T) { if err != nil { t.Fatalf("Open: %v", err) } - defer func() { _ = db.Close() }() + t.Cleanup(func() { require.NoError(t, db.Close()) }) key := []byte("k1") val := []byte("v1") @@ -52,10 +53,10 @@ func TestBatchAtomicWrite(t *testing.T) { if err != nil { t.Fatalf("Open: %v", err) } - defer func() { _ = db.Close() }() + t.Cleanup(func() { require.NoError(t, db.Close()) }) b := db.NewBatch() - defer func() { _ = b.Close() }() + t.Cleanup(func() { require.NoError(t, b.Close()) }) if err := b.Set([]byte("a"), []byte("1")); err != nil { t.Fatalf("batch set: %v", err) @@ -91,7 +92,7 @@ func TestIteratorBounds(t *testing.T) { if err != nil { t.Fatalf("Open: %v", err) } - defer func() { _ = db.Close() }() + t.Cleanup(func() { require.NoError(t, db.Close()) }) // Keys: a, b, c for _, k := range []string{"a", "b", "c"} { @@ -104,7 +105,7 @@ func TestIteratorBounds(t *testing.T) { if err != nil { t.Fatalf("NewIter: %v", err) } - defer func() { _ = itr.Close() }() + t.Cleanup(func() { require.NoError(t, itr.Close()) }) var keys []string for ok := itr.First(); ok && itr.Valid(); ok = itr.Next() { @@ -125,7 +126,7 @@ func TestIteratorPrev(t *testing.T) { if err != nil { t.Fatalf("Open: %v", err) } - defer func() { _ = db.Close() }() + t.Cleanup(func() { require.NoError(t, db.Close()) }) // Keys: a, b, c for _, k := range []string{"a", "b", "c"} { @@ -138,7 +139,7 @@ func TestIteratorPrev(t *testing.T) { if err != nil { t.Fatalf("NewIter: %v", err) } - defer func() { _ = itr.Close() }() + t.Cleanup(func() { require.NoError(t, itr.Close()) }) if !itr.Last() || !itr.Valid() { t.Fatalf("expected Last() to position iterator") @@ -190,7 +191,7 @@ func TestIteratorNextPrefixWithComparerSplit(t *testing.T) { if err != nil { t.Fatalf("Open: %v", err) } - defer func() { _ = db.Close() }() + t.Cleanup(func() { require.NoError(t, db.Close()) }) for _, k := range []string{"a/1", "a/2", "a/3", "b/1"} { if err := db.Set([]byte(k), []byte("x"), db_engine.WriteOptions{Sync: false}); err != nil { @@ -202,7 +203,7 @@ func TestIteratorNextPrefixWithComparerSplit(t *testing.T) { if err != nil { t.Fatalf("NewIter: %v", err) } - defer func() { _ = itr.Close() }() + t.Cleanup(func() { require.NoError(t, itr.Close()) }) if !itr.SeekGE([]byte("a/")) || !itr.Valid() { t.Fatalf("expected SeekGE(a/) to be valid") @@ -233,7 +234,7 @@ func TestErrNotFoundConsistency(t *testing.T) { if err != nil { t.Fatalf("Open: %v", err) } - defer func() { _ = db.Close() }() + t.Cleanup(func() { require.NoError(t, db.Close()) }) // Test that Get on missing key returns ErrNotFound _, err = db.Get([]byte("missing-key")) @@ -258,7 +259,7 @@ func TestGetReturnsCopy(t *testing.T) { if err != nil { t.Fatalf("Open: %v", err) } - defer func() { _ = db.Close() }() + t.Cleanup(func() { require.NoError(t, db.Close()) }) key := []byte("k") val := []byte("v") @@ -288,7 +289,7 @@ func TestBatchLenResetDelete(t *testing.T) { if err != nil { t.Fatalf("Open: %v", err) } - defer func() { _ = db.Close() }() + t.Cleanup(func() { require.NoError(t, db.Close()) }) // First, set a key so we can delete it if err := db.Set([]byte("to-delete"), []byte("val"), db_engine.WriteOptions{Sync: false}); err != nil { @@ -296,7 +297,7 @@ func TestBatchLenResetDelete(t *testing.T) { } b := db.NewBatch() - defer func() { _ = b.Close() }() + t.Cleanup(func() { require.NoError(t, b.Close()) }) // Record initial batch len (Pebble batch always has a header, so may not be 0) initialLen := b.Len() @@ -344,7 +345,7 @@ func TestIteratorSeekLTAndValue(t *testing.T) { if err != nil { t.Fatalf("Open: %v", err) } - defer func() { _ = db.Close() }() + t.Cleanup(func() { require.NoError(t, db.Close()) }) // Insert keys: a, b, c with values for _, kv := range []struct{ k, v string }{ @@ -361,7 +362,7 @@ func TestIteratorSeekLTAndValue(t *testing.T) { if err != nil { t.Fatalf("NewIter: %v", err) } - defer func() { _ = itr.Close() }() + t.Cleanup(func() { require.NoError(t, itr.Close()) }) // SeekLT("c") should position at "b" if !itr.SeekLT([]byte("c")) || !itr.Valid() { @@ -381,7 +382,7 @@ func TestFlush(t *testing.T) { if err != nil { t.Fatalf("Open: %v", err) } - defer func() { _ = db.Close() }() + t.Cleanup(func() { require.NoError(t, db.Close()) }) // Set some data if err := db.Set([]byte("flush-test"), []byte("val"), db_engine.WriteOptions{Sync: false}); err != nil { diff --git a/sei-db/state_db/sc/memiavl/benchmark_test.go b/sei-db/state_db/sc/memiavl/benchmark_test.go index 25fae1aa04..6c90a394f9 100644 --- a/sei-db/state_db/sc/memiavl/benchmark_test.go +++ b/sei-db/state_db/sc/memiavl/benchmark_test.go @@ -39,7 +39,7 @@ func BenchmarkRandomGet(b *testing.B) { require.NoError(b, err) snapshot, err := OpenSnapshot(snapshotDir, opts) require.NoError(b, err) - defer func() { _ = snapshot.Close() }() + b.Cleanup(func() { require.NoError(b, snapshot.Close()) }) b.Run("memiavl", func(b *testing.B) { require.Equal(b, targetValue, tree.Get(targetKey)) diff --git a/sei-db/state_db/sc/memiavl/db_rewrite_test.go b/sei-db/state_db/sc/memiavl/db_rewrite_test.go index e2ff7fd06e..062bbc8e80 100644 --- a/sei-db/state_db/sc/memiavl/db_rewrite_test.go +++ b/sei-db/state_db/sc/memiavl/db_rewrite_test.go @@ -81,7 +81,7 @@ func TestLoadMultiTreeWithPrefetchDisabled(t *testing.T) { db2, err := OpenDB(logger.NewNopLogger(), 0, opts) require.NoError(t, err) - defer db2.Close() + t.Cleanup(func() { require.NoError(t, db2.Close()) }) // Verify data is accessible tree := db2.TreeByName("test") diff --git a/sei-db/state_db/sc/memiavl/proof_test.go b/sei-db/state_db/sc/memiavl/proof_test.go index ceaa2bb2f5..b31f7c8b5c 100644 --- a/sei-db/state_db/sc/memiavl/proof_test.go +++ b/sei-db/state_db/sc/memiavl/proof_test.go @@ -48,7 +48,7 @@ func TestProofs(t *testing.T) { snapshot, err := OpenSnapshot(tmpDir, opts) require.NoError(t, err) ptree := NewFromSnapshot(snapshot, opts) - defer func() { _ = ptree.Close() }() + t.Cleanup(func() { require.NoError(t, ptree.Close()) }) proof, err = ptree.GetMembershipProof(tc.existKey) require.NoError(t, err) diff --git a/sei-db/state_db/sc/memiavl/snapshot_methods_test.go b/sei-db/state_db/sc/memiavl/snapshot_methods_test.go index a63835845f..50eca5dfee 100644 --- a/sei-db/state_db/sc/memiavl/snapshot_methods_test.go +++ b/sei-db/state_db/sc/memiavl/snapshot_methods_test.go @@ -24,7 +24,7 @@ func TestSnapshotLeaf(t *testing.T) { opts.FillDefaults() snapshot, err := OpenSnapshot(snapshotDir, opts) require.NoError(t, err) - defer snapshot.Close() + t.Cleanup(func() { require.NoError(t, snapshot.Close()) }) // Test Leaf method if snapshot.leavesLen() > 0 { @@ -49,7 +49,7 @@ func TestSnapshotScanNodes(t *testing.T) { opts.FillDefaults() snapshot, err := OpenSnapshot(snapshotDir, opts) require.NoError(t, err) - defer snapshot.Close() + t.Cleanup(func() { require.NoError(t, snapshot.Close()) }) // Test ScanNodes count := 0 @@ -77,7 +77,7 @@ func TestSnapshotKey(t *testing.T) { opts.FillDefaults() snapshot, err := OpenSnapshot(snapshotDir, opts) require.NoError(t, err) - defer snapshot.Close() + t.Cleanup(func() { require.NoError(t, snapshot.Close()) }) // Test Key method via scanning leaves if snapshot.leavesLen() > 0 { @@ -126,7 +126,7 @@ func TestPrefetchSnapshot(t *testing.T) { snapshot, err := OpenSnapshot(snapshotDir, opts) require.NoError(t, err) - defer snapshot.Close() + t.Cleanup(func() { require.NoError(t, snapshot.Close()) }) require.NotNil(t, snapshot) } diff --git a/sei-db/state_db/sc/memiavl/tree_test.go b/sei-db/state_db/sc/memiavl/tree_test.go index 04a9cd57e8..02f25e5c3a 100644 --- a/sei-db/state_db/sc/memiavl/tree_test.go +++ b/sei-db/state_db/sc/memiavl/tree_test.go @@ -259,7 +259,7 @@ func TestGetByIndex(t *testing.T) { snapshot, err := OpenSnapshot(dir, Options{}) require.NoError(t, err) ptree := NewFromSnapshot(snapshot, Options{ZeroCopy: true}) - defer func() { _ = ptree.Close() }() + t.Cleanup(func() { require.NoError(t, ptree.Close()) }) for i, pair := range changes.Pairs { idx, v := ptree.GetWithIndex(pair.Key) diff --git a/sei-db/wal/wal.go b/sei-db/wal/wal.go index 5987d071b3..9a538fb9a4 100644 --- a/sei-db/wal/wal.go +++ b/sei-db/wal/wal.go @@ -87,9 +87,9 @@ func NewWAL[T any]( // Whether the writes is in blocking or async manner depends on the buffer size. // For async writes, this also checks for any previous async write errors. func (walLog *WAL[T]) Write(entry T) error { + // Never hold walLog.mtx while doing a potentially-blocking send. Close() may run concurrently. walLog.mtx.Lock() defer walLog.mtx.Unlock() - if walLog.isClosed.Load() { return errors.New("wal is closed") } @@ -98,7 +98,6 @@ func (walLog *WAL[T]) Write(entry T) error { } writeBufferSize := walLog.config.WriteBufferSize if writeBufferSize > 0 { - // async write if walLog.writeChannel == nil { walLog.writeChannel = make(chan T, writeBufferSize) walLog.startAsyncWriteGoroutine() @@ -249,19 +248,22 @@ func (walLog *WAL[T]) StartPruning(keepRecent uint64, pruneInterval time.Duratio } func (walLog *WAL[T]) Close() error { - // Close should only be called once and run for once - if walLog.isClosed.CompareAndSwap(false, true) { - // Signal background goroutines to stop. - close(walLog.closeCh) - if walLog.writeChannel != nil { - close(walLog.writeChannel) - } - // Wait for all background goroutines (pruning + async write) to finish - walLog.wg.Wait() + // Close should only be called once. + if !walLog.isClosed.CompareAndSwap(false, true) { + return nil + } + walLog.mtx.Lock() + defer walLog.mtx.Unlock() + // Signal background goroutines to stop. + close(walLog.closeCh) + if walLog.writeChannel != nil { + close(walLog.writeChannel) walLog.writeChannel = nil - return walLog.log.Close() } - return nil + // Wait for all background goroutines (pruning + async write) to finish. + walLog.wg.Wait() + + return walLog.log.Close() } // recordAsyncWriteErr records the first async write error (non-blocking). diff --git a/sei-db/wal/wal_test.go b/sei-db/wal/wal_test.go index 952d860f67..0135d3529a 100644 --- a/sei-db/wal/wal_test.go +++ b/sei-db/wal/wal_test.go @@ -4,7 +4,10 @@ import ( "fmt" "os" "path/filepath" + "sync" + "sync/atomic" "testing" + "time" "github.com/stretchr/testify/require" "github.com/tidwall/wal" @@ -189,8 +192,7 @@ func TestOpenWithNilOptions(t *testing.T) { func TestTruncateAfter(t *testing.T) { changelog := prepareTestData(t) - err := changelog.Close() - require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, changelog.Close()) }) // Verify we have 3 entries lastIndex, err := changelog.LastOffset() @@ -219,8 +221,7 @@ func TestTruncateAfter(t *testing.T) { func TestTruncateBefore(t *testing.T) { changelog := prepareTestData(t) - err := changelog.Close() - require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, changelog.Close()) }) // Verify we have 3 entries starting at 1 firstIndex, err := changelog.FirstOffset() @@ -467,6 +468,282 @@ func TestWriteMultipleChangesets(t *testing.T) { require.Equal(t, "store3", readEntry.Changesets[2].Name) } +func TestConcurrentCloseWithInFlightAsyncWrites_NoPanicOrDeadlock(t *testing.T) { + dir := t.TempDir() + changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{WriteBufferSize: 8}) + require.NoError(t, err) + + // We intentionally do NOT use t.Cleanup here because we want to race Close() explicitly. + + var ( + stop atomic.Bool + panicOnce sync.Once + panicVal atomic.Value + ) + + writer := func() { + defer func() { + if r := recover(); r != nil { + panicOnce.Do(func() { panicVal.Store(r) }) + } + }() + for !stop.Load() { + entry := proto.ChangelogEntry{ + Changesets: []*proto.NamedChangeSet{{ + Name: "test", + Changeset: iavl.ChangeSet{Pairs: MockKVPairs("k", "v")}, + }}, + } + _ = changelog.Write(entry) // may return "wal is closed"; must not panic or deadlock + } + } + + var wg sync.WaitGroup + for i := 0; i < 8; i++ { + wg.Add(1) + go func() { defer wg.Done(); writer() }() + } + + // Give writers a moment to start pushing. + time.Sleep(20 * time.Millisecond) + + // Race: close while writes are potentially in-flight. + _ = changelog.Close() + + stop.Store(true) + + done := make(chan struct{}) + go func() { wg.Wait(); close(done) }() + select { + case <-done: + case <-time.After(3 * time.Second): + t.Fatalf("writers did not exit (possible deadlock)") + } + + if v := panicVal.Load(); v != nil { + t.Fatalf("panic during concurrent write/close: %v", v) + } +} + +func TestConcurrentAsyncWritesWithPruningAndTruncateBefore_NoDeadlockOrCorruption(t *testing.T) { + dir := t.TempDir() + changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{ + WriteBufferSize: 32, + KeepRecent: 50, + PruneInterval: 1 * time.Millisecond, + }) + require.NoError(t, err) + + var ( + stop atomic.Bool + panicOnce sync.Once + panicVal atomic.Value + ) + + // Writer goroutines. + var seq atomic.Uint64 + writeWorker := func() { + defer func() { + if r := recover(); r != nil { + panicOnce.Do(func() { panicVal.Store(r) }) + } + }() + for !stop.Load() { + n := seq.Add(1) + entry := proto.ChangelogEntry{ + Changesets: []*proto.NamedChangeSet{{ + Name: "test", + Changeset: iavl.ChangeSet{Pairs: MockKVPairs(fmt.Sprintf("k-%d", n), "v")}, + }}, + } + // Ignore errors (e.g. closed), but must not panic/deadlock. + _ = changelog.Write(entry) + if n >= 500 { + return + } + } + } + + // Truncation goroutine (front truncation only). + truncWorker := func() { + defer func() { + if r := recover(); r != nil { + panicOnce.Do(func() { panicVal.Store(r) }) + } + }() + ticker := time.NewTicker(2 * time.Millisecond) + defer ticker.Stop() + for !stop.Load() { + <-ticker.C + last, err := changelog.LastOffset() + if err != nil || last == 0 { + continue + } + // Try to keep last ~100 entries. + var pruneBefore uint64 + if last > 100 { + pruneBefore = last - 100 + } else { + pruneBefore = 1 + } + _ = changelog.TruncateBefore(pruneBefore) + } + } + + // Concurrent reader goroutine. + readWorker := func() { + defer func() { + if r := recover(); r != nil { + panicOnce.Do(func() { panicVal.Store(r) }) + } + }() + ticker := time.NewTicker(1 * time.Millisecond) + defer ticker.Stop() + for !stop.Load() { + <-ticker.C + first, err1 := changelog.FirstOffset() + last, err2 := changelog.LastOffset() + if err1 != nil || err2 != nil || first == 0 || last == 0 || first > last { + continue + } + // Read around the midpoint. + mid := first + (last-first)/2 + _, _ = changelog.ReadAt(mid) + } + } + + var writerWg sync.WaitGroup + for i := 0; i < 4; i++ { + writerWg.Add(1) + go func() { defer writerWg.Done(); writeWorker() }() + } + var bgWg sync.WaitGroup + bgWg.Add(1) + go func() { defer bgWg.Done(); truncWorker() }() + bgWg.Add(1) + go func() { defer bgWg.Done(); readWorker() }() + + writerWg.Wait() + stop.Store(true) + bgDone := make(chan struct{}) + go func() { bgWg.Wait(); close(bgDone) }() + select { + case <-bgDone: + case <-time.After(3 * time.Second): + t.Fatalf("background workers did not exit (possible deadlock)") + } + + require.NoError(t, changelog.Close()) + + if v := panicVal.Load(); v != nil { + t.Fatalf("panic during concurrent WAL operations: %v", v) + } + + // Reopen and ensure the remaining range is readable via Replay. + changelog2, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, changelog2.Close()) }) + + first, err := changelog2.FirstOffset() + require.NoError(t, err) + last, err := changelog2.LastOffset() + require.NoError(t, err) + require.True(t, first <= last, "invalid WAL index range: first=%d last=%d", first, last) + if last > 0 && first > 0 { + require.NoError(t, changelog2.Replay(first, last, func(index uint64, entry proto.ChangelogEntry) error { + // basic sanity: entries should be decodable and have expected structure + if len(entry.Changesets) > 0 && len(entry.Changesets[0].Changeset.Pairs) > 0 { + _ = entry.Changesets[0].Changeset.Pairs[0].Key + } + return nil + })) + } +} + +func TestConcurrentTruncateBeforeWithSyncWrites_DoesNotCorruptOrAffectWriteIndex(t *testing.T) { + dir := t.TempDir() + // Use sync writes to make the expected index deterministic (LastIndex+1 for each write). + changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, changelog.Close()) }) + + const ( + totalWrites = 300 + keepRecent = uint64(50) + ) + + stop := make(chan struct{}) + var truncPanic atomic.Value + go func() { + defer func() { + if r := recover(); r != nil { + truncPanic.Store(r) + } + }() + ticker := time.NewTicker(1 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-stop: + return + case <-ticker.C: + last, err := changelog.LastOffset() + if err != nil || last == 0 { + continue + } + var pruneBefore uint64 = 1 + if last > keepRecent { + pruneBefore = last - keepRecent + } + // Truncate front concurrently with writes. + _ = changelog.TruncateBefore(pruneBefore) + } + } + }() + + var lastSeen uint64 + for i := 1; i <= totalWrites; i++ { + entry := proto.ChangelogEntry{ + Changesets: []*proto.NamedChangeSet{{ + Name: "test", + Changeset: iavl.ChangeSet{Pairs: MockKVPairs(fmt.Sprintf("k-%d", i), "v")}, + }}, + } + require.NoError(t, changelog.Write(entry)) + + // The last offset should advance monotonically by exactly 1 per write + // because we only truncate the front (which doesn't change LastIndex). + last, err := changelog.LastOffset() + require.NoError(t, err) + require.Equal(t, uint64(i), last) + require.True(t, last > lastSeen, "last offset must be strictly increasing") + lastSeen = last + + // Sanity: the newest entry is readable and decodes correctly. + got, err := changelog.ReadAt(last) + require.NoError(t, err) + require.Len(t, got.Changesets, 1) + require.Equal(t, []byte(fmt.Sprintf("k-%d", i)), got.Changesets[0].Changeset.Pairs[0].Key) + } + + close(stop) + if v := truncPanic.Load(); v != nil { + t.Fatalf("panic during concurrent truncation: %v", v) + } + + // Reopen and ensure the remaining range is replayable. + changelog2, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, changelog2.Close()) }) + + first, err := changelog2.FirstOffset() + require.NoError(t, err) + last, err := changelog2.LastOffset() + require.NoError(t, err) + require.True(t, first <= last, "invalid WAL range after concurrent truncation") + require.NoError(t, changelog2.Replay(first, last, func(index uint64, entry proto.ChangelogEntry) error { return nil })) +} + func TestGetLastIndex(t *testing.T) { dir := t.TempDir() changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) From 08508a26694ed26393c67acac262c791bf6eb04f Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Thu, 15 Jan 2026 17:01:11 -0800 Subject: [PATCH 33/35] Simplify wal unit test --- sei-db/wal/wal_test.go | 325 ++++++++++++----------------------------- 1 file changed, 91 insertions(+), 234 deletions(-) diff --git a/sei-db/wal/wal_test.go b/sei-db/wal/wal_test.go index 0135d3529a..9bb26e7075 100644 --- a/sei-db/wal/wal_test.go +++ b/sei-db/wal/wal_test.go @@ -5,7 +5,6 @@ import ( "os" "path/filepath" "sync" - "sync/atomic" "testing" "time" @@ -468,240 +467,83 @@ func TestWriteMultipleChangesets(t *testing.T) { require.Equal(t, "store3", readEntry.Changesets[2].Name) } -func TestConcurrentCloseWithInFlightAsyncWrites_NoPanicOrDeadlock(t *testing.T) { +func TestConcurrentCloseWithInFlightAsyncWrites(t *testing.T) { dir := t.TempDir() changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{WriteBufferSize: 8}) require.NoError(t, err) - // We intentionally do NOT use t.Cleanup here because we want to race Close() explicitly. - - var ( - stop atomic.Bool - panicOnce sync.Once - panicVal atomic.Value - ) - - writer := func() { - defer func() { - if r := recover(); r != nil { - panicOnce.Do(func() { panicVal.Store(r) }) - } - }() - for !stop.Load() { - entry := proto.ChangelogEntry{ - Changesets: []*proto.NamedChangeSet{{ - Name: "test", - Changeset: iavl.ChangeSet{Pairs: MockKVPairs("k", "v")}, - }}, - } - _ = changelog.Write(entry) // may return "wal is closed"; must not panic or deadlock - } - } + // Intentionally avoid t.Cleanup here: we want Close() to race with in-flight async writes. + // Writers: keep calling Write() until it returns an error (which should happen once Close() starts). + // If Write() or Close() deadlocks, the test will time out waiting for the goroutines to exit. var wg sync.WaitGroup for i := 0; i < 8; i++ { wg.Add(1) - go func() { defer wg.Done(); writer() }() + go func() { + defer wg.Done() + for { + entry := proto.ChangelogEntry{ + Changesets: []*proto.NamedChangeSet{{ + Name: "test", + Changeset: iavl.ChangeSet{Pairs: MockKVPairs("k", "v")}, + }}, + } + if err := changelog.Write(entry); err != nil { + return + } + } + }() } - // Give writers a moment to start pushing. - time.Sleep(20 * time.Millisecond) - - // Race: close while writes are potentially in-flight. - _ = changelog.Close() + // Ensure we actually have in-flight async activity before closing. + require.Eventually(t, func() bool { + last, err := changelog.LastOffset() + return err == nil && last > 0 + }, 1*time.Second, 10*time.Millisecond, "expected some writes before Close()") - stop.Store(true) + closeDone := make(chan struct{}) + go func() { + _ = changelog.Close() // may return error depending on race; must not deadlock/panic + close(closeDone) + }() - done := make(chan struct{}) - go func() { wg.Wait(); close(done) }() - select { - case <-done: - case <-time.After(3 * time.Second): - t.Fatalf("writers did not exit (possible deadlock)") - } + // Wait for writers to observe Close() and exit. + writersDone := make(chan struct{}) + go func() { wg.Wait(); close(writersDone) }() + require.Eventually(t, func() bool { + select { + case <-writersDone: + return true + default: + return false + } + }, 3*time.Second, 10*time.Millisecond, "writers did not exit (possible deadlock)") - if v := panicVal.Load(); v != nil { - t.Fatalf("panic during concurrent write/close: %v", v) - } + // Ensure Close() returns too. + require.Eventually(t, func() bool { + select { + case <-closeDone: + return true + default: + return false + } + }, 3*time.Second, 10*time.Millisecond, "Close() did not return (possible deadlock)") } -func TestConcurrentAsyncWritesWithPruningAndTruncateBefore_NoDeadlockOrCorruption(t *testing.T) { +func TestConcurrentTruncateBeforeWithAsyncWrites(t *testing.T) { dir := t.TempDir() changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{ - WriteBufferSize: 32, - KeepRecent: 50, + WriteBufferSize: 10, + KeepRecent: 10, PruneInterval: 1 * time.Millisecond, }) require.NoError(t, err) - var ( - stop atomic.Bool - panicOnce sync.Once - panicVal atomic.Value - ) - - // Writer goroutines. - var seq atomic.Uint64 - writeWorker := func() { - defer func() { - if r := recover(); r != nil { - panicOnce.Do(func() { panicVal.Store(r) }) - } - }() - for !stop.Load() { - n := seq.Add(1) - entry := proto.ChangelogEntry{ - Changesets: []*proto.NamedChangeSet{{ - Name: "test", - Changeset: iavl.ChangeSet{Pairs: MockKVPairs(fmt.Sprintf("k-%d", n), "v")}, - }}, - } - // Ignore errors (e.g. closed), but must not panic/deadlock. - _ = changelog.Write(entry) - if n >= 500 { - return - } - } - } - - // Truncation goroutine (front truncation only). - truncWorker := func() { - defer func() { - if r := recover(); r != nil { - panicOnce.Do(func() { panicVal.Store(r) }) - } - }() - ticker := time.NewTicker(2 * time.Millisecond) - defer ticker.Stop() - for !stop.Load() { - <-ticker.C - last, err := changelog.LastOffset() - if err != nil || last == 0 { - continue - } - // Try to keep last ~100 entries. - var pruneBefore uint64 - if last > 100 { - pruneBefore = last - 100 - } else { - pruneBefore = 1 - } - _ = changelog.TruncateBefore(pruneBefore) - } - } - - // Concurrent reader goroutine. - readWorker := func() { - defer func() { - if r := recover(); r != nil { - panicOnce.Do(func() { panicVal.Store(r) }) - } - }() - ticker := time.NewTicker(1 * time.Millisecond) - defer ticker.Stop() - for !stop.Load() { - <-ticker.C - first, err1 := changelog.FirstOffset() - last, err2 := changelog.LastOffset() - if err1 != nil || err2 != nil || first == 0 || last == 0 || first > last { - continue - } - // Read around the midpoint. - mid := first + (last-first)/2 - _, _ = changelog.ReadAt(mid) - } - } - - var writerWg sync.WaitGroup - for i := 0; i < 4; i++ { - writerWg.Add(1) - go func() { defer writerWg.Done(); writeWorker() }() - } - var bgWg sync.WaitGroup - bgWg.Add(1) - go func() { defer bgWg.Done(); truncWorker() }() - bgWg.Add(1) - go func() { defer bgWg.Done(); readWorker() }() - - writerWg.Wait() - stop.Store(true) - bgDone := make(chan struct{}) - go func() { bgWg.Wait(); close(bgDone) }() - select { - case <-bgDone: - case <-time.After(3 * time.Second): - t.Fatalf("background workers did not exit (possible deadlock)") - } - - require.NoError(t, changelog.Close()) - - if v := panicVal.Load(); v != nil { - t.Fatalf("panic during concurrent WAL operations: %v", v) - } - - // Reopen and ensure the remaining range is readable via Replay. - changelog2, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) - require.NoError(t, err) - t.Cleanup(func() { require.NoError(t, changelog2.Close()) }) - - first, err := changelog2.FirstOffset() - require.NoError(t, err) - last, err := changelog2.LastOffset() - require.NoError(t, err) - require.True(t, first <= last, "invalid WAL index range: first=%d last=%d", first, last) - if last > 0 && first > 0 { - require.NoError(t, changelog2.Replay(first, last, func(index uint64, entry proto.ChangelogEntry) error { - // basic sanity: entries should be decodable and have expected structure - if len(entry.Changesets) > 0 && len(entry.Changesets[0].Changeset.Pairs) > 0 { - _ = entry.Changesets[0].Changeset.Pairs[0].Key - } - return nil - })) - } -} - -func TestConcurrentTruncateBeforeWithSyncWrites_DoesNotCorruptOrAffectWriteIndex(t *testing.T) { - dir := t.TempDir() - // Use sync writes to make the expected index deterministic (LastIndex+1 for each write). - changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) - require.NoError(t, err) - t.Cleanup(func() { require.NoError(t, changelog.Close()) }) - const ( - totalWrites = 300 - keepRecent = uint64(50) + totalWrites = 50 ) - stop := make(chan struct{}) - var truncPanic atomic.Value - go func() { - defer func() { - if r := recover(); r != nil { - truncPanic.Store(r) - } - }() - ticker := time.NewTicker(1 * time.Millisecond) - defer ticker.Stop() - for { - select { - case <-stop: - return - case <-ticker.C: - last, err := changelog.LastOffset() - if err != nil || last == 0 { - continue - } - var pruneBefore uint64 = 1 - if last > keepRecent { - pruneBefore = last - keepRecent - } - // Truncate front concurrently with writes. - _ = changelog.TruncateBefore(pruneBefore) - } - } - }() - - var lastSeen uint64 + // Write a bunch of entries (async writes). We'll wait until they're all persisted. for i := 1; i <= totalWrites; i++ { entry := proto.ChangelogEntry{ Changesets: []*proto.NamedChangeSet{{ @@ -710,38 +552,53 @@ func TestConcurrentTruncateBeforeWithSyncWrites_DoesNotCorruptOrAffectWriteIndex }}, } require.NoError(t, changelog.Write(entry)) + } - // The last offset should advance monotonically by exactly 1 per write - // because we only truncate the front (which doesn't change LastIndex). + // Ensure async writer has flushed to disk. + require.Eventually(t, func() bool { last, err := changelog.LastOffset() - require.NoError(t, err) - require.Equal(t, uint64(i), last) - require.True(t, last > lastSeen, "last offset must be strictly increasing") - lastSeen = last + return err == nil && last == uint64(totalWrites) + }, 3*time.Second, 10*time.Millisecond, "async writes did not flush") - // Sanity: the newest entry is readable and decodes correctly. - got, err := changelog.ReadAt(last) - require.NoError(t, err) - require.Len(t, got.Changesets, 1) - require.Equal(t, []byte(fmt.Sprintf("k-%d", i)), got.Changesets[0].Changeset.Pairs[0].Key) - } + // Let the background pruning goroutine run and advance FirstOffset. + require.Eventually(t, func() bool { + first, err := changelog.FirstOffset() + return err == nil && first > 1 + }, 3*time.Second, 10*time.Millisecond, "background pruning did not advance FirstOffset") - close(stop) - if v := truncPanic.Load(); v != nil { - t.Fatalf("panic during concurrent truncation: %v", v) - } + // Manual front truncation while pruning is enabled. + firstBefore, err := changelog.FirstOffset() + require.NoError(t, err) + last, err := changelog.LastOffset() + require.NoError(t, err) + require.True(t, firstBefore < last, "expected a non-empty range after writes") - // Reopen and ensure the remaining range is replayable. - changelog2, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) + require.NoError(t, changelog.TruncateBefore(firstBefore+1)) + require.Eventually(t, func() bool { + first, err := changelog.FirstOffset() + return err == nil && first >= firstBefore+1 + }, 3*time.Second, 10*time.Millisecond, "manual truncation did not take effect") + + // Read first + last entries to ensure no corruption (decode succeeds; expected structure). + first, err := changelog.FirstOffset() require.NoError(t, err) - t.Cleanup(func() { require.NoError(t, changelog2.Close()) }) + last, err = changelog.LastOffset() + require.NoError(t, err) + require.True(t, first <= last, "invalid WAL range after pruning/truncation") - first, err := changelog2.FirstOffset() + firstEntry, err := changelog.ReadAt(first) require.NoError(t, err) - last, err := changelog2.LastOffset() + require.NotEmpty(t, firstEntry.Changesets) + require.Equal(t, "test", firstEntry.Changesets[0].Name) + require.NotEmpty(t, firstEntry.Changesets[0].Changeset.Pairs) + + lastEntry, err := changelog.ReadAt(last) require.NoError(t, err) - require.True(t, first <= last, "invalid WAL range after concurrent truncation") - require.NoError(t, changelog2.Replay(first, last, func(index uint64, entry proto.ChangelogEntry) error { return nil })) + require.NotEmpty(t, lastEntry.Changesets) + require.Equal(t, "test", lastEntry.Changesets[0].Name) + require.NotEmpty(t, lastEntry.Changesets[0].Changeset.Pairs) + + require.NoError(t, changelog.Close()) } func TestGetLastIndex(t *testing.T) { From f10fb45b0cf2c8ab238f62fe82e4e85599916aab Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Thu, 15 Jan 2026 17:17:26 -0800 Subject: [PATCH 34/35] Fix ignore error --- sei-db/wal/wal_test.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/sei-db/wal/wal_test.go b/sei-db/wal/wal_test.go index 9bb26e7075..ca30eaeb0b 100644 --- a/sei-db/wal/wal_test.go +++ b/sei-db/wal/wal_test.go @@ -116,11 +116,11 @@ func prepareTestData(t *testing.T) *WAL[proto.ChangelogEntry] { dir := t.TempDir() changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) require.NoError(t, err) - writeTestData(changelog) + writeTestData(t, changelog) return changelog } -func writeTestData(changelog *WAL[proto.ChangelogEntry]) { +func writeTestData(t *testing.T, changelog *WAL[proto.ChangelogEntry]) { for _, changes := range ChangeSets { cs := []*proto.NamedChangeSet{ { @@ -130,7 +130,7 @@ func writeTestData(changelog *WAL[proto.ChangelogEntry]) { } entry := proto.ChangelogEntry{} entry.Changesets = cs - _ = changelog.Write(entry) + require.NoError(t, changelog.Write(entry)) } } @@ -257,7 +257,7 @@ func TestCloseSyncMode(t *testing.T) { require.NoError(t, err) // Write some data in sync mode - writeTestData(changelog) + writeTestData(t, changelog) // Close the changelog err = changelog.Close() @@ -307,7 +307,7 @@ func TestReopenAndContinueWrite(t *testing.T) { // Create and write initial data changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) require.NoError(t, err) - writeTestData(changelog) + writeTestData(t, changelog) err = changelog.Close() require.NoError(t, err) @@ -502,8 +502,9 @@ func TestConcurrentCloseWithInFlightAsyncWrites(t *testing.T) { }, 1*time.Second, 10*time.Millisecond, "expected some writes before Close()") closeDone := make(chan struct{}) + closeErr := make(chan error, 1) go func() { - _ = changelog.Close() // may return error depending on race; must not deadlock/panic + closeErr <- changelog.Close() close(closeDone) }() @@ -528,6 +529,8 @@ func TestConcurrentCloseWithInFlightAsyncWrites(t *testing.T) { return false } }, 3*time.Second, 10*time.Millisecond, "Close() did not return (possible deadlock)") + + require.NoError(t, <-closeErr) } func TestConcurrentTruncateBeforeWithAsyncWrites(t *testing.T) { @@ -605,7 +608,7 @@ func TestGetLastIndex(t *testing.T) { dir := t.TempDir() changelog, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{}) require.NoError(t, err) - writeTestData(changelog) + writeTestData(t, changelog) err = changelog.Close() require.NoError(t, err) From a387f134dc002db9ff58ec81e4a2c7d5b77f114e Mon Sep 17 00:00:00 2001 From: yzang2019 Date: Thu, 15 Jan 2026 18:14:10 -0800 Subject: [PATCH 35/35] Addree more comments --- sei-db/wal/wal.go | 29 ++++++++++++----------------- sei-db/wal/wal_test.go | 2 +- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/sei-db/wal/wal.go b/sei-db/wal/wal.go index 9a538fb9a4..a1cd79ff84 100644 --- a/sei-db/wal/wal.go +++ b/sei-db/wal/wal.go @@ -6,7 +6,6 @@ import ( "os" "path/filepath" "sync" - "sync/atomic" "time" "github.com/tidwall/wal" @@ -23,9 +22,9 @@ type WAL[T any] struct { marshal MarshalFn[T] unmarshal UnmarshalFn[T] writeChannel chan T - mtx sync.RWMutex - asyncWriteErrCh chan error // buffered=1; async writer reports first error non-blocking - isClosed atomic.Bool + mtx sync.RWMutex // guards WAL state: lazy init/close of writeChannel, isClosed checks + asyncWriteErrCh chan error // buffered=1; async writer reports first error non-blocking + isClosed bool closeCh chan struct{} // signals shutdown to background goroutines wg sync.WaitGroup // tracks background goroutines (pruning) } @@ -72,12 +71,11 @@ func NewWAL[T any]( unmarshal: unmarshal, closeCh: make(chan struct{}), asyncWriteErrCh: make(chan error, 1), - isClosed: atomic.Bool{}, } // Start the auto pruning goroutine if config.KeepRecent > 0 && config.PruneInterval > 0 { - w.StartPruning(config.KeepRecent, config.PruneInterval) + w.startPruning(config.KeepRecent, config.PruneInterval) } return w, nil @@ -90,7 +88,7 @@ func (walLog *WAL[T]) Write(entry T) error { // Never hold walLog.mtx while doing a potentially-blocking send. Close() may run concurrently. walLog.mtx.Lock() defer walLog.mtx.Unlock() - if walLog.isClosed.Load() { + if walLog.isClosed { return errors.New("wal is closed") } if err := walLog.getAsyncWriteErrLocked(); err != nil { @@ -152,10 +150,7 @@ func (walLog *WAL[T]) startAsyncWriteGoroutine() { // TruncateAfter will remove all entries that are after the provided `index`. // In other words the entry at `index` becomes the last entry in the log. func (walLog *WAL[T]) TruncateAfter(index uint64) error { - if err := walLog.log.TruncateBack(index); err != nil { - return err - } - return nil + return walLog.log.TruncateBack(index) } // TruncateBefore will remove all entries that are before the provided `index`. @@ -215,7 +210,7 @@ func (walLog *WAL[T]) Replay(start uint64, end uint64, processFn func(index uint return nil } -func (walLog *WAL[T]) StartPruning(keepRecent uint64, pruneInterval time.Duration) { +func (walLog *WAL[T]) startPruning(keepRecent uint64, pruneInterval time.Duration) { walLog.wg.Add(1) go func() { defer walLog.wg.Done() @@ -248,12 +243,12 @@ func (walLog *WAL[T]) StartPruning(keepRecent uint64, pruneInterval time.Duratio } func (walLog *WAL[T]) Close() error { - // Close should only be called once. - if !walLog.isClosed.CompareAndSwap(false, true) { - return nil - } walLog.mtx.Lock() defer walLog.mtx.Unlock() + // Close should only be executed once. + if walLog.isClosed { + return nil + } // Signal background goroutines to stop. close(walLog.closeCh) if walLog.writeChannel != nil { @@ -262,7 +257,7 @@ func (walLog *WAL[T]) Close() error { } // Wait for all background goroutines (pruning + async write) to finish. walLog.wg.Wait() - + walLog.isClosed = true return walLog.log.Close() } diff --git a/sei-db/wal/wal_test.go b/sei-db/wal/wal_test.go index ca30eaeb0b..76eaaf268b 100644 --- a/sei-db/wal/wal_test.go +++ b/sei-db/wal/wal_test.go @@ -264,7 +264,7 @@ func TestCloseSyncMode(t *testing.T) { require.NoError(t, err) // Verify isClosed is set - require.True(t, changelog.isClosed.Load()) + require.True(t, changelog.isClosed) // Reopen and verify data persisted changelog2, err := NewWAL(marshalEntry, unmarshalEntry, logger.NewNopLogger(), dir, Config{})