Skip to content

fix(proxy): reconcile owner replicas after leader purges abandoned bridge sessions - #1716

Closed
Soju06 wants to merge 14 commits into
mainfrom
fix/reconcile-purged-bridge-sessions
Closed

fix(proxy): reconcile owner replicas after leader purges abandoned bridge sessions#1716
Soju06 wants to merge 14 commits into
mainfrom
fix/reconcile-purged-bridge-sessions

Conversation

@Soju06

@Soju06 Soju06 commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Summary

Implements fix (b) of the approved design on #1354 (fix (a), turn-scoped leases, landed in #1476). The leader's purge_abandoned_before deletes durable HTTP-bridge rows only — the owner replica's in-memory session, and any account stream lease it holds, survives. That is exactly the reported "http_bridge_sessions is empty but the in-memory stream cap stays full until restart" state.

Fixes #1354.

How

  • purge_abandoned_before deleting >0 rows now bumps a new http_bridge_purge namespace on the existing cache-invalidation bus (leader-side, collapses per poll cycle).
  • On the bump, every replica runs reconcile_purged_http_bridge_sessions(): quiescent in-memory sessions (no pending/queued work, no admission waiter, no handoff in progress, no unanchored reservation, durable claim present) are bulk-looked-up by durable_session_id; sessions whose rows are gone are re-validated under the bridge lock, detached, and closed — releasing their account stream lease immediately instead of waiting out the 900s in-memory lease TTL.
  • The close passes release_durable_session=False (the row is already gone) and writes no account error health — identical semantics to idle eviction. Sessions with live rows or in-flight work are untouched.

Ownership/settlement invariants (review trapdoors)

  • No lease is force-released while a turn is in flight: pending/queued work exempts a session both at snapshot and at the under-lock re-validation after the durable lookup.
  • No health writes, no account exclusion, no cross-account movement — the reconcile only detaches + closes, through the same close helper as idle eviction.
  • Fresh sessions cannot be misclassified: candidates require a persisted durable claim (durable_session_id), and the abandoned purge itself only removes rows with expired leases older than the retention cutoff.

OpenSpec

openspec/changes/reconcile-purged-bridge-sessions/ — ADDED requirement on sticky-session-operations with four scenarios (orphan release, live-row survival, in-flight exemption, no-op purge). Validation passes.

Tests

  • Reconcile closes the purged quiescent session and releases its lease; keeps the live-row session; skips busy/unclaimed sessions and skips the durable lookup entirely with no candidates (tests/unit/test_proxy_http_bridge.py).
  • Cleanup scheduler bumps http_bridge_purge exactly when the abandoned purge deleted rows (tests/unit/test_sticky_session_cleanup_scheduler.py).
  • Namespace log-label coverage test keeps the new namespace registered.

No new settings; zero-config.

🤖 Generated with Claude Code

…idge sessions

The leader's abandoned-session purge deletes durable HTTP-bridge rows
only; the owner replica's in-memory session — and the account stream
lease it may hold — survives, which is issue #1354's observed
'http_bridge_sessions empty but in-memory stream cap full' state.

The purge now bumps a new http_bridge_purge cache-invalidation
namespace, and on that bump every replica reconciles quiescent
in-memory bridge sessions (no pending work, no admission waiter, no
handoff, no unanchored reservation) against the durable table: sessions
whose rows are gone are detached and closed, releasing their leases.
The close skips the durable release (row already gone) and writes no
account health, matching idle-eviction semantics.

Fixes #1354

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Soju06

Soju06 commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9940e719b8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/modules/proxy/_service/http_bridge/mixin.py Outdated
Comment thread app/modules/proxy/_service/http_bridge/mixin.py Outdated
…closes

- Re-validate the full candidate predicate under the lock: a session that
  gained an unanchored reservation or re-claimed a fresh durable row while
  lookup_sessions() was awaited is no longer detached.
- Run the orphan closes in a tracked background task so slow upstream-reader
  cancels never pin the sole cache-invalidation poller; sessions are already
  detached, so a concurrent bump cannot double-process them and shutdown
  drains the tracked task.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Soju06

Soju06 commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 96fd237e6f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/modules/sticky_sessions/cleanup_scheduler.py Outdated
Comment thread app/modules/proxy/_service/http_bridge/mixin.py Outdated
@Komzpa Komzpa added the 🤖 codex: needs work [@codex review] raised an issue label Aug 13, 2026
@Komzpa

Komzpa commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 5965a424d9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Komzpa Komzpa removed the 🤖 codex: needs work [@codex review] raised an issue label Aug 13, 2026
Komzpa's drain/activity registration and bump persistence are kept; this
fixes the three CI gates that were red on that head:

- mixin.py had grown to 2515 lines against the 2436 architecture ratchet.
  Move the reconcile into _service/http_bridge/purge_reconcile.py following
  the existing module decomposition, and factor the duplicated candidate
  predicate into one _purge_reconcile_eligible_durable_id() helper used by
  both the initial scan and the under-lock re-validation, so the two can no
  longer drift. mixin.py is back to 2427 lines.
- ty: assert Mock.await_args is not None before reading .kwargs.
- The namespace enumeration in test_cache_invalidation_bus.py needed the
  new http_bridge_purge namespace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Soju06

Soju06 commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

@Komzpa thanks — both of your changes are real gaps I missed and they stay: the synchronous bump() with request_bump() fallback closes the lost-invalidation window if the leader dies after the deletions commit, and the task-name registration is what actually makes the drain honour the comment I wrote above it.

Your head was red on three gates, now fixed in 36f5d0e:

  1. Architecture ratchetmixin.py had grown to 2515 lines against the 2436 limit (this also failed CI Required, test_check_proxy_architecture, and the lint job). The reconcile moved to _service/http_bridge/purge_reconcile.py following the existing module decomposition. While extracting I folded the duplicated candidate predicate into one _purge_reconcile_eligible_durable_id() used by both the initial scan and the under-lock re-validation, so those two can no longer drift apart. The drain side now references the shared _PURGE_RECONCILE_CLOSE_TASK_NAME constant (the activity snapshot keeps the literal — purge_reconcile imports helpers, so a constant there would be an import cycle).
  2. tyMock.await_args is _Call | None; asserted non-None before reading .kwargs.
  3. integration-core-1test_namespace_log_labels_cover_all_namespaces enumerates every namespace explicitly and needed http_bridge_purge.

All four gates pass locally (ruff, ruff format, scripts/check_proxy_architecture.py, scoped ty, 558 unit tests).

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36f5d0e585

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/modules/sticky_sessions/cleanup_scheduler.py Outdated
purge_abandoned_before commits its deletions in batches inside the loop, so
bumping only after it returns leaves every already-committed batch
unsignalled when the run is cancelled or the process dies mid-purge — the
owner replicas then keep exactly the orphaned sessions and stream leases
this change exists to release, until the lease TTL.

The repository now takes an on_batch_committed callback invoked after each
batch commits, and the scheduler's _signal_abandoned_bridge_purge persists
the bump synchronously per batch, falling back to the poller retry queue on
a failed or raising write. A signalling failure never aborts the purge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Soju06

Soju06 commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d1f77c0394

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/modules/sticky_sessions/cleanup_scheduler.py Outdated
The signal callback runs only after its batch is committed, so a graceful
shutdown that cancels the cleanup scheduler mid-write would drop the signal
for rows that are durably gone — and CancelledError is a BaseException, so
it bypassed the failure fallback entirely. Shield the bump and drain it
within a bounded grace before propagating the cancellation, queueing the
retry when it still does not land.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Soju06

Soju06 commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92d891df3b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/modules/proxy/_service/http_bridge/purge_reconcile.py
Comment thread app/modules/sticky_sessions/cleanup_scheduler.py
…p task

- Release every detached orphan's account stream lease up front instead of
  inside the serial close loop: releasing the cap slot is the point of the
  pass, and a close can block on an upstream-reader cancellation, so later
  orphans' capacity was held for as long as earlier readers took to unwind.
  Idempotent with the close's own release; one failure cannot strand the rest.
- Cancel the shielded bump task when the drain grace expires, logging its
  outcome, rather than leaving it running untracked for loop teardown to
  cancel silently after the poller has already stopped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Soju06

Soju06 commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 30e900d70f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/modules/proxy/_service/http_bridge/purge_reconcile.py Outdated
Comment thread app/modules/sticky_sessions/cleanup_scheduler.py
…oller stop

- The lease-release await sat between detaching the orphans and creating the
  tracked close task, so a concurrent shutdown could see an empty registry and
  finish its only bridge-cleanup drain before the task existed — and a
  cancellation in that window dropped the task entirely, leaking the detached
  sessions' upstream sockets. Detach and track are now adjacent; the releases
  moved inside the tracked task, still ahead of every close.
- CacheInvalidationPoller.stop() cancelled its polling task without flushing
  queued bumps, and main.py stops the poller right after the schedulers, so a
  bump that failed over to the retry queue during shutdown was discarded and
  peers kept stale caches. stop() now runs a final flush, logging whatever
  cannot be flushed. This applies to every namespace, not just the purge signal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Soju06

Soju06 commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae32c507fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/modules/proxy/_service/http_bridge/purge_reconcile.py
A session handed to an anchored request is not yet visible as busy: the
request sets last_used_at at handoff but only increments
admission_waiter_count once it reaches submit, and an anchored reuse can
await the retry-circuit lookup in between. Idle eviction is protected from
that window by its idle TTL; this pass exists to beat that TTL, so without
its own floor it could close a session out from under a live request.

Add a 30s recency floor to the candidate predicate. Any session this pass
legitimately targets has been abandoned for hours — the purge only deletes
rows whose activity predates the retention cutoff — so the floor costs
nothing while closing the handoff race.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Soju06

Soju06 commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 43a7477b2b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/core/cache/invalidation.py Outdated
The final flush added for the purge signal awaited the same database that
can block a caller's synchronous bump, with no deadline, so an unresponsive
driver could hold shutdown indefinitely — past both the drain budget and the
bounded grace that queued the fallback in the first place.

Bound it with _STOP_FLUSH_TIMEOUT_SECONDS, cancel the abandoned write with a
done-callback logging its outcome, and restore the pending markers on
timeout or failure: _flush_pending_bumps clears each marker before awaiting
its write, so an abandoned flush would otherwise lose the namespace it was
working on rather than leaving it queued and logged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Soju06

Soju06 commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae42938459

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/modules/proxy/_service/http_bridge/purge_reconcile.py Outdated
Comment thread app/core/cache/invalidation.py
Derive the purge reconcile's idle floor from proxy_request_budget_seconds
instead of a fixed 30s: that budget bounds the whole request, so a handoff
whose submit has not happened within it belongs to a request that already
failed. No arbitrary constant has to be assumed safe against a slow
intermediate lookup (e.g. the pre-created retry-circuit cooldown).

_flush_pending_bumps clears each marker before awaiting its write, so a
cancelled write left the namespace neither written nor pending. stop()
cancels the polling task by design, so this silently dropped whatever the
poll loop was mid-write on — before the final flush could see it. Restore
the marker on any abort, and handle CancelledError in the stop flush too:
it is a BaseException, so it bypassed the restore while the finally block
still cancelled the write, and stop() has already cleared _task so a
retried stop() would never revisit it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Soju06

Soju06 commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 93893ffa4c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/modules/proxy/_service/http_bridge/purge_reconcile.py Outdated
The bridge runs its own budget (http_responses_session_bridge_request_budget_seconds,
7200s) rather than the general proxy budget (600s), so the floor expired
while an anchored reuse was still inside its actual deadline.

This also fixes the unit failures on CI: time.monotonic() counts from an
arbitrary origin, and on a freshly booted container it can be smaller than
the budget, so tests relying on the session factory's fixed last_used_at=1.0
were machine-dependent. Reconcile candidates are now backdated relative to
the current clock via a shared helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Soju06

Soju06 commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0e30863ff3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/modules/proxy/_service/http_bridge/purge_reconcile.py Outdated
Comment thread app/core/cache/invalidation.py
The previous commit's floor (the 7200s bridge budget) exceeded the 3600s
abandoned-row retention cutoff, so the single bump the purge emits skipped
every session aged between them — and no later purge re-signals an already
deleted row, so those sessions were never reconciled at all. The two
constraints cannot both be met by outwaiting the request budget.

Correctness against the pre-submit window comes from the durable liveness
check instead: an anchored reuse renews the session's durable row under the
bridge lock before it yields, so a session a request is about to use is
found live and skipped. The idle floor is now a secondary guard for the
narrow residue, clamped to half the retention cutoff so no deployment can
configure the pass into a no-op. Invariant covered by a test.

Also own the cancelled final-flush unwind with a bounded await rather than
returning while cancel() is still in flight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Soju06

Soju06 commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b4956f5446

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +220 to +222
if _service_time().monotonic() - session.last_used_at < min_idle_seconds:
# Recently handed to a request that has not reached submit yet.
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace the one-shot idle floor with request ownership

When the leader's delete wins the race with an anchored reuse, _renew_durable_http_bridge_lease() returns normally after renew_live_session() returns None, so the claimed durable-liveness guard is absent while the request still receives the local session. A bump processed within this 30-second floor skips that now-orphaned session permanently because deleting the row produces no later signal; if processing is delayed past the floor while the request is still in a pre-submit lookup, reconciliation instead closes the selected session. Fresh evidence beyond the earlier floor thread is the final helper's explicit missing-row fallthrough at helpers.py:2102-2109; retain a request-owned marker through submission rather than relying on this one-shot age test.

AGENTS.md reference: AGENTS.md:L111-L115

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepting this one rather than patching around it again — you have now made the same point three times and been right each time, and this evidence (_renew_durable_http_bridge_lease() returning normally when renew_live_session() yields None, so the reuse proceeds with no live durable row) is what finally kills the last heuristic: the durable liveness check cannot be the primary guard if the leader can win that race.

Combined with the earlier finding that the floor must sit below the retention cutoff to work at all, there is no time-based value that is simultaneously long enough to cover an unbounded pre-submit window and short enough to reconcile anything. Only an explicit request-ownership marker established atomically at handoff satisfies both — which is what you proposed at the start.

That marker belongs in _get_or_create_http_bridge_session() and every submit/failure path that must clear it, and leaking it on an error path would make sessions permanently un-evictable — strictly worse than the bug being fixed. That is a deliberate change to the bridge hot path, not a review-loop patch, so I am leaving it to the maintainer rather than landing it autonomously. Marking the PR draft with a summary; leaving this thread open as the blocking item.

Comment thread app/modules/sticky_sessions/cleanup_scheduler.py
Comment on lines +143 to +144
finally:
session.account_lease = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep failed leases available for the close retry

When release_account_lease() raises for one detached orphan, the finally block clears its only lease reference even though the load balancer still counts that lease. The subsequent _close_http_bridge_session() therefore sees None and cannot retry the release, leaving that account's stream capacity consumed until stale-lease reclamation—the exact condition this reconciliation is intended to repair. Clear session.account_lease only after a successful release so the later close can retry, while continuing the loop for the remaining sessions.

AGENTS.md reference: AGENTS.md:L111-L115

Useful? React with 👍 / 👎.

…e close retry

- The scheduler's abandoned-signal path cancelled the shielded write and
  returned while cancellation was merely requested; own the unwind with a
  bounded await, matching the poller's stop path.
- A failing release_account_lease cleared the session's only lease reference
  even though the load balancer still counts that lease, denying the close
  its retry and stranding the slot until the TTL. Keep the reference on
  failure and clear it only on success.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Soju06
Soju06 marked this pull request as draft August 14, 2026 04:07
@Soju06

Soju06 commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

Status: draft — blocked on a design decision, not on polish

Marking this draft after 11 review rounds. Everything except one item is done and green, but that item is a genuine design question that should not be answered inside a review loop.

What is settled and working

  • Fix (b) itself: leader purge → per-committed-batch http_bridge_purge signal → owner replicas reconcile in-memory sessions against the durable table, releasing orphaned stream leases instead of waiting out the 900s lease TTL.
  • Signal durability: emitted per committed batch (deletions commit inside the repository loop), persisted synchronously, shielded from shutdown cancellation with a bounded drain, deterministic cleanup of an abandoned write, retry queued when it cannot land.
  • A real pre-existing bug fixed along the way: _flush_pending_bumps cleared each pending marker before awaiting its write, so a cancelled write — which stop() causes by design — silently lost that namespace. This affected every invalidation namespace, not just this signal.
  • Capacity is freed before any close blocks; detach and task-tracking are adjacent so a concurrent shutdown cannot race the drain; a failed lease release keeps its reference for the close retry.
  • Gates green locally: ruff, ruff format, check_proxy_architecture.py, scoped ty, 586 tests. mixin.py was over the 2436-line ratchet, so the pass now lives in _service/http_bridge/purge_reconcile.py.

The blocking item

The reconcile must not detach a session a request has been handed but has not yet submitted on. I tried three guards and codex correctly refuted each:

  1. Fixed 30s idle floor — unsound, the pre-submit path can await an unbounded durable lookup.
  2. Floor = bridge request budget (7200s) — sound against that window, but above the 3600s abandoned-row retention cutoff. Since the purge signals only when it deletes a row and never re-signals a deleted one, this skipped exactly the sessions the signal existed for. The pass became a no-op.
  3. Durable liveness as the primary guard (reuse renews the row under the bridge lock) — refuted by this thread: when the leader's delete wins that race, _renew_durable_http_bridge_lease() returns normally with no live row, so an in-use session looks reconcilable.

The constraints are irreconcilable with any time-based value: it must exceed an unbounded window and stay under the retention cutoff. Only an explicit request-ownership marker set atomically at handoff satisfies both — which is what codex proposed in the first round.

Why I stopped here

That marker means mutating _get_or_create_http_bridge_session() and every submit and failure path that must clear it, in the proxy's hottest path. Leaking it on an error path would make sessions permanently un-evictable — strictly worse than the 15-minute stall this fixes. Per CONTRIBUTING's one-concern rule that is its own change with its own review, not a patch appended to this one.

@Soju06 two options:

  1. Land the ownership marker here — I can implement it, but it wants a deliberate review of the handoff path rather than another round-trip at the end of a long thread.
  2. Split: land the ownership marker first as its own PR, then rebase this one to simply skip sessions carrying the marker, dropping the idle floor entirely.

I lean toward (2): it makes each change reviewable on its own terms, and the marker is independently useful — idle eviction relies on the idle TTL for the same protection today.

The #1695 bridge_instance_mismatch flake also hit this PR's integration-bridge shard twice during these rounds (four occurrences across four unrelated PRs in three days).

@Soju06

Soju06 commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

Closing in favour of #1747, which fixes the same issue in +127 lines with no new machinery.

The premise this PR was built on was wrong, and @Soju06 caught it: releasing an account lease is a counter decrement that cannot disturb an already-dispatched request, and — decisively — fix (a) (#1476) already made leases turn-scoped. _maybe_release_idle_http_bridge_session_lease releases when queued_request_count == 0 and admission_waiter_count == 0 and not pending_requests, which is exactly the set of sessions this reconcile could target; anything still holding a lease has pending work and was skipped. So this pass freed approximately zero cap slots, and the "release capacity before the slow closes" work I added mid-review was solving a non-problem.

Once the capacity rationale went, the remaining value was closing orphaned sockets on a replica that has stopped receiving traffic — and the existing idle sweep already does that, with the idle-TTL guard against the pre-submit handoff race that this PR spent eleven rounds failing to cover with heuristics. It was simply never being run off the request path. #1747 does that and nothing else.

@Komzpa your two commits here were correct fixes to real gaps and I'm sorry they go down with the branch — the bump-persistence one in particular exposed a genuine pre-existing defect in the invalidation bus (_flush_pending_bumps clears each pending marker before awaiting its write, so a cancelled write silently drops that namespace, for every namespace, not just this signal). I'll send that as its own PR with credit to you, since it stands on its own.

The codex findings on this PR were all legitimate and each one moved the analysis forward — the last of them is what proved no time-based guard could work here.

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.

HTTP-bridge stream lease held for whole session lifetime exhausts per-account stream cap (header-less stalls)

2 participants