Release the mutation lock before emitting the result - #475
Merged
Conversation
Lock release was fire-and-forget, so a completed response did not imply a released lock. doFinally runs after the result has propagated to the caller, and the .subscribe() inside it only submits the release to a scheduler queue before returning. A second mutation on the same edge could arrive while the lock was still held, exhaust its retry budget (50 x 100ms) and fail with LockAcquisitionFailed. The gap is 1-2ms normally, but grows to seconds when boundedElastic is saturated. The lag was already instrumented but never fixed: AbstractLabel.lockMonitor logs an error for every lock not released within 2 seconds. - success: delayUntil, so the release completes before the result is emitted - error: onErrorResume releases, then propagates the original error - cancel: doFinally(CANCEL) keeps the previous fire-and-forget release - a failed release stays a warn log; the stale-lock sweep clears it Applied to AbstractLabel (v2 path) and V2BackedTableBinding (v3 path). No call sites change; public API, exception types, response payloads and storage format are unchanged. mutationRequestTimeout already wraps the whole mutation, so the added wait cannot become unbounded.
em3s
force-pushed
the
fix/release-mutation-lock-before-emit
branch
from
July 29, 2026 01:27
b28dc97 to
94f53f8
Compare
Contributor
Author
|
Optimistic |
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.
Summary
Mutation lock release was fire-and-forget, so a completed response did not imply a released lock.
doFinallyruns after the result has propagated to the caller, and the.subscribe()inside it only submits the release to a scheduler queue before returning. A second mutation on the same edge can therefore arrive while the lock is still held, exhaust its retry budget (50 × 100ms = 5s), and fail withLockAcquisitionFailed. The gap is 1–2ms normally, but grows to seconds whenboundedElasticis saturated.The lag was already instrumented but never fixed:
AbstractLabel.lockMonitorlogs an error for every lock not released within 2 seconds.Changes
Applied identically to
AbstractLabel(v2 path) andV2BackedTableBinding(v3 path).doFinally)doFinally { release.subscribe() }delayUntil { release }doFinallyonErrorResume { release.then(error) }doFinallydoFinally(CANCEL)— unchangedonErrorDropped)No call sites changed. Public API, exception types, response payloads and storage format are unchanged. The wait cannot become unbounded —
mutationRequestTimeoutalready wraps the whole mutation. The trade-off is that storage degradation now surfaces as latency instead of failure.Verified: no double release (the success, error and cancel paths are mutually exclusive), and a release cannot delete another holder's lock (
cadis a conditional delete against the holder's own value).How to Test
Measured on a larger change set that exercises this path: without the fix the full suite failed 2 of 4 runs; with it, 3 consecutive runs passed.
Production impact can be assessed by grepping for
was not released within— if it fires, this fixes an active issue rather than a latent one.AI Assistance