Skip to content

Conversation

@flemzord
Copy link
Member

@flemzord flemzord commented Feb 6, 2026

Summary

Backport of #1250 onto release/v2.3.

  • Add CountLedgersInBucket to the system store interface and implementation
  • Replace the dynamic _system.ledgers subquery in newScopedSelect() with a pre-computed isAloneInBucket boolean flag
  • Set the flag at store creation (CreateLedger) and opening (OpenLedger) time
  • Add safety comment about cache invalidation for the flag

This avoids degraded PostgreSQL query plans when a ledger is alone in its bucket, and guards against potential cross-ledger reads if the store is cached without proper invalidation.

Files changed

  • internal/storage/system/store.go — new CountLedgersInBucket method
  • internal/storage/driver/store.goSystemStore interface update
  • internal/storage/driver/driver.go — flag initialization in CreateLedger and OpenLedger
  • internal/storage/ledger/store.goisAloneInBucket field, simplified newScopedSelect(), SetAloneInBucket method
  • internal/storage/driver/store_generated_test.go — regenerated mock

Test plan

  • go build ./... passes
  • go test -tags it ./internal/storage/ledger ./internal/storage/driver (requires Postgres)

Replace the dynamic _system.ledgers subquery in newScopedSelect() with a
pre-computed isAloneInBucket boolean flag. The flag is set at store
creation/opening time via CountLedgersInBucket, avoiding degraded
PostgreSQL query plans when a ledger is alone in its bucket.

Backport of #1250.
@flemzord flemzord requested a review from a team as a code owner February 6, 2026 10:42
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 6, 2026

Walkthrough

The changes introduce a bucket-level optimization that detects when a ledger is the sole member of its bucket. When creating or opening a ledger, the system counts ledgers in the bucket and sets a flag. The ledger store subsequently uses this flag to simplify database query construction by skipping unnecessary ledger filters.

Changes

Cohort / File(s) Summary
System & Driver Storage Layer
internal/storage/system/store.go, internal/storage/driver/store.go
Added CountLedgersInBucket method to interfaces and implemented it in DefaultStore with SQL query against the system ledgers table filtered by bucket.
Driver Usage
internal/storage/driver/driver.go
Integrated bucket ledger counting into CreateLedger and OpenLedger to detect and flag when a ledger is alone in its bucket.
Ledger Query Optimization
internal/storage/ledger/store.go
Introduced isAloneInBucket field and SetAloneInBucket setter; simplified newScopedSelect to skip ledger filtering when the flag is true.
Test Mocks
internal/storage/driver/store_generated_test.go
Extended MockSystemStore with CountLedgersInBucket method and corresponding call recorder for test verification.

Sequence Diagram(s)

sequenceDiagram
    participant Driver as Driver Layer
    participant SystemStore as System Store
    participant LedgerStore as Ledger Store
    participant DB as Database

    Driver->>SystemStore: CountLedgersInBucket(ctx, bucket)
    SystemStore->>DB: SELECT COUNT(*) FROM _system.ledgers WHERE bucket = ?
    DB-->>SystemStore: count
    SystemStore-->>Driver: count
    Driver->>LedgerStore: SetAloneInBucket(count == 1)
    LedgerStore->>LedgerStore: isAloneInBucket = true/false
    
    Note over LedgerStore: Future queries in newScopedSelect<br/>skip ledger filter if isAloneInBucket
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A ledger counts its bucket-mates with care,
When alone, no filter fills the air—
Query speeds along the path so clear,
One hop, one flag, optimization's here! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(storage): scope ledger queries with bucket hint' accurately describes the main change—a storage optimization that scopes ledger queries using a bucket hint mechanism.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the optimization's purpose, implementation details, affected files, and test plan.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch flemzord/backport-alone-bucket

Comment @coderabbitai help to get the list of available commands and usage tips.

@flemzord flemzord changed the title fix(storage): scope ledger queries with cached bucket count hint fix(storage): scope ledger queries with bucket hint Feb 6, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/storage/driver/driver.go`:
- Around line 93-113: OpenLedger currently calls systemStore.GetLedger and
systemStore.CountLedgersInBucket separately which can race with concurrent
CreateLedger and leave an already-open Store with a stale isAloneInBucket flag;
fix by performing the ledger lookup and bucket count in a single read-only
transaction/snapshot (use whatever transaction/snapshot API your DB/systemStore
exposes) so the CountLedgersInBucket result is consistent with GetLedger, then
call store.SetAloneInBucket based on that transactional count; alternatively, if
a transactional snapshot isn't available, document that callers must not reuse
the returned *ledgerstore.Store across requests and add a per-request refresh
path that re-evaluates CountLedgersInBucket before critical reads (reference:
OpenLedger, systemStore.GetLedger, systemStore.CountLedgersInBucket,
store.SetAloneInBucket).
🧹 Nitpick comments (2)
internal/storage/driver/driver.go (1)

113-113: Nit: return explicit nil error for clarity.

At this point err is guaranteed to be nil (the non-nil path returns on Line 109), but returning the shadowed err variable is slightly confusing. Returning nil explicitly makes the success path self-evident.

Suggested change
-	return store, ret, err
+	return store, ret, nil
internal/storage/ledger/store.go (1)

27-32: Good documentation on the field — consider sync/atomic if refresh is ever added.

The comment clearly documents the invariant and the invalidation risk. Today the flag is set-once before the store is returned, so there's no data race. However, if a future change refreshes the flag while the store is serving concurrent queries (as the comment itself suggests may be needed), the unsynchronized bool becomes a data race. An atomic.Bool would make the field safe for concurrent read/write at negligible cost.

Comment on lines 93 to 113
func (d *Driver) OpenLedger(ctx context.Context, name string) (*ledgerstore.Store, *ledger.Ledger, error) {
// todo: keep the ledger in cache somewhere to avoid read the ledger at each request, maybe in the factory
ret, err := d.systemStoreFactory.Create(d.db).GetLedger(ctx, name)
// NOTE: if the store is ever cached, the isAloneInBucket flag must be
// refreshed/invalidated when bucket membership changes, otherwise
// cross-ledger reads may occur (missing WHERE ledger = ?).
systemStore := d.systemStoreFactory.Create(d.db)

ret, err := systemStore.GetLedger(ctx, name)
if err != nil {
return nil, nil, err
}

store := d.ledgerStoreFactory.Create(d.bucketFactory.Create(ret.Bucket), *ret)

count, err := systemStore.CountLedgersInBucket(ctx, ret.Bucket)
if err != nil {
return nil, nil, fmt.Errorf("counting ledgers in bucket: %w", err)
}
store.SetAloneInBucket(count == 1)

return store, ret, err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Staleness risk: isAloneInBucket is never refreshed for an already-opened store.

The comment on Lines 95-97 correctly warns about cache invalidation, but the risk goes beyond caching. If the store returned by OpenLedger is held open (even transiently) while a concurrent CreateLedger adds a second ledger to the same bucket, the first store will silently skip the WHERE ledger = ? filter, leaking rows from the new ledger.

For the CreateLedger path this is safe (transactional), but OpenLedger is outside any transaction and the flag is set-once. Consider:

  1. Wrapping GetLedger + CountLedgersInBucket in a read-only transaction (or snapshot) so the count is at least consistent with the ledger lookup.
  2. Documenting that callers must not reuse the returned *Store across requests when bucket membership may change, or adding a per-request refresh.

This is flagged for awareness — the window is narrow but the impact (cross-ledger data leakage) is high if it occurs.

🤖 Prompt for AI Agents
In `@internal/storage/driver/driver.go` around lines 93 - 113, OpenLedger
currently calls systemStore.GetLedger and systemStore.CountLedgersInBucket
separately which can race with concurrent CreateLedger and leave an already-open
Store with a stale isAloneInBucket flag; fix by performing the ledger lookup and
bucket count in a single read-only transaction/snapshot (use whatever
transaction/snapshot API your DB/systemStore exposes) so the
CountLedgersInBucket result is consistent with GetLedger, then call
store.SetAloneInBucket based on that transactional count; alternatively, if a
transactional snapshot isn't available, document that callers must not reuse the
returned *ledgerstore.Store across requests and add a per-request refresh path
that re-evaluates CountLedgersInBucket before critical reads (reference:
OpenLedger, systemStore.GetLedger, systemStore.CountLedgersInBucket,
store.SetAloneInBucket).

@flemzord flemzord closed this Feb 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant