fix: retry contended SQLite writes outside deadlines - #10
Merged
Conversation
Asynchronous enqueue had no Ruby-level retry budget, so a contended write depended entirely on SQLite's busy handler and surfaced SQLite3::BusyException once concurrent writers exhausted it. This is the intermittent EnqueueTest failure that has cost re-runs on three pull requests. Retry busy errors with bounded exponential backoff, capped by lock_retry_attempts. Also pin every GitHub Actions reference to a commit SHA, and add a benchmark for the three reactive delivery paths.
Greptile SummaryThe PR adds bounded Ruby-level retries for contended SQLite transactions outside synchronous deadlines, pins GitHub Actions to immutable revisions, and documents a benchmark of reactive delivery paths.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[SQLite transaction] --> B{Sync deadline active?}
B -->|Yes| C[Use deadline-based lock retry]
B -->|No| D[Execute transaction]
D --> E{Busy error?}
E -->|No| F[Return result or propagate other error]
E -->|Yes| G{Retry budget exhausted?}
G -->|Yes| H[Re-raise busy error]
G -->|No| I[Wait with capped exponential backoff]
I --> D
Reviews (2): Last reviewed commit: "chore: prepare 0.7.1 release" | Re-trigger Greptile |
Owner
Author
|
@greptileai review |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the three items left open after 0.7.0.
1. The intermittent
EnqueueTestfailurewith_lock_retryis a no-op unless aSyncDeadlineis active, so an asynchronous enqueue hadno Ruby-level retry budget and depended entirely on SQLite's busy handler. Once concurrent
writers exhaust that handler, the caller gets
SQLite3::BusyExceptionwith nothing to catch it.This is the failure that forced a re-run on three separate pull requests.
DatabaseAdapters::Sqlite#transactionnow retries busy errors outside a deadline, withexponential backoff capped at 250ms and a bounded attempt count from the new
lock_retry_attemptssetting (default 10). Behaviour inside a synchronous deadline is unchanged:that path still uses
with_lock_retryand its deadline budget.On the evidence. SQLite's own busy handler masks the gap until exhausted, so a test that
merely holds a lock proves nothing on fast hardware. The three new tests disable the busy handler
so that only a Ruby-level retry can succeed:
All three fail on
mainwithSQLite3::BusyExceptionand pass here.I could not reproduce the original
EnqueueTestflake locally even with the busy handlerdisabled entirely: this machine never contends enough, which matches earlier attempts. The
targeted tests are the evidence for the mechanism; the CI flake staying gone is the evidence for
the outcome, and that can only accumulate over time.
2. Every GitHub Actions reference pinned
All of
actions/checkout,ruby/setup-ruby,actions/setup-node, andsoftprops/action-gh-releaseare now pinned to commit SHAs with the tag kept as a trailingcomment.
rubygems/configure-rubygems-credentialswas already pinned. Zero mutable referencesremain, which matters because every one of these jobs gates the release job.
Note this does not resolve the Node 20 deprecation warning on
action-gh-release; that needs anupstream release targeting Node 24, and pinning does not change the runtime it requests.
3. Benchmark evidence for the delivery paths
benchmark/component_delivery.rb, for one mutation that changes three components:The request column is the point. Server render time is small in every path, so the win is not
faster rendering, it is fewer round trips: each individual refresh is a full HTTP request through
the Rails middleware stack.
These are server-side numbers. They exclude network latency, Action Cable delivery, and
browser rendering, which dominate wall-clock time in a real deployment and make the request-count
difference matter more than these figures suggest. End-to-end latency against a deployed
application is still unmeasured, and I have said so in
docs/benchmarks.mdrather than implyingthe benchmark covers it.
Compatibility
No database change, no migration, no initializer regeneration, no token format change.
lock_retry_attemptsis additive with a default. The retry only engages on lock contention thatpreviously raised, so no currently-succeeding call behaves differently.
Validation
Standard Ruby, RuboCop, RBS, Steep, and Brakeman clean.