Skip to content

fix(lifespan): bound the startup gate below the client handshake budget - #199

Merged
Cipher208 merged 1 commit into
masterfrom
fix/lifespan-gate-budget
Nov 13, 2026
Merged

Cipher208 merged 1 commit into
masterfrom
fix/lifespan-gate-budget

Conversation

@Cipher208

Copy link
Copy Markdown
Owner

Problem

A stdio MCP server blocked in maintenance_lock.acquire() waited the full 120 s default, while the opencode v2 client gives up on initialize after ~6 s and logs:

mcp transport error ... process exited

The process was alive and polling the lock the whole time, so the client's message was wrong — but the effect was real: for every session that started during the wedge there was no memory server at all. That is exactly the 2026-11-13 incident (a wedged v1 process held .maintenance.lock for 3.5 days; v2 clients saw a dead server).

The gate's own docstring already promised the fallback is "better a noisy success than a server that never comes up". A 120 s wait broke that promise: the ungated fallback only ran long after the client had stopped listening.

Evidence

Measured with a probe process holding the lock:

gate contended handshake
120 s (before) never — client gave up at ~6 s
4 s 6.24 s — still late, right on the cliff
2 s (after) 4.27 s — inside the budget

Uncontended startup is ~2.7 s (imports + migrations), so the gate has to leave room inside the budget rather than assume the work is free.

Fix

  • CLIENT_HANDSHAKE_BUDGET_S = 6.0 records the constraint that was implicit before.
  • MIGRATION_GATE_TIMEOUT_S default 120 → 2, overridable with ARIEL_MIGRATION_GATE_TIMEOUT_S when a deliberately long migration must be waited out.
  • Regression test asserts MIGRATION_GATE_TIMEOUT_S < CLIENT_HANDSHAKE_BUDGET_S, so the asymmetry cannot come back silently.

Verification

  • contended handshake 4.27 s, uncontended 2.7 s
  • tests/test_mcp_server/test_lifespan_gate.py: 5 passed
  • full suite: 2870 passed
  • mypy over 10 dirs: clean; ruff check/format: clean

A stdio server blocked in maintenance_lock.acquire() waited the full 120 s
default, while the opencode v2 client gives up on initialize after ~6 s and
logs 'mcp transport error ... process exited'. The process was alive and
polling the lock the whole time, so the client's message was simply wrong —
but the effect was real: for every session that started during the wedge
there was no memory server at all.

The gate's own docstring already promised the fallback is 'better a noisy
success than a server that never comes up'. A 120 s wait broke that promise:
the ungated fallback only ran long after the client had stopped listening.

Measured 2026-11-13 (holding the lock with a probe process):
  uncontended handshake   ~2.7 s
  gate 4 s + work         ~6.24 s  (still late, right on the cliff)
  gate 2 s + work         ~4.27 s  (inside the budget)

So: gate 2 s, with CLIENT_HANDSHAKE_BUDGET_S recording the constraint, and
ARIEL_MIGRATION_GATE_TIMEOUT_S to override when a long migration must be
waited out. Regression test asserts gate < budget.

Verified: contended handshake 4.27 s, uncontended 2.7 s, 5 gate tests pass,
full suite 2870 passed, mypy clean.
@coderabbitai

coderabbitai Bot commented Nov 13, 2026

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@github-actions github-actions Bot added the fix label Nov 13, 2026
@Cipher208
Cipher208 merged commit 1179aae into master Nov 13, 2026
19 checks passed
@Cipher208
Cipher208 deleted the fix/lifespan-gate-budget branch November 13, 2026 13:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant