Repository navigation
Conversation
delete_session marks the row Deleted and queues the purge, which removes the row when it finishes. Until then create_or_validate_session matches the key, sees the configuration agrees and returns as though it created the session -- and the Active read-back then finds nothing and raised a bare RuntimeError, so deleting a project and creating it again answered 500 with no handler entry in the log. create_session now waits for that purge, polling the row rather than an in-process event because the purge may belong to another worker. If it does not finish inside ten seconds the caller gets SessionAlreadyExistsError, which the API maps to 409: true, actionable and logged, where the 500 was none of those. Found by mm-test functional FP-18 and FM-62. Signed-off-by: Haiyan Wang <[email protected]>
The first commit turned an unfinished delete into SessionAlreadyExistsError, a 409. The Python client reads 409 as "created concurrently" and fetches the project, which 404s while the delete runs. The wait now raises the new SessionDeletionPendingError, which the API maps to 503 with Retry-After: temporary, and retryable. RestError gains an optional headers argument to carry it. The wait's timeout parameter is renamed max_wait to satisfy ruff ASYNC109. Adds tests for waiting out a purge, for a purge that outlasts the wait, and for the 503 mapping. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Haiyan Wang <[email protected]>
|
Fix CI. |
ty rejects assigning a functools.partial over the bound method because the partial's signature differs; monkeypatch.setattr sets it without the check. Signed-off-by: Haiyan Wang <[email protected]>
leomem
left a comment
There was a problem hiding this comment.
Two comments inline, both about what happens when the pending delete never finishes.
| # alone, so creating during that window would "succeed" against the | ||
| # dying row. Wait for the purge instead. Poll the row rather than an | ||
| # in-process event, because the purge may run in another worker. | ||
| await self._await_pending_delete(session_key) |
There was a problem hiding this comment.
If the purge fails, this key stays blocked until the server restarts.
_delete_session_worker catches and logs any exception from _delete_queued_session, so the row stays Deleted and manager.delete_session never runs. start() only re-queues Deleted rows at startup, so nothing clears the key until a restart.
Until then, every POST /api/v2/projects for this key polls the DB about 200 times over max_wait=10s, then returns 503 with Retry-After: 1 and "retry shortly". A client that follows the header repeats the 10s wait indefinitely. One way to hit this: the vector store is unreachable during the purge and the user retries creating the project.
Could the worker re-queue the row or mark it as failed when the purge raises? Or, at minimum, the error could say the delete is stuck instead of suggesting a short retry.
There was a problem hiding this comment.
Good catch. The delete worker now re-queues a failed delete with backoff (1s, doubling to 60s) instead of dropping it, so a temporary failure like an unreachable vector store clears on its own once the store is back, and the next create succeeds. stop() cancels pending retries; start() re-queues Deleted rows as before. The 503 message also now says the delete may have failed if it persists, instead of "retry shortly". Fixed in 972e0d9.
I left out the "mark as failed" state. A delete that fails every time (e.g. #1575) is still retried every 60s and keeps the name blocked. That and partial deletes are already tracked in #1577, and the lifecycle redesign (#1579) covers a proper failed or reconciled state, so I'd rather fix it there than add a new status here.
| code=503, | ||
| message="Project is still being deleted; retry shortly", | ||
| ex=e, | ||
| headers={"Retry-After": "1"}, |
There was a problem hiding this comment.
This 503 only goes out after _await_pending_delete has already polled for 10s. At that point the purge is long-running (a large session, or a queue of deletes ahead of it on the single worker), and Retry-After: 1 invites tight retry loops that each hold a connection for another 10s. A value tied to max_wait would fit better.
There was a problem hiding this comment.
Agreed. SessionDeletionPendingError now carries the wait the server already did, and the router sends that rounded up as Retry-After (10s by default), so clients back off for as long as the server waited instead of retrying after 1s. Fixed in 972e0d9.
A purge that raised was logged and dropped, leaving the row Deleted until restart; every create of that key then waited 10s and returned 503. The delete worker now re-queues a failed purge with backoff (1s doubling to 60s); stop() cancels pending retries and start() re-queues the rows. The 503 now carries Retry-After equal to the wait the server already did, instead of 1s, and its message says the delete may have failed. Signed-off-by: Haiyan Wang <[email protected]>
|
Fixed in 820d59b (the ty failure in the new tests); CI is green on that commit. |
|
Tracked under #1755: the wait-and-503 semantics stay; the in-process retry moves into a durable delete job (#1749) in step (c) of the PR split there, so the retry survives a crash and does not fan out across replicas at boot. 🤖 Written by Claude Code (Claude Fable 5.1) on behalf of @edwinyyyu. |
|
Closing this in favour of Shu's fix, which takes a better approach to the same Recording what the bug is before this closes, so it is not lost with the PR — The server side:
Two things worth keeping separate for whichever fix lands:
For reference, this is a regression against the release: a functional run on |
Problem
Deleting a project and creating it again right away returned 500 with no
handler entry in the log (found by mm-test functional FP-18 and FM-62).
delete_sessionmarks the session rowDeletedand queues the purge, whichremoves the row only when it finishes. During that window
create_or_validate_sessionmatches the row by key, finds the config equal,and returns as if it created the session. The
Activeread-back then findsnothing, and
create_sessionraised a bareRuntimeError.Fix
create_sessionwaits for the pending purge before creating, polling therow (up to 10s). It polls the database rather than waiting on an in-process
event, because the purge may run in another worker.
SessionDeletionPendingError, which the API maps to 503 withRetry-After: 10, the wait the server already did. The message says thedelete may have failed if this persists.
get_or_create_projectreads 409 as "createdconcurrently" and fetches the project, which would 404 while the delete
runs.
RestErrortakes an optionalheadersargument to carryRetry-After.with backoff (1s, doubling to 60s) until it succeeds. Before, the row stayed
Deleteduntil restart and every create of that name returned 503.stop()cancels pending retries;start()re-queuesDeletedrows asbefore.
Flipping the
Deletedrow back toActivewas rejected: the in-flight purgewould then delete the new session's data and its row.
Tests
exists.
Retry-Afterrounded up from the wait.stop()cancels a pending retry instead of waiting out its backoff.Both new
MemMachinetests fail with the wait disabled.Follow-up (not in this PR)
A purge that fails every time (for example #1575, deleting with semantic
memory disabled) is retried every 60s and the name stays blocked. Partial
deletes, and a terminal state for failed deletes, are tracked in #1577, with
the lifecycle redesign in #1579. Partly addresses #1577 (the retry).