Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f6c3652
execution/cache: test unwind fill-readmission window
yperbasis Aug 6, 2026
975b518
execution/cache, db/state/execctx: reject stale unwind fills
yperbasis Aug 7, 2026
0c14b98
execution/cache: clarify read-view epoch semantics
yperbasis Aug 7, 2026
c60c003
Merge remote-tracking branch 'origin/main' into yperbasis/statecache-…
yperbasis Aug 7, 2026
4370502
db/state: reject late stale cache fills after unwind
yperbasis Aug 7, 2026
a4a2af9
db/state/execctx: tolerate the nil placeholder tx in cacheFrontierFor
yperbasis Aug 7, 2026
d3ce694
db/state: validate overlay cache fills against backing tx
yperbasis Aug 12, 2026
9de861c
Merge remote-tracking branch 'origin/main' into yperbasis/statecache-…
yperbasis Aug 12, 2026
3658ffa
execution/cache, db/state: close staged-unwind fill window
yperbasis Aug 12, 2026
89169c5
execution/cache: validate fill authority when views bind
yperbasis Aug 12, 2026
ca98680
db/kv/membatchwithdb: flush only changed sequences
yperbasis Aug 12, 2026
992265a
db/state/execctx: honor unwind bounds in code lookup
yperbasis Aug 12, 2026
4e4878a
db/state/execctx: preserve fills after state commit
yperbasis Aug 12, 2026
09e6216
db/state/execctx: retry cache state version reads
yperbasis Aug 12, 2026
1b47021
execution/exec, execmodule: quiesce read-ahead across unwinds
yperbasis Aug 12, 2026
e4b570e
execution/cache, db/state: clean up unwind cache tests and docs
yperbasis Aug 12, 2026
db28c7a
execution, db/state: centralize state version advancement
yperbasis Aug 12, 2026
009990d
db/kv/membatchwithdb: lazily overlay sequences
yperbasis Aug 12, 2026
0b77ca6
execution/cache: keep view binding live during publication
yperbasis Aug 12, 2026
3ef99ed
db/state/execctx: preserve cache unwind across merge
yperbasis Aug 12, 2026
94cf65b
execution/cache: test stale fills through applier
yperbasis Aug 12, 2026
93f712b
execution/cache: clarify read view fill contract
yperbasis Aug 12, 2026
6a72e4c
execution/cache: share state version advance check
yperbasis Aug 12, 2026
0b3c9f7
db/state/execctx: group staged cache unwind state
yperbasis Aug 12, 2026
bb2c92e
db/state/execctx: close state caches in tests
yperbasis Aug 12, 2026
7d1b795
execution/cache: clarify read view publication docs
yperbasis Aug 12, 2026
64ab1f7
db/state/execctx: require known base state version
yperbasis Aug 12, 2026
2d1cf7e
execution/cache: benchmarks for publication under concurrent readers
AskAlexSharov Aug 13, 2026
fc35f2b
Merge remote-tracking branch 'origin/main' into yperbasis/statecache-…
yperbasis Aug 13, 2026
452aab4
db/kv/membatchwithdb: use plain tx for overlay reads
yperbasis Aug 13, 2026
3105099
execution/execmodule: guard validation unwind from read-ahead
yperbasis Aug 13, 2026
4c3a6b3
execution/cache: preserve unwind on rejected publication
yperbasis Aug 13, 2026
7386067
execution/cache, db/state/execctx: avoid retrying stale cache bindings
yperbasis Aug 13, 2026
eb41519
execution: make read-ahead suspension cancellable
yperbasis Aug 13, 2026
ba03dd8
db: harden detached sequence access
yperbasis Aug 13, 2026
85d8925
db/state/execctx: clarify cache publication comments
yperbasis Aug 13, 2026
e932177
execution/cache: keep raw mutation helpers test-only
yperbasis Aug 13, 2026
4da24f1
db/state/execctx: enforce unwind bounds on parent reads
yperbasis Aug 13, 2026
d52c260
execution/execmodule: clarify notification state version ownership
yperbasis Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 51 additions & 43 deletions db/kv/membatchwithdb/memory_mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package membatchwithdb
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"sync"
"time"
Expand Down Expand Up @@ -55,6 +56,7 @@ type MemoryMutation struct {
deletedEntries map[string]map[string]struct{}
deletedDups map[string]map[string]map[string]struct{}
clearedTables map[string]struct{}
readTx kv.Tx
db kv.TemporalTx
statelessCursors map[string]kv.RwCursor
DomainReader DomainReader
Expand All @@ -72,12 +74,10 @@ type MemoryMutation struct {
func NewMemoryBatch(tx kv.TemporalTx, tmpDir string, logger log.Logger) (*MemoryMutation, error) {
mem := newMemStore()
memDB := &memStoreDB{store: mem}
if err := initSequences(tx, mem); err != nil {
return nil, fmt.Errorf("NewMemoryBatch: init sequences: %w", err)
}

return &MemoryMutation{
mu: &sync.RWMutex{},
readTx: tx,
db: tx,
memDb: memDB,
memTx: mem,
Expand All @@ -101,13 +101,9 @@ func NewMemoryBatchMDBX(tx kv.TemporalTx, tmpDir string, logger log.Logger) (mm
if err != nil {
return nil, fmt.Errorf("NewMemoryBatchMDBX: begin tx: %w", err)
}
if err = initSequences(tx, memTx); err != nil {
memTx.Rollback()
return nil, fmt.Errorf("NewMemoryBatchMDBX: init sequences: %w", err)
}

return &MemoryMutation{
mu: &sync.RWMutex{},
readTx: tx,
db: tx,
memDb: tmpDB,
memTx: memTx,
Expand Down Expand Up @@ -137,6 +133,7 @@ func (m *MemoryMutation) Pin() kv.TemporalFilesPin {
func (m *MemoryMutation) UpdateTxn(tx kv.TemporalTx) {
m.mu.Lock()
defer m.mu.Unlock()
m.readTx = tx
m.db = tx
m.statelessCursors = nil
}
Expand All @@ -145,10 +142,13 @@ func (m *MemoryMutation) UpdateTxn(tx kv.TemporalTx) {
// is a pure in-memory structure with no external resources — Close/Rollback
// only frees the in-memory memDb. This makes the overlay safe to publish via
// Events for concurrent RPC reads (consumers create ReadViews with their own tx).
// Untouched sequences also require a read view because the overlay stores only
// explicit sequence changes.
func (m *MemoryMutation) DetachDB() kv.TemporalTx {
m.mu.Lock()
defer m.mu.Unlock()
db := m.db
m.readTx = nil
m.db = nil
m.statelessCursors = nil
return db
Expand Down Expand Up @@ -185,33 +185,20 @@ func (m *MemoryMutation) DBSize() (uint64, error) {
panic("not implemented")
}

func initSequences(db kv.Tx, memTx kv.RwTx) error {
cursor, err := db.Cursor(kv.Sequence)
if err != nil {
return err
}
defer cursor.Close()
for k, v, err := cursor.First(); k != nil; k, v, err = cursor.Next() {
if err != nil {
return err
}
if err := memTx.Put(kv.Sequence, k, v); err != nil {
return err
}
}
return nil
}

func (m *MemoryMutation) IncrementSequence(bucket string, amount uint64) (uint64, error) {
m.mu.Lock()
defer m.mu.Unlock()
return m.memTx.IncrementSequence(bucket, amount)
current, err := m.readSequenceLocked(bucket)
if err != nil || amount == 0 {
return current, err
}
return current, m.memTx.ResetSequence(bucket, current+amount)
}

func (m *MemoryMutation) ReadSequence(bucket string) (uint64, error) {
m.mu.RLock()
defer m.mu.RUnlock()
return m.memTx.ReadSequence(bucket)
return m.readSequenceLocked(bucket)
}

func (m *MemoryMutation) ResetSequence(bucket string, newValue uint64) error {
Expand All @@ -220,6 +207,27 @@ func (m *MemoryMutation) ResetSequence(bucket string, newValue uint64) error {
return m.memTx.ResetSequence(bucket, newValue)
}

func (m *MemoryMutation) readSequenceLocked(bucket string) (uint64, error) {
key := []byte(bucket)
value, err := m.memTx.GetOne(kv.Sequence, key)
if err != nil {
return 0, err
}
if value != nil {
if len(value) == 0 {
return 0, nil
}
return binary.BigEndian.Uint64(value), nil
}
if m.isTableCleared(kv.Sequence) || m.isEntryDeleted(kv.Sequence, key) {
return 0, nil
}
if m.readTx == nil {
return 0, fmt.Errorf("read sequence %q: no backing transaction is attached", bucket)
}
return m.readTx.ReadSequence(bucket)
}

func (m *MemoryMutation) ForAmount(bucket string, prefix []byte, amount uint32, walker func(k, v []byte) error) error {
if amount == 0 {
return nil
Expand Down Expand Up @@ -285,11 +293,11 @@ func (m *MemoryMutation) GetOne(table string, key []byte) ([]byte, error) {
if v != nil {
return v, nil
}
// Fall back to underlying DB (nil when overlay is detached for publishing).
if m.db == nil {
// Fall back to the caller's transaction (nil on an unbound detached overlay).
if m.readTx == nil {
return nil, nil
}
return m.db.GetOne(table, key)
return m.readTx.GetOne(table, key)
}

func (m *MemoryMutation) Last(table string) ([]byte, []byte, error) {
Expand All @@ -312,10 +320,10 @@ func (m *MemoryMutation) Has(table string, key []byte) (bool, error) {
if err != nil || has {
return has, err
}
if m.db == nil {
if m.readTx == nil {
return false, nil
}
return m.db.Has(table, key)
return m.readTx.Has(table, key)
}

func (m *MemoryMutation) Put(table string, k, v []byte) error {
Expand Down Expand Up @@ -382,8 +390,8 @@ func (m *MemoryMutation) StreamDescend(table string, fromPrefix, toPrefix []byte
func (m *MemoryMutation) Range(table string, fromPrefix, toPrefix []byte, asc order.By, limit int) (stream.KV, error) {
s := &rangeIter{orderAscend: bool(asc), limit: int64(limit)}
var err error
if m.db != nil {
if s.iterDb, err = m.db.Range(table, fromPrefix, toPrefix, asc, limit); err != nil {
if m.readTx != nil {
if s.iterDb, err = m.readTx.Range(table, fromPrefix, toPrefix, asc, limit); err != nil {
return s, err
}
}
Expand Down Expand Up @@ -467,8 +475,8 @@ func (s *rangeIter) Next() (k, v []byte, err error) {
func (m *MemoryMutation) RangeDupSort(table string, key []byte, fromPrefix, toPrefix []byte, asc order.By, limit int) (stream.KV, error) {
s := &rangeDupSortIter{key: key, orderAscend: bool(asc), limit: int64(limit)}
var err error
if m.db != nil {
if s.iterDb, err = m.db.RangeDupSort(table, key, fromPrefix, toPrefix, asc, limit); err != nil {
if m.readTx != nil {
if s.iterDb, err = m.readTx.RangeDupSort(table, key, fromPrefix, toPrefix, asc, limit); err != nil {
return s, err
}
}
Expand Down Expand Up @@ -887,8 +895,8 @@ func (m *MemoryMutation) makeCursor(bucket string) (kv.RwCursorDupSort, error) {
c.table = bucket

var err error
if m.db != nil {
c.cursor, err = m.db.CursorDupSort(bucket) //nolint:gocritic
if m.readTx != nil {
c.cursor, err = m.readTx.CursorDupSort(bucket) //nolint:gocritic
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -930,7 +938,7 @@ func (m *MemoryMutation) ApplyRw(_ context.Context, f func(tx kv.RwTx) error) er
}

func (m *MemoryMutation) ViewID() uint64 {
return m.db.ViewID()
return m.readTx.ViewID()
}

func (m *MemoryMutation) CHandle() unsafe.Pointer {
Expand Down Expand Up @@ -1071,10 +1079,9 @@ func (m *MemoryMutation) Unwind(ctx context.Context, txNumUnwindTo uint64, chang
}

// NewReadView creates a lightweight read-only view of this overlay backed by
// the given tx for fallback reads. The view shares the same in-memory data
// (memTx, deletedEntries, clearedTables) and the parent's mutex, but has its
// own db field set to the caller's tx. All existing cursor/read logic works
// naturally — memTx first, then db fallback.
// the given tx for fallback reads. The view shares the in-memory data and the
// parent's mutex, but uses the caller's tx for its own backing reads. Temporal
// methods also use it when it implements kv.TemporalTx.
//
// The returned kv.TemporalTx only exposes read methods. Callers cannot write
// to the overlay through this view. The caller must not Close the returned
Expand All @@ -1097,6 +1104,7 @@ func (m *MemoryMutation) newReadViewMut(tx kv.Tx) *MemoryMutation {
deletedEntries: m.deletedEntries,
deletedDups: m.deletedDups,
clearedTables: m.clearedTables,
readTx: tx,
db: dbTx,
DomainReader: m.DomainReader,
}
Expand Down
Loading
Loading