Skip to content

fix(cloud): bound and yield the relay's global cell-inventory lock - #18521

Merged
Jinwoo-H merged 1 commit into
mainfrom
cloud/relay-cell-inventory-lock
Sep 3, 2026
Merged

Jinwoo-H merged 1 commit into
mainfrom
cloud/relay-cell-inventory-lock

Conversation

@Jinwoo-H

@Jinwoo-H Jinwoo-H commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1107 $\color{#cf222e}{\Huge{\mathbf{−}}}$​3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1104
Prod 8 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​332 $\color{#cf222e}{\Huge{\mathbf{−}}}$​59 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​273

Mirrors stablyai/orca-cloud#471 (byte-identical under cloud/; verified per file against private main). Private PR body follows.

The contention

lockCellInventory runs SELECT * FROM relay_cells ORDER BY cell_id ASC FOR UPDATE:
one global lock over a 23-row table, taken from 31 call sites including the
assignment hot path and the director sweeps. Five director instances plus 23
cells serialise on it against a single Cloud SQL primary.

The pool sets lock_timeout = 1000ms, so a blocked waiter also holds a pooled
client for a full second. Lock contention becomes pool exhaustion.

Signal (production, 5-minute windows) Observed
orca_relay_postgres_transaction_retry ~690
orca_relay_postgres_transaction_exhausted ~12
Director exhaustions in the cell-inventory phase 540 of 541
Deadlocks 0
Pool waiters, peak 100
Pool wait, peak 2s
Cloud SQL CPU, peak 77%

Zero deadlocks with all exhaustion in one phase is the signature of a queue, not
a cycle.

Bounded wait, scoped to one statement

RelayLockOptions gains lockTimeoutMs. The Postgres transaction path issues
SET LOCAL lock_timeout immediately before the locked select and restores
POSTGRES_LOCK_TIMEOUT_MS in a finally, on the error path too.

The restore is load-bearing. SET LOCAL lasts to COMMIT, so without it the bound
would govern every later locked statement in the transaction and misattribute
their 55P03s. rebalanceDormant, assignOnce, assignStickyOnce and
activateControl all take further locks after the inventory.

The bound is CELL_INVENTORY_LOCK_TIMEOUT_MS = 500. Worst-case request queueing
becomes about 1.5s across the wrapper's three attempts, down from about 3s. An
invalid bound throws rather than reaching SQL. SET LOCAL is skipped for NOWAIT,
skipped in autocommit where it cannot survive, and absent on SQLite.

Three-way classification, enforced by a census

The same lock is taken by live requests and by background sweeps, and the right
failure differs. failIfUnavailable is replaced by an explicit mode, so each of
the 31 sites states its intent:

Mode Sites Behaviour
request 11 bounded wait at 500ms
nowait 14 never queue; the caller skips and re-derives next tick
pool-default 3 unbounded wait at the pool lock_timeout
threaded from the caller 3 assign, per entry point

pool-default exists because a sweep that fails sooner turns ordinary contention
into a terminal transaction failure, and one of those freezes the incident gate.
evacuateDeadCells re-enters placement, so assign threads the caller's mode:
clients keep the bound, that sweep does not.

cell-inventory-lock-census.test.ts scrapes every call site from source, derives
sweep reachability by walking the call graph from the ten cleanup steps plus
claimRegionalRehome and recordRegionalRehomeDispatchFailure, and fails if a
site is new, reclassified, unclassified, or sweep-reachable on the bounded wait.

Sweeps yield instead of queueing

Six sweep sites take the inventory NOWAIT: regional rehome claim, fleet safety,
candidate start, completion, expiry abort, and expired-evacuation abort. Each
skips quietly and logs at most one orca_relay_sweep_cell_inventory_busy summary
per tick, reporting how many candidates the tick abandoned. Contention is never a
sweep failure and never quarantines a rehome attempt. A NOWAIT skip raises
database_lock_unavailable, which is not a retryable 55P03, so sweeps also stop
spending the transaction retry budget.

Director sweep periods are now jittered per process. Instances boot from one
rollout, so their 30s cleanup and 1s rehome ticks land on the same wall-clock
second. The offset only ever lengthens a period.

Hold-time measurement

500ms is a first value, not a measurement, and nothing measured the hold before.
The lock is held to COMMIT and the assignment path runs many statements after
taking it. Both dialects now time each inventory lock from acquisition to COMMIT
and report cellInventoryHoldMsMax, cellInventoryHoldMsP95 and
cellInventoryHolds as additive runtime-metrics fields, so the bound can be tuned
after the first director deploy.

Monitor

relayPostgresRetries: 300 is unchanged; it is correctly detecting this
regression. relayPostgresRetryExhausted stays at 0, with a comment naming
exactly which callers can still reach the retry wrapper. Splitting the counter by
the phase label PR #423 put on the log payload would need a labelled log-based
metric, which this signal does not carry, and that is a terraform change this PR
is out of scope for.

Rollout

This ships inside the relay image, so it needs a director blue/green and a cell
roll
to take full effect. The pieces are safe partially deployed: the bound is
per-transaction, and the NOWAIT sweeps only ever do less work than before.

Verification

  • pnpm --filter @orca-cloud/relay test — 462 passed, 80 skipped
  • pnpm --filter @orca-cloud/relay-ops test — 76 passed
  • typecheck, which is also this repo's lint, clean for both packages
  • Postgres 17, --no-file-parallelism: these suites are flaky on origin/main
    itself, so both sides were measured. Warmed, three consecutive full runs each:
    origin/main 10, 8, 10 failures of 510; this branch 10, 8, 8 of 540. Same
    failure count on 30 more tests, same clusters, differing sets run to run.
  • Mutation: 39 mutants across four passes, 38 killed. The survivor inserts a clear
    of the pending rehome disable log above its rethrow, observable only through a
    later tick reading private state.

Mirrors stablyai/orca-cloud#471 (squash c3354e8), byte-identical under cloud/.

The relay's cell-inventory lock (SELECT ... FROM relay_cells FOR UPDATE over
all 23 rows) is one global critical section shared by the assignment hot path
and every director sweep; with the pool's 1s lock_timeout a blocked waiter held
a pooled client for a full second, producing ~690 55P03 retries per 5 minutes
in production. Request paths now bound the wait at 500ms with a SET LOCAL that
is restored to the pool default before the next statement; director-only sweeps
take the lock NOWAIT and skip the tick; sweep timers are jittered; hold time is
exported as additive runtime-metrics fields so the bound can be tuned.
@coderabbitai

coderabbitai Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The relay now uses explicit cell-inventory lock modes for request and sweep paths. PostgreSQL and SQLite record measured inventory-lock hold durations and expose aggregate metrics. Background sweeps skip contended inventory locks and emit summarized warnings. Assignment cleanup and regional rehome intervals use positive jitter. Tests cover lock routing, timeout restoration, hold metrics, sweep behavior, schedule jitter, and incident evaluation for exhausted PostgreSQL retries.

Merge Risk: 🔵 Low · up to 1ad7a

Contended activity-expiry cleanup ticks can appear idle rather than busy, reducing operational visibility into deferred cleanup. Add the existing sweep contention summary instrumentation before merging.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 25 functions across 13 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: bounding and yielding the relay global cell-inventory lock.
Description check ✅ Passed The description gives detailed context, explains the changes and rationale, documents rollout requirements, and reports extensive verification. It does not follow the template headings and does not pr…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description gives detailed context, explains the changes and rationale, documents rollout requirements, and reports extensive verification. It does not follow the template headings and does not provide a concrete linked issue, but the core required information is present.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 72089bd7-378a-41be-a164-4263ff5bb171

📥 Commits

Reviewing files that changed from the base of the PR and between aa78d4a and 1ad7ab3.

📒 Files selected for processing (13)
  • cloud/apps/relay-ops/src/incident-monitor.test.ts
  • cloud/apps/relay-ops/src/incident-monitor.ts
  • cloud/apps/relay/src/assignment-store.ts
  • cloud/apps/relay/src/cell-inventory-hold-samples.ts
  • cloud/apps/relay/src/cell-inventory-lock-census.test.ts
  • cloud/apps/relay/src/cell-inventory-lock-contention.test.ts
  • cloud/apps/relay/src/database.ts
  • cloud/apps/relay/src/index.ts
  • cloud/apps/relay/src/regional-rehome-store.test.ts
  • cloud/apps/relay/src/regional-rehome-worker.ts
  • cloud/apps/relay/src/relay-observability.ts
  • cloud/apps/relay/src/relay-sweep-schedule.test.ts
  • cloud/apps/relay/src/relay-sweep-schedule.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.

const lease = activityLeaseById(activityLeases, text(candidate, 'activity_id'))
if (!lease || integer(lease, 'expires_at') > now) return false
await this.lockCellInventory(transaction, true)
await this.lockCellInventory(transaction, 'nowait')

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Track cell-inventory contention in both activity-expiry sweeps.

lockCellInventory does not log database_lock_unavailable. Increment a dedicated counter when each sweep skips because this lock is busy, then call warnSweepCellInventoryBusy after processing. In releaseExpiredActivity, keep assignment-row lock failures separate from cell-inventory failures.

@pullfrog pullfrog Bot left a comment

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.

✅ No new issues found.

Reviewed changes

  • Bounded, per-transaction cell-inventory wait — RelayLockOptions.lockTimeoutMs issues SET LOCAL lock_timeout before the locked select and restores POSTGRES_LOCK_TIMEOUT_MS in a finally, binding the request path at 500ms instead of the pool's 1s while SET LOCAL still cannot leak into later statements in the transaction.
  • Three-way lock-mode classification — every lockCellInventory/lockGeneralCellInventory site now states request/nowait/pool-default; assign threads the caller's mode so evacuateDeadCells re-enters placement unbounded.
  • Sweeps yield instead of queueing — six sweep sites take the inventory NOWAIT, skip the tick, and emit at most one orca_relay_sweep_cell_inventory_busy summary per tick, keeping database_lock_unavailable off the retry counters.
  • Hold-time telemetry — a bounded reservoir (CellInventoryHoldSamples) reports cellInventoryHoldMsMax/P95/Holds to runtime metrics so the 500ms bound can be tuned against real data.
  • Sweep-schedule jitter — jitteredSweepIntervalMs lengthens the 30s cleanup and 1s rehome periods per process so co-booted directors stop piling onto the lock on the same second.
  • Source-ratchet census — cell-inventory-lock-census.test.ts scrapes every call site from source and derives sweep-reachability from the call graph, failing on any new, reclassified, or bounded sweep-reachable site.

I traced the classification by hand against the store: every sweep-reachable site (assign/assignOnce/assignStickyOnce, completeEvacuation, leastLoadedCell, reconcileReservationAccounting, and the rehome sweeps) lands on nowait or pool-default, never the bounded wait, so the relayPostgresRetryExhausted = 0 gate cannot be tripped by a sweep under contention. The SET LOCAL restore-on-error path, the invalid_lock_timeout validation, and the NOWAIT-catch accounting (per-candidate inventoryBusy, the candidatesTotal - candidatesFinished report, and the swallowed-but-cleared pendingRegionalRehomeDisableLog) are all consistent. The new tests assert exact behavior (SQL statement order, lock-mode choices, warning payloads) rather than tautologies.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@Jinwoo-H
Jinwoo-H merged commit d66386b into main Sep 3, 2026
29 of 30 checks passed
@Jinwoo-H
Jinwoo-H deleted the cloud/relay-cell-inventory-lock branch September 3, 2026 21:28
Jinwoo-H added a commit that referenced this pull request Sep 4, 2026
… a measured bar (#18569)

* fix(cloud): recalibrate the relay monitor's exhausted-retry freeze to a measured bar

The pre-drain dry-run froze at minute one on relayPostgresRetryExhausted: 0
in every run since #18521 reached the director, blocking the cell roll that
carries the same fix. #18521 made contended request-path waiters fail fast
(500 ms) instead of succeeding slowly, so exhaustion is now a steady
contention rate: 236/236 five-minute windows non-zero over 23 h; post-#18521
p50 42 / p90 147 / max 220 fleet-wide; the 2026-08-23 incident peaked at 467.
300 clears every measured healthy window and stays under the incident shape.
/v1/assign 503 share was unchanged by #18521 (13.9% vs 12.3%).

* test(cloud): pin the exhausted-retry freeze boundary at exactly 300

* docs(cloud): reword relay comments that still described the zero exhausted-retry bar
Jinwoo-H added a commit that referenced this pull request Sep 4, 2026
…a measured bar (#18580)

The global relay_cells FOR UPDATE lock made successful retries a
steady-state rate: fleet-wide p50 430 / p90 924 / p99 1320 / max 1504
per five minutes over the last 24 h, 55% of windows over the 300 bar,
only 22% of 15-minute gates clean. Three read-only dry-runs on
2026-09-04 froze on it, blocking the same-cap roll that carries #18521
and the beginProof crash guard to the 23 cells. 2000 clears every
measured healthy gate; the exhausted-retry, director concurrency, and
pool bars keep the incident discriminator role.
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…tablyai#18521)

Mirrors stablyai/orca-cloud#471 (squash c3354e8), byte-identical under cloud/.

The relay's cell-inventory lock (SELECT ... FROM relay_cells FOR UPDATE over
all 23 rows) is one global critical section shared by the assignment hot path
and every director sweep; with the pool's 1s lock_timeout a blocked waiter held
a pooled client for a full second, producing ~690 55P03 retries per 5 minutes
in production. Request paths now bound the wait at 500ms with a SET LOCAL that
is restored to the pool default before the next statement; director-only sweeps
take the lock NOWAIT and skip the tick; sweep timers are jittered; hold time is
exported as additive runtime-metrics fields so the bound can be tuned.
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
… a measured bar (stablyai#18569)

* fix(cloud): recalibrate the relay monitor's exhausted-retry freeze to a measured bar

The pre-drain dry-run froze at minute one on relayPostgresRetryExhausted: 0
in every run since stablyai#18521 reached the director, blocking the cell roll that
carries the same fix. stablyai#18521 made contended request-path waiters fail fast
(500 ms) instead of succeeding slowly, so exhaustion is now a steady
contention rate: 236/236 five-minute windows non-zero over 23 h; post-stablyai#18521
p50 42 / p90 147 / max 220 fleet-wide; the 2026-08-23 incident peaked at 467.
300 clears every measured healthy window and stays under the incident shape.
/v1/assign 503 share was unchanged by stablyai#18521 (13.9% vs 12.3%).

* test(cloud): pin the exhausted-retry freeze boundary at exactly 300

* docs(cloud): reword relay comments that still described the zero exhausted-retry bar
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…a measured bar (stablyai#18580)

The global relay_cells FOR UPDATE lock made successful retries a
steady-state rate: fleet-wide p50 430 / p90 924 / p99 1320 / max 1504
per five minutes over the last 24 h, 55% of windows over the 300 bar,
only 22% of 15-minute gates clean. Three read-only dry-runs on
2026-09-04 froze on it, blocking the same-cap roll that carries stablyai#18521
and the beginProof crash guard to the 23 cells. 2000 clears every
measured healthy gate; the exhausted-retry, director concurrency, and
pool bars keep the incident discriminator role.
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.

1 participant