Add durable streaming recovery benchmarks - #3966
Conversation
Amp-Thread-ID: https://ampcode.com/threads/T-01a0d300-a45d-754b-9fb1-373a5f1f640a Co-authored-by: Amp <amp@ampcode.com>
…ures Verify worker-service reconnect, exact-cursor Takeover, asymmetric siblings, and second-restart session retirement. Include nested mid-production regression coverage exposing terminal resume cursor propagation. Amp-Thread-ID: https://ampcode.com/threads/T-01a0d328-73c0-7761-a589-2e04adc193e3 Co-authored-by: Amp <amp@ampcode.com>
Initialize all private protocol validators from executor-verified terminal cursors. Require unique requested output offsets, preserve closed roots across Takeover, and continue requiring child output terminals. Amp-Thread-ID: https://ampcode.com/threads/T-01a0d328-73c0-7761-a589-2e04adc193e3 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0d300-a45d-754b-9fb1-373a5f1f640a Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0d300-a45d-754b-9fb1-373a5f1f640a Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0d300-a45d-754b-9fb1-373a5f1f640a Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a0d380-a61a-70a1-b5a9-cef0af93ffa0
Keep domain, environment, application, and owning account cleanup best effort, recording every real failure. Amp-Thread-ID: https://ampcode.com/threads/T-01a0d380-a61a-70a1-b5a9-cef0af93ffa0 Co-authored-by: Amp <amp@ampcode.com>
Measure trusted resume and takeover phases with deterministic gated prefixes, physical older-session index checks, exact suffix validation, and public retirement observations. Amp-Thread-ID: https://ampcode.com/threads/T-01a0d380-a61a-70a1-b5a9-cef0af93ffa0 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0d300-a45d-754b-9fb1-373a5f1f640a Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0d300-a45d-754b-9fb1-373a5f1f640a Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0d300-a45d-754b-9fb1-373a5f1f640a Co-authored-by: Amp <amp@ampcode.com>
✅ Deploy Preview for golemcloud canceled.
|
| repeated string command_path = 12; | ||
| // Requested output cursors verified against durable history to already name a terminal. | ||
| // No suffix or repeated terminal is sent for these streams on this attachment. | ||
| repeated golem.common.UUID terminal_cursor_stream_ids = 14; |
There was a problem hiding this comment.
Why did we have to extend the protocol?
There was a problem hiding this comment.
The nested recovery case exposed a production protocol gap, not a benchmark-only need. On reconnect the public client sends opaque cursor tokens through the stateless worker-service proxy. Only the executor, after reading authoritative durable state, knows which requested output streams had already terminated. Without carrying that verified set in InvocationAccepted, the proxy starts with fresh state and waits for already-delivered root terminals forever (or would have to guess and risk suppressing unfinished child suffixes). terminal_cursor_stream_ids carries only executor-verified, requested output streams; shared protocol validation rejects duplicates, unrequested/input/start IDs, and initializes those streams as closed. This is what makes nested public reconnect sound.
| run: | | ||
| cargo run --profile benchmarks -p integration-tests --bin benchmarks '--' \ | ||
| --primary-only \ | ||
| --retain-details \ |
There was a problem hiding this comment.
Why do we need these new parameters?
There was a problem hiding this comment.
They made failure detail and selected zero-count series survive --primary-only, but that changes the existing publisher workflow and is not necessary for the benchmark scenarios themselves. I removed both workflow arguments in 9adb4e2c.
| self.blocking_kill(); | ||
| } | ||
|
|
||
| async fn kill_and_wait(&self, deadline: tokio::time::Instant) -> anyhow::Result<()> { |
There was a problem hiding this comment.
How is this more than the existing kill which wasl also blocking on the process kill?
There was a problem hiding this comment.
The old kill called Child::kill, immediately dropped the child handle, and never observed process exit. That can race a restart and can leave an unreaped child. kill_and_wait owns the lifecycle lock, sends the kill, polls try_wait until the shared absolute deadline, retains the child handle on timeout so it can be reaped later, and reports failure instead of silently continuing. Per your follow-up, 8a48f778c removes the weaker executor kill/kill_all APIs and converts all callers to the bounded variants.
| return; | ||
| } | ||
| } | ||
| let decision = if let Some(deadline) = recovery.deadline { |
There was a problem hiding this comment.
How is this change related to benchmarks?
There was a problem hiding this comment.
It is related because the nested benchmark reproduced a real reconnect failure through the public worker-service WebSocket path: terminal root streams were known to the executor but not to the stateless proxy. The earlier local HTTP-driver initialization was only a partial workaround. The final implementation removes that local special case; the executor now returns the authoritative terminal-stream set in acceptance and the shared session state initializes from that one contract, so gRPC proxy and HTTP/public clients behave consistently.
| fn agent_id() -> Option<AgentId> { | ||
| Some(AgentId { | ||
| component_id: None, | ||
| component_id: Some(golem_api_grpc::proto::golem::component::ComponentId { |
There was a problem hiding this comment.
Why are we returning a fake component id here?
There was a problem hiding this comment.
This is inside the protocol-test mock only. The tested conversion requires a ComponentDto, but the component identity is irrelevant to the acceptance/stream-state behavior under test, so the fixture supplies a dummy ID rather than creating registry state. No production path returns this ID.
|
|
||
| pub type BenchmarkRegistry = BTreeMap<&'static str, BenchmarkRunFn>; | ||
|
|
||
| pub fn benchmark_registry() -> BenchmarkRegistry { |
There was a problem hiding this comment.
What is this replacing and why was it needed?
There was a problem hiding this comment.
It extracted the registration table that previously lived in the benchmark binary so a temporary direct smoke runner could reuse it. That was scaffolding for development, not a production requirement. I removed the standalone module in 8a48f778c; registration is owned by all.rs again.
| "streaming-rpc-history" | ||
| } | ||
|
|
||
| fn description() -> &'static str { |
There was a problem hiding this comment.
This description is not really easy to understand. Take a look at the existing benchmark descriptions. We need something that explains what our benchmark scenario is and what is measured.
There was a problem hiding this comment.
| } | ||
| } | ||
|
|
||
| pub struct InvocationSession { |
There was a problem hiding this comment.
What is this, and why are we not using the streaming client of golem-client?
There was a problem hiding this comment.
Agreed. The benchmarks now use golem_client::invocation_session::InvocationSession over the real authenticated public worker-service WebSocket endpoint for Start, Resume, and Takeover. The new code is only a thin benchmark observation adapter around that client so it can record milestone times and validate returned values; reconnect/cursor/protocol handling remains in golem-client. The private driver remains only in the dedicated protocol integration tests. Implemented in 8a48f778c.
| type Call = (&'static str, &'static str, &'static str); | ||
|
|
||
| #[derive(Debug)] | ||
| pub(crate) struct CountingKeyValueStorage { |
There was a problem hiding this comment.
Why do we need o count KV store operations?
There was a problem hiding this comment.
This wrapper existed only to make exact in-process assertions for the bounded/missing-index unit tests added with the benchmarks. It did not prove a previously reported production issue and was unrelated to the requested streaming benchmark behavior. I removed the wrapper and those tests in 8a48f778c. The benchmarks still record the production logical storage metrics.
| pub sessions_present: BTreeSet<IdempotencyKey>, | ||
| } | ||
|
|
||
| pub async fn inspect_live_session_index( |
There was a problem hiding this comment.
Explain why do we need this machinery
There was a problem hiding this comment.
This was benchmark-specific PostgreSQL fault-injection machinery: stop/reap executors, inspect the private session-index rows, delete exactly that namespace while proving oplog/payload/KV state was unchanged, then restart so the executor rebuilt derived AgentStatusRecord/session-index state by folding the oplog. It enabled a synthetic “missing index rebuild” scenario that public APIs cannot create. A healthy cold-index benchmark does not need it: it can restart normally, make the first public invocation, and use logical storage metrics to detect history-sized regressions. Since rebuilding deliberately deleted derived storage is outside the streaming-benchmark scope, I removed this helper and the cold-rebuild benchmark in 8a48f778c.
Amp-Thread-ID: https://ampcode.com/threads/T-01a0d300-a45d-754b-9fb1-373a5f1f640a Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0d300-a45d-754b-9fb1-373a5f1f640a Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0d300-a45d-754b-9fb1-373a5f1f640a Co-authored-by: Amp <amp@ampcode.com>
Populate the terminal_cursor_stream_ids field added to the durable streaming protocol on the main-side rewritten RPC acceptance path (a fresh start carries no output cursors, so the list is empty), and pass the session callee fingerprint to lookup_durable_stream_control_metadata in the session index tests, matching the fingerprint-scoped signature. Amp-Thread-ID: https://ampcode.com/threads/T-01a0d842-615a-746d-a864-29d8de10614c Co-authored-by: Amp <amp@ampcode.com>
…chmarks Amp-Thread-ID: https://ampcode.com/threads/T-01a0d842-615a-746d-a864-29d8de10614c Co-authored-by: Amp <amp@ampcode.com> # Conflicts: # golem-worker-executor/src/services/worker/session_index_tests.rs
Summary
Verification
cargo test -p golem-api-grpc --lib -- invocation_session_protocol::tests --report-time(47 passed)cargo test -p golem-worker-executor --lib -- durable_stream_index_reads_are_bounded durable_stream_missing_index_rebuilds_once logical_storage_counters --report-time(4 passed)cargo test -p integration-tests --lib -- benchmark_registry_resolves_streaming_suites streaming_history::tests streaming_recovery::tests --report-time(7 passed)cargo test -p golem-test-framework --lib -- live_inspection live_session_index --report-time(2 passed)cargo clippyforgolem-test-framework,integration-tests, andgolem-worker-executorwith-D warningsResolves GOL-552