fix(proxy): purge stale hard codex_session mappings pinned to a durably unavailable owner - #1417
Conversation
|
Thanks — #1435 has now landed and realigned the proxy architecture ratchets. Please rebase this branch onto current |
d3f65da to
f79ceb7
Compare
|
Rebased onto current `main` (post-#1435/#1437), no conflicts — this branch only touches `sticky_repository.py`/`cleanup_scheduler.py`, disjoint from #1437's changes. Architecture check, ruff, ty, and the full test suite are all green (aside from the pre-existing, unrelated flaky `test_quota_planner_warm_now_keeps_bootstrap_for_metadata_less_primary_rows`, and two `test_metrics.py` tests that fail identically on a clean checkout of current `main` in this environment — confirmed via a separate worktree, unrelated to this diff). |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f79ceb737d
ℹ️ 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".
|
Ran a local review pass on ad15548 after getting CI green (the two failures were flakes — a
The overall shape — background-only purge, delete-never-rebind, hot path untouched — is right, and the earlier updated_at concern is properly fixed by ad15548. Happy to merge once these two are handled. |
|
Pushed `0ae21cc9` addressing both findings from the last review:
Updated the OpenSpec proposal/design/spec delta to cover both, and added regression coverage:
Full suite green aside from the three pre-existing, unrelated failures already confirmed to fail identically on a clean `main` checkout in this environment (`test_quota_planner_warm_now_keeps_bootstrap_for_metadata_less_primary_rows`, and two `test_metrics.py` prometheus-availability tests). `ruff`, `ty check`, and the architecture gate are all clean. |
|
Thanks for 0ae21cc — the future-
CI is green and the rest looks ready — happy to run the gates again after these two. |
|
Thanks for the thorough second pass — both fixed in 966bd61. 1. Purging the sole owner proof strands turn-state sessions Went with the "record metadata to explicitly authorize abandoning continuity" option, implemented as a tombstone-then-delete purge instead of a one-shot delete:
New coverage: 2. Startup seeding resets the grace clock on every boot Replaced the every-boot reseed with a one-time-per-database backfill gated on a durable marker in Both OpenSpec docs (proposal/design/tasks/spec) updated to match. Full suite green (ty check, ruff check/format, architecture-check script, and the full test suite — 6476 passed, only the one previously-flagged pre-existing |
|
Thanks — 966bd61 resolves both remaining findings, and the designs are the right ones.
Two mechanical items before this can land, both from
No source-level findings remain — once the rebase lands with CI green I'll run the review gates one more time and merge. |
|
Friendly ping @softkleenex — nothing new is needed beyond the 07-27 note: fold |
966bd61 to
9bd7354
Compare
…ly unavailable owner Squashed for rebase onto main (was ad15548..966bd61 on purge-stale-hard-codex-session-mappings); see PR Soju06#1417 for the per-round review history.
|
Rebase is up (`9bd7354e`).
`ty check`, ruff check/format, the architecture-check script, the migration/autogenerate-drift tests, and the full suite (6892 passed, 76 skipped, only the previously-flagged unrelated `test_quota_planner_warm_now_keeps_bootstrap_for_metadata_less_primary_rows` flake deselected) are all green locally. CI just kicked off on the force-pushed branch. |
|
One more hardening pass while waiting on review: added |
|
Heads up: |
|
The 08-01 rebase predates #1562 landing (08-01 16:40 UTC), and main's alembic head has moved again: |
…ly unavailable owner Squashed for rebase onto main (was ad15548..966bd61 on purge-stale-hard-codex-session-mappings); see PR Soju06#1417 for the per-round review history.
…path The rebase onto Soju06#1492's race-safe get_account_id folded the tombstone check into whichever row survives a concurrent-write re-read, but that exact branch (delete-on-expiry predicate misses, re-read observes a concurrently tombstoned row) had no direct coverage — every existing purge/tombstone test exercises the plain max_age_seconds=None path.
Main's alembic head moved again via Soju06#1562 (capability lineage markers + merge). This branch's migration predates that merge, so merging as-is would recreate a multiple-heads failure.
47304b3 to
12c5770
Compare
|
Re-parented ( Also checked the
|
|
CI is green ( |
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Summary
A
codex_session-kind sticky mapping is hard-bound and never rebound atrequest time, even once its owner account becomes unavailable
(rate-limited/quota-exceeded/paused) — correctly, per
sticky-session-operations, since the mapping can represent live,unverifiable session state (mid-flight tool calls, account-scoped state)
that isn't safe to move to a different account mid-session.
But that protection has no expiry. If the owner never recovers, every
future request against that session fails closed with
previous_response_owner_unavailable("Hard affinity owner account isunavailable") indefinitely. We hit this in production and had to delete
~250 stale mappings by hand directly in the database to unblock it.
This adds a bounded exception, enforced only by a periodic background purge
— never by the hot-path selection logic, and never by rebinding:
StickySessionsRepository.purge_stale_hard_codex_session_mappings, deletescodex_sessionmappings whose account is non-active (PAUSED,RATE_LIMITED, orQUOTA_EXCEEDED) and whose mapping hasn't actuallybeen reused since well before a cutoff.
StickySessionCleanupScheduler(alreadyrunning every 300s) calls this once per cycle with a fixed 6-hour
threshold — deliberately far longer than any ordinary quota-reset window,
so a transient blip never loses its mapping.
request against that session simply re-resolves fresh, exactly as it
already does today for a session with no mapping at all.
load_balancer.py'shard_stickyselection branch is untouched — thecorrectness invariant it protects (never reallocate mid-flight to an
unverified account) is preserved; this only changes what eventually happens
to an already-abandoned mapping between requests.
An earlier version of this gated on
Account.reset_at/blocked_at, whichlooked right on paper but doesn't hold up against real account data:
reset_atis frequently unset (upstream hasn't reported fresh quota data),and
blocked_atis explicitly cleared toNoneon pause. Gating onStickySession.updated_atinstead — "how long since this mapping wasactually last used" — is both simpler and correct across all three
statuses. See
design.mdin the linked OpenSpec change for the fullrationale.
Full proposal, design rationale, and spec delta:
openspec/changes/purge-stale-hard-codex-session-mappings/.Test plan
being-down) mapping survives; a durably stale one (owner non-active
and unused past the cutoff) is purged; a healthy owner's mapping is
never touched
(
tests/integration/test_proxy_sticky_sessions.py::test_purge_stale_hard_codex_session_mappings_only_drops_durably_unavailable_owners)cleanup cycle with the expected cutoff
codex_sessionmappings are "never purged" by this jobuv run pytest— full suite green except one pre-existing, unrelatedflaky integration test
(
test_quota_planner_warm_now_keeps_bootstrap_for_metadata_less_primary_rows,confirmed to fail identically on unmodified
origin/main)uv run ruff check ./uv run ruff format --check .— cleanuv run ty check— cleanNote: like my other recent PRs, CI's
Lint (ruff)check will likely stillshow red here due to the pre-existing, unrelated
service.py/load_balancer.pyarchitecture line-count gate failures on currentmain(see #1416 and my comments on #1408/#1401) — unrelated to this diff.