Skip to content

[Brook] Wrote a run's buffer before its Local Activity starts. - #29

Closed
moetemp wants to merge 12 commits into
moe/brook/v4/s12-stream-ref-backendfrom
moe/brook/v4/s13-local-activity-flush
Closed

moetemp wants to merge 12 commits into
moe/brook/v4/s12-stream-ref-backendfrom
moe/brook/v4/s13-local-activity-flush

Conversation

@moetemp

@moetemp moetemp commented Oct 22, 2026

Copy link
Copy Markdown
Owner

What changed?

This PR adds the extension call FlushRunBuffer, which writes a run's buffered Workflow calls before a Local Activity of that run starts.
FlushRunBufferRequest { namespace, run_id, workflow_task, timeout } takes the dispatching Workflow Task from ActivityTask.Start field 20. FlushRunBufferResponse { records } answers how many Records it wrote. The crate writes the run's buffered creates and appends in that task's epoch, with the same Producer, epoch fence, dedupe and retry-run rules as the before_complete flush. Closes still wait for an accepted completion.
Core gives a task's withdrawal facts (previous completed task, failed tasks) only with the completion. A batch with no previous completed task would withdraw every earlier task, so the early batch is deferred: Withdrawal.defer, sent to the append script as the completed task key *. It withdraws nothing, flags nothing and keeps no lf facts, but it still fences, dedupes, enters the task index and applies withdraw_run. The memory Store does the same.
The task's completion writes only what was buffered after the early write. Its batches go on from the early batch numbers of the same epoch, so nothing goes in twice and nothing is dropped as a resend. It also sends a batch with the facts, an empty one if needed, to each Stream that the early write reached, so the withdrawal reaches those Streams.
A Local Activity of a task that already completed (for example a retry after a heartbeat completion), or of a task older than an earlier early write, writes nothing. When the early write's task never completes, the next task writes those calls again in its own epoch, and its facts withdraw the old ones. A failed early write fails the run's next completion with the same error, so the retry withdraws what went in. A failed task keeps the batch numbers of its epoch, so a late write in that epoch never reuses a number. One lock per run orders its writes, so a Local Activity that starts while another one's flush runs waits until the run's Records are in. A run with nothing buffered answers at once and needs no Redis.

Why?

Decision S70. Without this, a Local Activity could publish before the Workflow Records that came before it in Workflow code, since the Workflow buffer goes out only when the task completes. The epoch of the dispatching task keeps the existing withdrawal rules, so a Workflow Task that fails after the early write has those Records withdrawn.

How did you test it?

cargo fmt --all -- --check, cargo clippy --workspace --tests -- -D warnings, and STREAMS_REQUIRE=redis cargo test --workspace on standalone Redis, a 3-primary Redis cluster and the memory Store.
New Workflow tests check the order (Workflow Records, then the Local Activity's, then the rest) with no duplicate and with two Local Activities in one task, that the completion takes its facts to a Stream that only the early write reached, that a task that fails after an early write has its Records and its Local Activity's withdrawn by the retry, also with a late flush of the failed attempt, that a Local Activity of a completed task writes nothing, that two Local Activities that start together both see the run's Records before they go on, and that a run with nothing buffered needs no Redis. A capability test checks that a failed early write fails the task's completion with nothing written. A Store test checks the deferred batch on Redis and memory.

@moetemp

moetemp commented Oct 22, 2026

Copy link
Copy Markdown
Owner Author

Review fixes in 78650ea: a deferred batch that withdraws a retry run's previous run writes a Superseded item with no epoch, so a Reader withdraws only that run. Each run buffer has an id that a failed task or an eviction changes, so a write that ends after either one leaves nothing in the new buffer. The completion and the early flush wait for the run's lock only up to their deadline. A Store error of a Workflow flush now names the Stream and the first Record's Topic. New tests cover the flagged deferred batch, a task that fails while its early write runs, and the error text.

@moetemp

moetemp commented Oct 22, 2026

Copy link
Copy Markdown
Owner Author

Review item 4 fixed in the new s13 head: a Workflow batch with facts and no Records goes in also after a close, and a read that reaches the close Record gives the Superseded items written after it just before the close Record. So a task that completes after an auto close or a client close still withdraws a failed task's Records. Test: a_completion_after_a_close_still_withdraws_a_failed_task. Item 3: the stale early rewrite covers a late Local Activity flush of a failed attempt that took the retry's calls (a_task_that_fails_after_an_early_flush_has_its_records_withdrawn). The code comment now names that path.

@moetemp

moetemp commented Oct 22, 2026

Copy link
Copy Markdown
Owner Author

Per the lead's decision, the close rule stays. The post-close withdrawal change (d220473) is reverted on this layer. docs/redis.md now has a Known limits section (added on s11): if a Stream closes between a failed Workflow Task and its retry, the failed task's Records stay visible. A Reader of an auto-close Stream needs Redis write access.

@moetemp

moetemp commented Oct 22, 2026

Copy link
Copy Markdown
Owner Author

Final state, as the lead decided: the post-close withdrawal fix is restored. 7682982 reverts the revert. It now also covers a retry batch with Records: when a close refuses it, the Records are dropped and a batch with only the facts goes in. docs/redis.md has a "Withdrawals after a close" section, and its Known limits say that offsets inside a page may not increase across a close, so Readers resume from next_offset.

@moetemp

moetemp commented Oct 25, 2026

Copy link
Copy Markdown
Owner Author

Closing as stale. This layer is folded into the v5 tip, #41, and the upstream review uses a fresh series instead. Plan: https://www.notion.so/4048fc567738814990b7c796ac28fe3f

@moetemp moetemp closed this Oct 25, 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