rpc: answer calls pipelined on an already-failed answer with that exception (rebased on L17) - #10
Merged
Merged
Conversation
…eption A Call targeting the promisedAnswer of an answer that already returned an exception queued in pending_promises forever: resolved_answers records results Returns only, so a failed answer missed there identically to a still-pending one, and the queued-call drain in sendReturnException runs before a pipelined call sent in the same burst can arrive. Found on the wire by the cross-impl L3 lane (pipelined-provide scenarios): the C++ recipient pipelines its Call on the Accept question, the host refuses the Accept fail-closed, and the Call never got a Return — the recipient hung on the exactly-one-Return-per-call guarantee. Record exception Returns in a failed_answers map (owned reason + type, kept until Finish — the resolved_answers lifecycle), written in sendReturnExceptionNoDrain, the funnel every exception Return passes through, so drained pipeline children self-record and transitive late arrivals are covered. planPromisedTarget consults the record exactly where it would otherwise queue, and the call is answered immediately with a COPY of the recorded exception, preserving the retryability signal — the broken-pipeline behavior of the C++ reference. failed_answers also joins the inbound answer-id namespace so a stale record can never be replayed against a reused id, and the record is skipped for loopback and finished-early answers (no Finish would ever clear it). Ablation-proven tests pin both shapes: the refused cross-peer Accept with a pipelined Call (the e2e wire trace, in the vatc suite) and the plain two-party failed answer (from_peer suite), each asserting reason AND type survive the copy and that Finish drains the record. The e2e C++ driver now observes the refusal THROUGH a pipelined call instead of working around the gap with whenResolved(); the full lane passes against the vendored C++ reference (18/18), full suite 914/914, Stable API surface unchanged (experimental snapshot +4 lines). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…remove Its original staging used a receiverHosted target, which f2d89ee now SERVES, so the test referenced a constant that no longer exists and did not compile. The rule under test is the broken-pipeline one -- every Call gets exactly one Return, and a call pipelined on a failed answer gets a copy of that exception -- not any particular refusal, so it moves to the refusal that survives: a site-2 `.promised` target whose import dies before the Accept (site 2 takes no Provide-time pin by design). Also applies the lift's probe-lifetime lesson: the staging probes live in the test frame, because the staged question is still outstanding when the vat is torn down and is cancelled THROUGH that ctx pointer. Ablation: disabling the failed_answers lookup reddens this test with `expected .exception, found null` -- the parked-forever symptom -- and #8's own unit test alongside it. The teeth survive the rebase. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merge artifact, caught by CI's e2e lane. On main the probe was `whenResolved()`, which settles on the Accept's Return WITHOUT invoking the capability, so one real call followed and the driver asserted `localNum.calls == 1`. The rebase correctly kept the stronger probe from the broken-pipeline work -- a genuinely pipelined `getNumberRequest()`, which is what exercises the rule -- but L17's count assertion rode along unchanged. Two invocations is the right expectation now, and both are load-bearing: the first proves a call pipelined on the Accept question is answered, the second that the settled capability still routes to B's own cap rather than host-Carol's. Lane verified locally: 18/18. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #8 — same single commit, rebased onto
mainafter the receiverHosted lift (f2d89ee) landed. Opened as a new branch rather than force-pushing #8's, which is destructive and was blocked.#8 could not merge as-is: it conflicted with L17 in
mod.zig, the vatc test file, the experimental snapshot, and the e2e driver.What the rebase had to reconcile
The textual conflict was one comment block in
tests/e2e/cpp/l3_vatc_client.cpp, but the real collision was semantic: #8's cross-impl probe assumed the host REFUSES thepipelined-providescenario, and L17 makes it SERVE.whenResolved()) and rewrote the comment for the merged behaviour. It still guards the broken-pipeline rule in the answered direction — this exact wait is what hung before the fix.#8's test staged a receiverHosted refusal, which no longer exists. Repointed at a refusal that survives L17 — the site-2 vanished-import shape (CrossPeerProvisionTargetUnavailable). What is under test is the broken-pipeline rule, not which refusal produced it. Also applied L17's probe-lifetime lesson: the staging probes live in the test frame, since the staged question outlives the helper and is cancelled through itsctxatdeinit.Verification
Ablation-proven the teeth survived: disabling the
failed_answerslookup reddens both the repointed vatc test (expected .exception, found null— the parked-forever symptom) and #8's own unit test.Gates:
test,test-rpc-peer,check,check-api,api-closure,hardening,test-oom,test-release-fast,fmt-check,docs-smoke,check-generated, the fullzig build test -Doptimize=ReleaseSafe(the lane that caught L17's SEGV), andjust e2e-l3-vatc18/18.Honest gap: the FAILED-answer direction of the broken-pipeline rule no longer has a cross-impl cell, since L17 turned that scenario's refusal into a success. Unit coverage only. Noted in the driver comment.
Closes #8.
🤖 Generated with Claude Code