Report a delete whose reply was lost as having succeeded - #3764
Conversation
|
✅ All contributors have signed the CLA. |
|
recheck |
f3889cb to
bf36c3d
Compare
|
I'm not sure about this. The not-found message was intentionally introduced in #3133 Now with this, I understand we 'survive' a scenario where the first request deletes the agent but the reply is lost, and a retried deletion sees no agent anymore and returns with a failure. |
|
You're right, and that trade wasn't the right one to make. #3133 resolved #2404, where the complaint was precisely "delete reports success when there was no agent", and counting dispatches hands that back whenever a retry happened. Fixed in cdf0895 by settling existence with a read rather than inferring it from a retry. A retried delete can't distinguish "I already deleted it" from "it was never here" —
Cost is one extra RPC on the delete path; a healthy delete p50s at 42ms. What's left rounding the other way is narrower than the case you described: the agent existed at that read, and a third party deleted it before one of our dispatches landed. That needs a concurrent deleter on the same id, and the agent is gone either way. If the extra round trip isn't worth it, the alternative is dropping the routing-layer retry for delete entirely — then not-found is always first-dispatch and always honest, but a lost reply surfaces as a transport error and the caller is left to decide what it means. Happy to go that way instead. The test is |
Chaos scenario S6 (GOL-372) killed an executor mid-deletion on golem-dev, twice, against v1.5.10. Both runs reported deletes that had in fact succeeded as
AGENT_NOT_FOUND.AGENT_NOT_FOUNDdespite succeedingdelete_worker_internalopens withget_latest_metadata(...).ok_or(worker_not_found), so deleting is not idempotent in its response. The first attempt lands, its executor dies before replying, worker-service invalidates its routing table and retries, and the new owner correctly reports nothing there. All seven took 11s, matching the executor client's ~10s connect timeout before the retry; a healthy delete p50s at 42ms.The effect was always correct — the resurrection oracle was clean across 138,900 rounds. Only the answer was wrong. Same family as #3752: the routing layer re-sends a request whose meaning depends on work the first attempt already did.
The fix
Settle existence with a read. A retried delete cannot tell "I already deleted it" from "it was never here". A read can, because repeating it does not change the answer.
WorkerService::deletenow does oneget_metadatabefore anything is dispatched. Absent →AGENT_NOT_FOUND, nothing dispatched, nothing to retry. This is what keeps #3133's answer (which resolved #2404, a user told a delete worked when no agent existed) intact rather than trading one wrong answer for another.Count dispatches, not routing attempts. Given the agent existed, a not-found from a retried dispatch means our own delete landed, so it is reported as success. The counter lives in the remote-call closure rather than reading
call_worker_executor's attempt number, because there are two retry layers:MultiTargetGrpcClient::callhas its ownretries_on_unavailableloop that reconnects and re-sends, and the routing layer never sees those. A counted dispatch is one handed to a connection, not one known to have arrived —attemptraces the call against connection retirement, so a request that never left looks identical to a reply that was lost. That ambiguity is the thing being counted.Discount dispatches the executor turned away.
delete_worker_internalcallsensure_worker_belongs_to_this_executorbefore the metadata lookup, so anInvalidShardIdorShardingNotReadyreply proves that dispatch deleted nothing. It still gets retried. Counting it would mean an ordinary rebalance reported success for a delete that never happened — no crash required.The request is also built once above the retry closure and cloned per attempt, as #3752 left
invoke_agent.What remains rounding the other way is narrow: the agent existed at the read and a third party deleted it before one of our dispatches landed. That needs a concurrent deleter on the same id, and the agent is gone either way.
Tests
Eight.
handle_delete_replyis shared between the retry closure and the tests, so the sequence tests drive the real arithmetic rather than a copy of it. Every guard was verified by disabling it:deleting_an_agent_that_never_existed_still_reports_it_missinga_delete_whose_reply_was_lost_is_not_reported_as_a_missing_agent> 0the_only_dispatch_of_a_delete_still_reports_a_missing_agenta_dispatch_the_executor_turned_away_is_taken_back_out_of_the_countThe pre-check test asserts that nothing was dispatched, not merely that the right error came back — a dispatched delete is one that can be retried, and the retry is what would turn the answer into a success.
a_dispatch_that_was_never_answered_still_makes_the_next_answer_ambiguousis the positive control: a transport failure never reacheshandle_delete_reply, which is why dispatches are counted rather than replies. The turned-away test coversShardingNotReadyas well asInvalidShardId, exercising the proto round-trip both take.Notes for review
RecordingWorkerClientsits abovecall_worker_executor, so no test at that seam sees a second attempt — the same gap Answer callers whose agent moved, and stop retries duplicating their work #3752 documented. Closing it needs a fakeWorkerExecutorgRPC server, which the repo does not have.max_retries: 0on delete stays necessary. A driver-level retry is a fresh request with its own dispatch count.build-golem-moonbitandit-cliare both red on1.5.xbecause the pinned0.10.1+a46be2066toolchain 404s out of the CDN. Update test-r to 3.0.12 #3760 fixed this onmainas part of atest-rupdate; this is the one line of it that matters, applied by hand. Both jobs now pass.mainalso droppedmoon installfrom these jobs — deliberately not backported, since it does not fix this and came with a MoonBit workspace restructure1.5.xdoes not have.