Skip to content

better lock parallelism for views - #4137

Open
cody-littley wants to merge 1 commit into
mainfrom
cjl/view-parallelism
Open

better lock parallelism for views#4137
cody-littley wants to merge 1 commit into
mainfrom
cjl/view-parallelism

Conversation

@cody-littley

Copy link
Copy Markdown
Contributor

Describe your changes and provide context

Restructure view caching to allow better lock parallelism. Uses an approximate LRU instead of a standard LRU.

@cursor

cursor Bot commented Sep 10, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes locking and eviction on the core view read path; behavior stays correct but cache retention is approximate and concurrent-read semantics must remain sound under the new RWMutex split.

Overview
Improves read parallelism on view shards by switching the shard lock from a single Mutex to an RWMutex, so cache hits and other read-only work can run under the read lock while write lock is reserved for scheduling DB reads and mutations.

The read-through cache drops the shared LRUQueue in favor of in-cache trackedBytes / trackedCount and epoch-stamped entries (atomic recency). Eviction is now sample-based approximate LRU (configurable EvictionSampleSize), with inserts allowed to overshoot the budget up to a hard cap (EvictionSlackDivisor) until MaintainWLocked runs on Commit to bring the cache back under maxSize.

API surface is reorganized with explicit lock contracts (RLocked / WLocked / Unlocked), including AttemptFastLookupRLocked on the hot read path and renamed cache/shard helpers (LookupWLocked, ResolveUnlocked, TakeOutOfService, etc.).

Reviewed by Cursor Bugbot for commit d100c06. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedSep 10, 2026, 4:03 PM

@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.30233% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.37%. Comparing base (152d04d) to head (d100c06).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
sei-db/db_engine/view/shard.go 83.00% 8 Missing and 9 partials ⚠️
sei-db/db_engine/view/view_manager_config.go 33.33% 2 Missing and 2 partials ⚠️
sei-db/db_engine/view/read_cache.go 97.91% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4137      +/-   ##
==========================================
- Coverage   61.35%   60.37%   -0.99%     
==========================================
  Files        2191     2074     -117     
  Lines      192106   178722   -13384     
==========================================
- Hits       117870   107904    -9966     
+ Misses      62752    60478    -2274     
+ Partials    11484    10340    -1144     
Flag Coverage Δ
sei-chain-pr 87.03% <89.30%> (?)
sei-db 69.80% <ø> (-0.22%) ⬇️
sei-db-state-db ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
sei-db/db_engine/view/view_manager_impl.go 86.78% <100.00%> (ø)
sei-db/db_engine/view/read_cache.go 97.26% <97.91%> (-0.45%) ⬇️
sei-db/db_engine/view/view_manager_config.go 86.66% <33.33%> (-8.21%) ⬇️
sei-db/db_engine/view/shard.go 88.21% <83.00%> (-3.85%) ⬇️

... and 118 files with indirect coverage changes

🚀 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.

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sound restructuring of the shard lock into an RWMutex with an RLock-only read fast path, plus a swap from an exact LRU to an epoch-stamped sampled LRU; locking, snapshot consistency, and size accounting all check out. Remaining notes are non-blocking: an eviction sample that can abandon a pass, one inaccurate rationale comment, and test coverage for the new eviction policy and config guards.

Findings: 0 blocking | 4 non-blocking | 2 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] The core property the new lastRead/epoch machinery exists for — that an entry marked recently used survives eviction over one that was not — is untested. TestMaintenanceEvictsBackToBudget deliberately asserts only that the total lands back under budget ("Which keys survive is deliberately not asserted"), and this PR deletes 408 lines of exact-LRU tests that pinned ordering. The property is deterministically testable without relying on map iteration order: set EvictionSampleSize to the entry count so the sample is the whole map, stamp some entries via AttemptFastLookupRLocked/markRecentlyUsed across two epochs (MaintainWLocked advances the epoch), then evict and assert the newer-stamped entries remain. Without it, a regression that stops stamping recency — e.g. dropping the entry.lastRead.Store(c.epoch) in trackWLocked, or an inverted comparison in evictWLocked — leaves every existing test green while the cache degrades to random eviction.
  • [suggestion] TestConfigValidateRejectsBadFields (sei-db/db_engine/view/view_manager_test.go) carries one row per validated field, including overheadZero for the closely analogous EstimatedOverheadPerEntry guard. The two new guards in Validate have no rows, so nothing records that a zero EvictionSlackDivisor (which would panic on divide-by-zero in hardCap) or a zero EvictionSampleSize is rejected. Add evictionSlackDivisorZero and evictionSampleSizeZero rows.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

}
}

if victim == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] visited++ runs before the entry.size > 0 check, so entries in statusScheduled consume sample slots without being evictable. When all EvictionSampleSize sampled entries are in-flight reads, the function returns having reclaimed nothing and the cache stays above the cap it was asked to enforce — including on the hardCap() insert path, which is the only inline ceiling between maintenance passes.

This is practically unreachable at production sizes (a 64 MiB shard cache with the 256-byte per-entry overhead holds ≥256k entries, against in-flight reads bounded by the read pool, so the odds of 8 random draws all being pending are vanishing), so I'd not block on it. But it's cheap to make robust: bound the walk separately from the sample, e.g. require EvictionSampleSize eligible entries while capping total steps at a small multiple, or retry the sample a bounded number of times before giving up. Either keeps the no-spin property this early return protects.

Also, the comment's "The next maintenance pass samples a different part of the map" understates the recovery: every insert-path call (injectValueUnlocked, PutRetiredWLocked) re-samples too, which is most of why the overshoot stays bounded.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1, this might be a blocking issue

if c.EvictionSlackDivisor == 0 {
return fmt.Errorf("EvictionSlackDivisor must be greater than 0")
}
// Zero would leave eviction unable to find a victim, so the cache would grow without bound.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] This comment misstates the failure mode. With EvictionSampleSize == 0, visited is incremented to 1 before the visited == c.config.EvictionSampleSize comparison in evictWLocked, so the break never fires and the loop walks the entire entries map, picking the globally oldest entry. Eviction still finds a victim and the cache does not grow without bound — it degrades to an exact-LRU O(n) scan per evicted entry while holding the shard write lock, which on a full shard cache means hundreds of thousands of map steps per victim.

The guard is worth keeping; only the stated reason needs rewriting to the real one (a per-victim full-map scan under the shard lock).

}
}

if victim == nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1, this might be a blocking issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants