Skip to content

Carried stream subscriptions through reset and failover. - #8

Closed
moetemp wants to merge 75 commits into
moe/AI-198-srv-5-delivery-replayfrom
moe/AI-198-srv-6-reset-failover
Closed

moetemp wants to merge 75 commits into
moe/AI-198-srv-5-delivery-replayfrom
moe/AI-198-srv-6-reset-failover

Conversation

@moetemp

@moetemp moetemp commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

This PR carries stream subscriptions through a workflow reset and characterizes namespace failover.

What changed?

  • service/history/workflow/mutable_state_rebuilder.go recreates a cursor from the subscribe event and moves it with each completed task's range. A range for a stream with no cursor proves a service subscription, which leaves no event, so the cursor comes from the range.
  • InheritStreamsOnReset in chasm/lib/workflow/workflow.go copies what only the base run knows. A cursor on another execution's stream keeps reading it from the base run's last frontier. A cursor on an owned stream gets the reset run's own stream, starting where the cursor stands. The offset space continues, so a range in this run's History names one run. A publisher's reset run still restarts its stream at offset zero, and TestResetOfAPublisherRestartsItsStreamAtZero pins that open gap.
  • Replay reads each range from the run that holds it. A reset point names the base run for every range before it, and everything after belongs to the consumer. Nothing pins the base run, so once namespace retention deletes it, the reset run's cold replay can't start.
  • service/history/ndc/workflow_resetter.go re-delivers the input the reset-point task consumed. It reads that task's ranges off the completion event it doesn't copy, and Stream.Seed puts those records into the reset run's stream at their offsets. A seeded batch keeps the source's age and counts as held bytes, so the byte cap and retention see the copy as the original. Records the base run took after that task stay on the abandoned timeline, and nothing records it.
  • The reset re-registration passes its inherited offset as an explicit start position. Outside consumers aren't told about a reset, since that needs a record shape on the wire.
  • tests/xdc/stream_failover_test.go pins failover in both replication modes. State-based replication carries the stream, its batches and the cursor, and the workflow goes on there. Event-based replication rebuilds only the cursor, so its first task fails with STREAM_RANGE_UNAVAILABLE.
  • The branch has the replaced Added server-side streams for workflows and external clients. #2's head as an ancestor, through an empty merge. That's why the commit list repeats the old history. It adds no tree change, and work pinned to that head keeps clean diffs.

Part of AI-198 (epic AI-37).

Why?

Reset is where the two halves of a subscription, the events and the CHASM state, have to agree. A reset run is rebuilt from events, and the cursors are CHASM state the events never touch. On failover, state-based replication carries a stream and event-based replication doesn't yet. Both read better after the behavior they characterize.

How did you test it?

go build ./... and make fmt are clean. Unit tests ran for ./chasm/lib/stream/, ./chasm/lib/workflow/ and ./service/history/..., with the seed tests. The functional reset tests in tests/stream_reset_test.go and the XDC failover suites ran with -tags test_dep on this tip.

  • Unit Tests
  • Staging
  • End to End Tests

`make fmt` left these two files dirty, which fails the `fmt` job. The proto
also carried a truncated comment for a field that is no longer there.
The stream log methods on `ExecutionStore` had no fault-injection or
telemetry wrapper, so `make go-generate` left the tree dirty.
Both were rewritten to point at `add_stream_log`, which dropped
`nexus_incoming_services` from the Cassandra upgrade path and `tasks_v2`
from the SQLite one. A released version has to stay what it was.
Cassandra goes to 1.14, SQLite to 0.12, MySQL and PostgreSQL to 1.20. The
MySQL and PostgreSQL stores already query `stream_log`, so their base
schema needed the table too.
It holds one cluster per arm for the whole run, which outlasts the
dedicated pool and parks the functional suite until the 35 minute timeout.
Three placeholders carried a trailing digit. MySQL connections interpolate
parameters, so the digit was appended to each value and a read returned
batches past the offset it asked for.
The orphan comment dropped from the `.proto` still rode the generated
field, which leaves `make proto` dirty.
The `api-go` branch was rebased onto its upstream `main`, which pulls in
newer gRPC and OpenTelemetry requirements through the module graph.
The new api pin ships the nexusoperation proto, so the generated import map has to list it.
The module pins its own grpc version and has to track the root module after the api pin bumped it.
The mixed brain tests live in their own module, so the root go test invocation cannot reach them.
The metric SDK returns a nil slice when no exemplar was recorded and an empty one when the reservoir was already allocated, which made the comparison flaky.
The log is appended to, so a failure from an earlier run kept failing every later verification.
Taken from upstream temporalio/temporal temporalio#11906. The api version this branch pins rejects a system payload that is not binary/protobuf, so the endpoint has to refuse one instead of labelling it.
Payloads live in the CHASM component, so the table, the store methods, the four schema bumps and the scavenger marker had no caller on the serving path. The internal protos reserve the field numbers the log addressing used.
The completion path reached the stream with the request context's engine, which only serves shards this host owns. Registration now resolves its start offset in one transition, and routed calls made under the workflow lock carry a bounded deadline.
Cold replay walks every page of history for the recorded ranges instead of stopping at the first. A range that is truncated, deleted or over the replay budget fails the task with its own cause rather than handing matching a retry. Slices now name the run that holds the stream.
A refused publish or subscribe came back as a plain error, which failed the completion call and left the worker retrying the same command with nothing in History saying why.
The publish commits with the task, so a task that fails after publishing has to leave no event, no offset and no message behind.
Delivery to a workflow drops anything that is not data, so a producer leaving the field unset got an offset for a message no subscriber ever saw.
The batch and the frontier commit in one transition and the execution serializes transitions, so the pin only made an outside producer fail when the workflow published in between. The stripe lock and the separate create step went with it.
The event bounds its batch with a from and a to offset like every other range in this API.
…-reset-failover

# Conflicts:
#	chasm/lib/workflow/stream_admission_test.go
#	chasm/lib/workflow/stream_cursor_test.go
A reset re-ran the reset-point task, but the range that task had been given
from a stream the base run owned lived only in the base run, so the reset
run's first task saw an empty stream where the base run had seen input. The
reset now reads the range off the completion event it does not copy and
seeds the reset run's own stream with those records at their offsets; ranges
recorded before the reset point are still re-supplied from the base run.
…-reset-failover

# Conflicts:
#	chasm/lib/stream/doc.go
A batch copied into a reset run's stream is as old as it was in the base run,
so the copy carries the source's stamp and the bytes count toward what the
new stream holds, the same as an append would.
@moetemp

moetemp commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

Replaced by #18, #19, #20, #21, #22, #23, #24, #25, #26, #27, #28, #29, #30, #31, #32, #33, #34, #35, #36 and #37.

Same content, split into 20 PRs in the v3 series: the notification channel first, then the streaming interface, then native streams and the rest. The branch stays as a pin.

@moetemp moetemp closed this Oct 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant