Skip to content

Add durable streaming recovery benchmarks - #3966

Merged
vigoo merged 18 commits into
mainfrom
gol-552-streaming-benchmarks
Sep 25, 2026
Merged

vigoo merged 18 commits into
mainfrom
gol-552-streaming-benchmarks

Conversation

@vigoo

@vigoo vigoo commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add trusted invocation-session tooling and deterministic fixture APIs for reconnect, cold reconstruction, and hard-crash stream recovery
  • propagate executor-authoritative terminal cursor metadata through invocation acceptance so proxies and clients validate resumed terminal streams consistently
  • add benchmark infrastructure for structured phase failures, lifecycle kill/reap controls, labelled storage-operation metrics, PostgreSQL session-index inspection/deletion, and SHA-256 artifact provenance
  • add warm-history, cold-indexed, missing-index rebuild, reconnect, flat recovery, sibling recovery, and nested recovery benchmarks with correctness checks
  • register the dedicated crash-recovery integration target and all streaming benchmarks in smoke, quick, and daily suites

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)
  • scoped cargo clippy for golem-test-framework, integration-tests, and golem-worker-executor with -D warnings
  • spawned smoke runs for B–H and dedicated hard-crash recovery suite (4/4)
  • benchmark-results append, tests, production build, and regression analysis accept the generated provenance JSON

Resolves GOL-552

vigoo and others added 12 commits September 24, 2026 15:32
…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>
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>
@vigoo
vigoo requested a review from a team September 24, 2026 16:23
@netlify

netlify Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for golemcloud canceled.

Name Link
🔨 Latest commit eb6c26c
🔍 Latest deploy log https://app.netlify.com/projects/golemcloud/deploys/6ab67d99ca309a000827a682

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;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we have to extend the protocol?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread .github/workflows/benchmark.yaml Outdated
run: |
cargo run --profile benchmarks -p integration-tests --bin benchmarks '--' \
--primary-only \
--retain-details \

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need these new parameters?

@vigoo vigoo Sep 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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<()> {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this more than the existing kill which wasl also blocking on the process kill?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this change related to benchmarks?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we returning a fake component id here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this replacing and why was it needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@vigoo vigoo Sep 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewritten. The descriptions now state the exact setup, what size and length mean, where the restart/disconnect occurs, which timings and counters are measured, and which setup work is excluded. The description rewrite is in c52ee8dcb, with the public-WebSocket wording updated by 8a48f778c.

}
}

pub struct InvocationSession {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this, and why are we not using the streaming client of golem-client?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need o count KV store operations?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explain why do we need this machinery

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

vigoo and others added 6 commits September 25, 2026 09:00
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
@vigoo
vigoo merged commit 0816546 into main Sep 25, 2026
69 checks passed
@vigoo
vigoo deleted the gol-552-streaming-benchmarks branch September 25, 2026 14:31
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant