Skip to content

Conversation

@flemzord
Copy link
Member

@flemzord flemzord commented Feb 6, 2026

Summary

  • Replace the per-query subquery approach in newScopedSelect with a shared *atomic.Bool per bucket, managed by the DefaultFactory
  • When a second ledger is created in the same bucket, SetAloneInBucket(false) now propagates immediately to all existing stores in that bucket
  • Add CountLedgersInBucket to the system store interface so CreateLedger and OpenLedger can seed the flag correctly

Root cause

The previous subquery-based approach (SELECT count = 1 FROM _system.ledgers ...) in newScopedSelect added overhead to every query. More critically, when stores were cached, a per-store boolean could miss bucket membership changes: existing stores kept believing they were alone and skipped the WHERE ledger = ? predicate, causing cross-ledger data leaks (e.g. TestAccountsList failures).

Changes

  • internal/storage/ledger/store.go: aloneInBucket *atomic.Bool field + SetAloneInBucket / newScopedSelect using atomic load
  • internal/storage/ledger/factory.go: bucketFlags map[string]*atomic.Bool shared across all stores of the same bucket
  • internal/storage/driver/driver.go: CreateLedger and OpenLedger call CountLedgersInBucket and SetAloneInBucket
  • internal/storage/system/store.go: new CountLedgersInBucket method on interface + implementation
  • internal/storage/driver/store.go: CountLedgersInBucket added to local SystemStore interface
  • internal/storage/driver/store_generated_test.go: mock updated for new method

Test plan

  • earthly -P +tests passes (all unit, integration, e2e, stress tests)
  • TestAccountsList no longer flaky with multiple ledgers in the same bucket

…Factory

The previous subquery-based approach in newScopedSelect executed a
COUNT(*) on _system.ledgers for every scoped query. Replace it with a
cached *atomic.Bool per bucket, shared across all stores through the
DefaultFactory.

When a new ledger is created, SetAloneInBucket(false) immediately
propagates to every store in that bucket, ensuring the WHERE ledger=?
predicate is never incorrectly skipped.

Also adds CountLedgersInBucket to the system store interface so
CreateLedger and OpenLedger can seed the flag correctly.
@flemzord flemzord requested a review from a team as a code owner February 6, 2026 13:03
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 6, 2026

Walkthrough

The changes implement a per-bucket optimization flag for ledgers. When creating or opening a ledger, the code counts ledgers in the target bucket and sets an atomic flag if the ledger is the sole ledger in its bucket. This flag is shared across all stores in the same bucket via the factory, enabling efficient query optimization without repeated database counts.

Changes

Cohort / File(s) Summary
Storage Interface Additions
internal/storage/driver/store.go, internal/storage/system/store.go
Added CountLedgersInBucket(ctx context.Context, bucket string) (int, error) method to SystemStore interface and implemented it in DefaultStore to count ledgers matching a specified bucket.
Driver Ledger Operations
internal/storage/driver/driver.go
Modified CreateLedger and OpenLedger to count ledgers in the target bucket and mark the resulting store as AloneInBucket when count equals 1. Added error handling for bucket-counting operations.
Factory Concurrency Controls
internal/storage/ledger/factory.go
Introduced sync.Mutex and per-bucket atomic.Bool flag map in DefaultFactory to track bucket-level isolation state. Factory.Create now safely assigns the appropriate per-bucket flag to each store.
Store Optimization Logic
internal/storage/ledger/store.go
Added aloneInBucket atomic.Bool pointer field and SetAloneInBucket method to Store. Reworked newScopedSelect to conditionally filter by ledger name only when the ledger is not the sole ledger in its bucket, replacing prior complex ledger/COUNT logic.

Sequence Diagram(s)

sequenceDiagram
    participant Driver
    participant SystemStore
    participant Factory
    participant Store
    
    Driver->>SystemStore: CountLedgersInBucket(bucket)
    SystemStore-->>Driver: count
    Driver->>Factory: Create(ctx, ledger)
    Factory->>Factory: Get/Create atomic.Bool for bucket
    Factory->>Store: newStore(...)
    Store-->>Factory: Store instance
    Factory->>Store: SetAloneInBucket(count == 1)
    Factory-->>Driver: Store with flag set
    
    Note over Store: Uses aloneInBucket flag<br/>in newScopedSelect for<br/>query optimization
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A clever flag hops through each bucket bright,
Counting ledgers with atomic care,
Alone or plenty, the logic's tight,
No queries repeated, just a flag we share!
Optimization magic, swift and fair! ✨

🚥 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 accurately describes the main change: replacing a per-query subquery approach with a shared atomic.Bool flag per bucket managed by DefaultFactory.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the root cause, specific file changes, and test plan for the bucket flag sharing optimization.

✏️ 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 fix/main/shared-alone-in-bucket-flag

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

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 78-84: The CreateLedger path currently uses
systemStore.CountLedgersInBucket and may incorrectly set
ret.SetAloneInBucket(count == 1) under READ COMMITTED; change CreateLedger to
always call ret.SetAloneInBucket(false) (since creating a ledger cannot make it
alone) and remove/ignore the COUNT-based logic here; rely on OpenLedger to
compute and promote aloneInBucket=true when it can safely observe committed
state, or alternatively move the COUNT call to after the transaction commit if
you prefer the other approach.
🧹 Nitpick comments (1)
internal/storage/system/store.go (1)

73-82: Consider filtering out soft-deleted ledgers.

GetLedger (Line 198) and DeleteBucket apply a deleted_at IS NULL filter, but CountLedgersInBucket does not. This means soft-deleted ledgers inflate the count, causing the aloneInBucket optimization to be missed when a bucket contains one active ledger and one or more soft-deleted ledgers. The behavior is conservative (safe), but suboptimal.

Suggested fix
 func (d *DefaultStore) CountLedgersInBucket(ctx context.Context, bucket string) (int, error) {
 	count, err := d.db.NewSelect().
 		Model(&ledger.Ledger{}).
 		Where("bucket = ?", bucket).
+		Where("deleted_at IS NULL").
 		Count(ctx)
 	if err != nil {
 		return 0, postgres.ResolveError(err)
 	}
 	return count, nil
 }

Comment on lines +78 to +84
count, err := systemStore.CountLedgersInBucket(ctx, l.Bucket)
if err != nil {
return fmt.Errorf("counting ledgers in bucket: %w", err)
}

ret = d.ledgerStoreFactory.Create(b, *l)
ret.SetAloneInBucket(count == 1)
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

TOCTOU race: concurrent CreateLedger calls for the same bucket can leave aloneInBucket = true with 2 ledgers.

Under PostgreSQL READ COMMITTED (the default with nil tx options on Line 44), each transaction's COUNT only sees rows committed before that statement began plus its own inserts. When two CreateLedger calls for the same bucket overlap, both can observe count == 1 (each seeing only its own uncommitted row), leading both to call SetAloneInBucket(true). After both commit, the bucket has 2 ledgers but the shared flag is true, causing newScopedSelect to omit the WHERE ledger = ? predicate and potentially leaking cross-ledger data.

This is the exact class of bug described in the PR root-cause. The flag self-corrects on the next OpenLedger, but the window could be significant if stores are cached.

Simplest safe fix: always set aloneInBucket = false inside CreateLedger (since creating a ledger can only increase the count), and rely on OpenLedger to promote the flag to true when appropriate.

Suggested fix
-		count, err := systemStore.CountLedgersInBucket(ctx, l.Bucket)
-		if err != nil {
-			return fmt.Errorf("counting ledgers in bucket: %w", err)
-		}
-
 		ret = d.ledgerStoreFactory.Create(b, *l)
-		ret.SetAloneInBucket(count == 1)
+		// Always conservative: a concurrent create may not yet be visible.
+		// OpenLedger will promote to true when the count is actually 1.
+		ret.SetAloneInBucket(false)

 		return nil

Alternatively, move the count + set after the transaction commits so the read sees all committed rows:

Alternative fix — count after commit
 	if err != nil {
 		return nil, postgres.ResolveError(err)
 	}

+	count, err := d.systemStoreFactory.Create(d.db).CountLedgersInBucket(ctx, l.Bucket)
+	if err != nil {
+		return nil, fmt.Errorf("counting ledgers in bucket: %w", err)
+	}
+	ret.SetAloneInBucket(count == 1)
+
 	return ret, nil
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
count, err := systemStore.CountLedgersInBucket(ctx, l.Bucket)
if err != nil {
return fmt.Errorf("counting ledgers in bucket: %w", err)
}
ret = d.ledgerStoreFactory.Create(b, *l)
ret.SetAloneInBucket(count == 1)
ret = d.ledgerStoreFactory.Create(b, *l)
// Always conservative: a concurrent create may not yet be visible.
// OpenLedger will promote to true when the count is actually 1.
ret.SetAloneInBucket(false)
return nil
🤖 Prompt for AI Agents
In `@internal/storage/driver/driver.go` around lines 78 - 84, The CreateLedger
path currently uses systemStore.CountLedgersInBucket and may incorrectly set
ret.SetAloneInBucket(count == 1) under READ COMMITTED; change CreateLedger to
always call ret.SetAloneInBucket(false) (since creating a ledger cannot make it
alone) and remove/ignore the COUNT-based logic here; rely on OpenLedger to
compute and promote aloneInBucket=true when it can safely observe committed
state, or alternatively move the COUNT call to after the transaction commit if
you prefer the other approach.

@codecov
Copy link

codecov bot commented Feb 6, 2026

Codecov Report

❌ Patch coverage is 83.33333% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.16%. Comparing base (a2b701e) to head (5b29bdf).

Files with missing lines Patch % Lines
internal/storage/driver/driver.go 60.00% 2 Missing and 2 partials ⚠️
internal/storage/system/store.go 75.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1255      +/-   ##
==========================================
- Coverage   82.20%   82.16%   -0.05%     
==========================================
  Files         198      198              
  Lines       10224    10243      +19     
==========================================
+ Hits         8405     8416      +11     
- Misses       1326     1330       +4     
- Partials      493      497       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@flemzord flemzord enabled auto-merge February 6, 2026 13:41
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.

2 participants