Summary
MDBX GC can only reclaim freed pages when openTxs=1 at commit time (only the committing RW tx itself). Any RO tx opened before the RW tx holds an older snapshot, preventing the GC from advancing cached_oldest and reclaiming pages from prior transactions. This causes unbounded DB file growth.
RO txs opened after BeginTemporalRw see the same snapshot (txnid N) as the RW tx and do NOT block GC — they don't hold older snapshots.
Proposed API Changes
BeginTemporalRw(ctx context.Context, drainReaders ...bool) (TemporalRwTx, error)
When drainReaders=true:
- Acquire exclusive write lock on a shared
sync.RWMutex (the "commit gate")
- Call the underlying
BeginRw to create the MDBX write transaction
- Release the write lock immediately
This drains all in-flight background RO txs that hold older snapshots before the RW tx is created. New RO txs can open immediately after — they see the same snapshot as the RW tx and won't block GC.
TryBeginTemporalRo(ctx context.Context) (TemporalRoTx, bool, error)
Attempts commitGate.TryRLock(). If the gate is held by a committing writer, returns (nil, false, nil) instead of blocking. Callers can return cached/stale data.
TryView(ctx context.Context, f func(tx Tx) error) (bool, error)
Same as View but uses TryRLock. Returns (false, nil) if a commit is in progress, allowing the caller to skip the read without blocking.
Background
The commit gate is an sync.RWMutex:
- Background readers (collation
db.View(), sentry status, etc.) hold RLock — multiple can run concurrently
- Commit paths set
drainReaders=true on BeginTemporalRw — waits for existing RLock holders to finish, then creates the RW tx and releases immediately
- Lock hold time is microseconds (just the
BeginRw call), not the full commit duration
Without this, background collation and sentry status db.View() calls open RO txs that overlap with commits, keeping openTxs > 1 and preventing MDBX GC page reclamation — leading to unbounded DB growth (observed: 8 GB → 526 GB overnight).
Implementation Notes
- The
sync.RWMutex lives on the Aggregator and is exposed via CommitGate() *sync.RWMutex
TryBeginTemporalRo / TryView are opt-in for background readers that can tolerate stale data
- Critical readers that MUST have fresh data continue using
BeginTemporalRo / View (blocking RLock)
- The temporal DB layer (
kv_temporal.go) implements the gate internally — callers don't manage locks directly
🤖 Generated with Claude Code
Summary
MDBX GC can only reclaim freed pages when
openTxs=1at commit time (only the committing RW tx itself). Any RO tx opened before the RW tx holds an older snapshot, preventing the GC from advancingcached_oldestand reclaiming pages from prior transactions. This causes unbounded DB file growth.RO txs opened after
BeginTemporalRwsee the same snapshot (txnid N) as the RW tx and do NOT block GC — they don't hold older snapshots.Proposed API Changes
BeginTemporalRw(ctx context.Context, drainReaders ...bool) (TemporalRwTx, error)When
drainReaders=true:sync.RWMutex(the "commit gate")BeginRwto create the MDBX write transactionThis drains all in-flight background RO txs that hold older snapshots before the RW tx is created. New RO txs can open immediately after — they see the same snapshot as the RW tx and won't block GC.
TryBeginTemporalRo(ctx context.Context) (TemporalRoTx, bool, error)Attempts
commitGate.TryRLock(). If the gate is held by a committing writer, returns(nil, false, nil)instead of blocking. Callers can return cached/stale data.TryView(ctx context.Context, f func(tx Tx) error) (bool, error)Same as
Viewbut usesTryRLock. Returns(false, nil)if a commit is in progress, allowing the caller to skip the read without blocking.Background
The commit gate is an
sync.RWMutex:db.View(), sentry status, etc.) holdRLock— multiple can run concurrentlydrainReaders=trueonBeginTemporalRw— waits for existingRLockholders to finish, then creates the RW tx and releases immediatelyBeginRwcall), not the full commit durationWithout this, background collation and sentry status
db.View()calls open RO txs that overlap with commits, keepingopenTxs > 1and preventing MDBX GC page reclamation — leading to unbounded DB growth (observed: 8 GB → 526 GB overnight).Implementation Notes
sync.RWMutexlives on theAggregatorand is exposed viaCommitGate() *sync.RWMutexTryBeginTemporalRo/TryVieware opt-in for background readers that can tolerate stale dataBeginTemporalRo/View(blockingRLock)kv_temporal.go) implements the gate internally — callers don't manage locks directly🤖 Generated with Claude Code