fix(tc-token): atomic newer-wins store; drop tc_token_lock and close the cross-source race - #980
Conversation
…en_lock The tc-token lost-update guard was a non-atomic get-then-store in two callers: the history-sync candidate path (serialized by tc_token_lock) and the privacy-notification path (not serialized). The lock only ordered history-sync against itself — the two sources still raced each other, and each read-then-write could let an older token clobber a fresher one. Push the newer-wins rule into the store: store_received_tc_token now overwrites the token pair only when the stored token is a byte-less placeholder or the incoming timestamp is at least as new. sqlite does the read + conditional upsert in an IMMEDIATE transaction (WAL + busy_timeout serialize concurrent writers); in-memory does it under its state lock; the trait default is a best-effort read-modify-write for third-party backends. The sender bucket stays an independent advance-only upsert, so neither writer clobbers the other's field. With the store atomic, the history-sync candidate path drops its get-then-store and the tc_token_lock (field removed), and both sources converge lock-free. privacy_business keeps its pre-filter (it also gates the presence re-subscribe on a genuinely-new token) — now belt-and-suspenders with the atomic store. Tests: newer-wins (older rejected, newer accepted, placeholder accepts the first real token, sender bucket preserved) in both backends. wacore + sqlite + main lib suites pass; clippy clean; wasm32 builds.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Review limit reached
Next review available in: 45 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughRemoves the ChangesTC-token newer-wins concurrency change
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Address Greptile P2: the newer-wins call-site comments restated the store's internal semantics instead of the local intent. Condense them to the 'why' (lock-free convergence, IMMEDIATE for atomicity) per the AGENTS.md comment guideline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0116cfRpumbEv3H4qUNmtNk4
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
storages/sqlite-storage/src/sqlite_store.rs (1)
3012-3062: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winRoute
store_received_tc_tokenthroughwith_retry.
This write path bypasses the retry wrapper used elsewhere for SQLite commits.BEGIN IMMEDIATEcan still surfaceSQLITE_BUSYunder contention, so a transient lock failure can bubble out instead of being retried.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@storages/sqlite-storage/src/sqlite_store.rs` around lines 3012 - 3062, The store_received_tc_token write path currently bypasses the SQLite retry helper, so transient SQLITE_BUSY errors from the immediate transaction can fail instead of being retried. Wrap the spawn_blocking transaction in the same with_retry flow used by other SQLite commit paths, and keep the existing atomic read-modify-write logic inside the retryable closure so contention is handled consistently without changing the token conflict behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@storages/sqlite-storage/src/sqlite_store.rs`:
- Around line 3012-3062: The store_received_tc_token write path currently
bypasses the SQLite retry helper, so transient SQLITE_BUSY errors from the
immediate transaction can fail instead of being retried. Wrap the spawn_blocking
transaction in the same with_retry flow used by other SQLite commit paths, and
keep the existing atomic read-modify-write logic inside the retryable closure so
contention is handled consistently without changing the token conflict behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: efa239af-1dd8-4247-b64f-2d33e479cedd
📒 Files selected for processing (6)
src/client.rssrc/client/lifecycle.rssrc/history_sync.rsstorages/sqlite-storage/src/sqlite_store.rswacore/src/store/in_memory.rswacore/src/store/traits.rs
💤 Files with no reviewable changes (2)
- src/client/lifecycle.rs
- src/client.rs
There was a problem hiding this comment.
0 issues found across 3 files (changes from recent commits).
Requires human review: Refactors concurrent token storage to a newer-wins rule, removing a mutex and changing the tc-token write path. Despite tests, this affects core token sync logic and should be reviewed to prevent subtle race conditions.
Re-trigger cubic
Address CodeRabbit: the atomic newer-wins write bypassed the SQLite retry wrapper, so a transient SQLITE_BUSY from BEGIN IMMEDIATE could surface instead of being retried. Wrap the read + conditional upsert in with_retry, matching take_sent_message and the other commit paths; the atomic transaction logic is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0116cfRpumbEv3H4qUNmtNk4
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Requires human review: Refactors tc-token storage with atomic newer-wins logic, removing a lock. High-impact change in critical data path; requires human review to verify atomicity and correctness.
Re-trigger cubic
📦 Binary size report
.text per crate
Top movers (cargo-bloat attribution)
Baseline: |
Summary
Follow-up to #975 (item #3 from the perf-audit follow-up list). #975 fixed a tc-token lost-update by adding a
tc_token_lockaround the history-sync candidate path, but deliberately left the store's overwrite unconditional. That lock only ordered history-sync chunks against each other — the other writer, the privacy-notification path (privacy_business.rs), was never under the lock, so the two sources still raced, and each did a non-atomic get-then-store that could let an older token clobber a fresher one.Change
Push the newer-wins rule into the store.
store_received_tc_tokennow overwrites the(token, token_timestamp)pair only when the stored token is a byte-less placeholder or the incoming timestamp is at least as new:IMMEDIATEtransaction (WAL + 30 sbusy_timeoutserialize concurrent writers, so the read+write can't lose to another writer).The
sender_timestampbucket stays an independent advance-only upsert (touch_tc_token_sender_timestamp), so neither writer clobbers the other's field.With the store atomic:
tc_token_lock(theClientfield is removed) — concurrent chunks converge on the store.token_stored), and it's now belt-and-suspenders with the atomic store.Why the timestamps are comparable
Both callers already compare the incoming timestamp against the stored
token_timestampon the same scale (privacy_businessdoesreceived.timestamp < existing.token_timestamp), so a single newer-wins rule subsumes both callers' existing checks. Verified that no existing test overwrites a newer real token with an older one.Testing
store_received_tc_token_is_newer_winsin both backends — older write rejected, newer accepted, a placeholder still accepts the first real token, sender bucket preserved.--no-default-featuresbuilds.Generated by Claude Code