Skip to content

runtime-local: extract the local task-drive layer from flowctl - #3416

Merged
jgraettinger merged 4 commits into
masterfrom
johnny/v2-tests-2-runtime-local
Sep 1, 2026
Merged

runtime-local: extract the local task-drive layer from flowctl#3416
jgraettinger merged 4 commits into
masterfrom
johnny/v2-tests-2-runtime-local

Conversation

@jgraettinger

@jgraettinger jgraettinger commented Aug 27, 2026

Copy link
Copy Markdown
Member

Description:

Stack 2 of 5. flowctl raw preview-next and the forthcoming catalog-test runner need the same thing — run a runtime-next task locally with synthetic shards, no Gazette broker, no etcd, no Go consumer — and differ only in three runtime-next host seams. This moves that machinery into a new runtime-local crate rather than let a second copy grow beside it, migrating flowctl onto it in the same commit so no divergent copy ever exists.

Four commits:

  • tests: move preview snapshots to tests/preview — re-points the flowctl preview snapshot tests at raw preview-next. The baseline the extraction is checked against.
  • labels: hoist even shard splitting into labels::shard::even_splits — four places computed the same even split of the u32 key space. runtime-local is the fifth caller, so the helper lands first.
  • runtime-next: shard handlers own their RocksDB directory — a shard now owns and removes its own storage, which is what makes a dropped local run safe, plus the protocol changes that follow from it.
  • runtime-local: extract the local task-drive layer from flowctl — the move itself.

Workflow steps:

flowctl raw preview-next behaves identically. poetry run pytest tests/preview runs the snapshot suite — it is not wired into CI, so it's a developer-run baseline; CI coverage for this stack arrives in #3417.

Documentation links affected:

None. crates/runtime-local/README.md is new; tests/README.md becomes a roadmap over preview/ and soak/, with the detailed instructions moving to tests/preview/README.md.

Notes for reviewers:

⚠️ The RocksDB-ownership commit does not build on its own. flowctl raw preview-next is broken against the new SessionLoop until the commit after it, which extracts that layer and adopts all of it. Teaching a soon-to-be-replaced copy semantics it will never use wasn't worth it, so the two are separable stories but not separable builds. Review them as a pair; the PR tip builds.

Ownership is the change with consequences outside this stack: a controller can no longer open a shard's RocksDB by path once its stream ends, so --initial-state and --output-state move onto the protocol, Stopped loses Copy, and Go's removeRecoveryDir becomes a no-op wherever a descriptor was sent.

The extraction is otherwise behavior-preserving. Three things worth a close look:

  • The two generalized seamsControls over its publisher and logger factories, and build_shuffle replacing fixture: bool. The latter is what keeps journal authorization in flowctl: reading live journals needs a logged-in user token, and only flowctl has one.
  • Clock policy stays with the caller, because preview must keep reproducing the legacy harness's 3600 * ordinal + index stamping while the test runner paces clocks against its own synthetic time.
  • A real defect fixed in passingRun documented that its server task aborts on drop, but a plain JoinHandle detaches rather than aborts, so the tonic server outlived every Run.

Stack — each PR is based on the one above it, so review only the top commits of each.

  1. Housekeeping and prep for the catalog-test harness #3415 — Housekeeping and prep (merged)
  2. runtime-local: extract the local task-drive layer from flowctl #3416 — runtime-local: extract the local task-drive layer ← you are here
  3. catalog-tests: the catalog test harness, and flowctl raw test #3417 — catalog-tests: the harness, and flowctl raw test
  4. control-plane-api: run publication tests on catalog-tests #3418 — control-plane-api: run publication tests on catalog-tests
  5. Remove the V1 catalog-test machinery #3419 — Remove the V1 catalog-test machinery

jacobmarble
jacobmarble previously approved these changes Aug 28, 2026
/// frontier: a [`TxnState::new`] -> [`push_binding`] -> [`finish_txn`] sequence
/// over `items`, each of which carries the binding it feeds and its clock.
#[allow(clippy::too_many_arguments)]
pub fn write_transaction_for_bindings(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This must be called by a subsequent change in the stack, I can't find any call to it in this change.

@jgraettinger
jgraettinger force-pushed the johnny/v2-tests-2-runtime-local branch from 38a589b to a2d44ab Compare August 30, 2026 17:56
Base automatically changed from johnny/v2-tests-1-housekeeping to master August 31, 2026 21:14
@jgraettinger
jgraettinger force-pushed the johnny/v2-tests-2-runtime-local branch 3 times, most recently from e392a76 to a4ad7c4 Compare August 31, 2026 22:19
@jgraettinger
jgraettinger force-pushed the johnny/v2-tests-2-runtime-local branch 2 times, most recently from 2d160a7 to 13ac318 Compare September 1, 2026 00:51
@jgraettinger

Copy link
Copy Markdown
Member Author

@jacobmarble PTAL

@jgraettinger
jgraettinger force-pushed the johnny/v2-tests-2-runtime-local branch from 13ac318 to bf1c380 Compare September 1, 2026 04:46
Relocate the `flowctl preview` snapshot tests into `tests/preview/`, a sibling
of `tests/soak/`, and re-point them at `flowctl raw preview-next`.

Three mechanical consequences of the swap:

* Fixtures become NDJSON: `raw preview-next` reads `["collection",<doc>]` lines
  separated by `{"commit": true}` markers, where legacy `preview` read a nested
  JSON array of transactions. Transaction boundaries are preserved, and the
  files are renamed `.ndjson`.

* `pytest-insta` derives snapshot names from the test node ID, so the added
  path segment renames every snapshot.

* Legacy `preview` printed bare documents for derivations while
  `raw preview-next` uses `["collection",<doc>]` framing everywhere, so the two
  derive snapshots are regenerated. Stripping the new wrapper reproduces the old
  files exactly.

`tests/README.md` becomes a roadmap over `preview/` and `soak/`.
Four places computed the same even split of the `u32` key space from scratch:
`activate`'s initial-splits path, and three shuffle test harnesses which were
literally identical.

`even_splits` returns the range, its encoded LabelSet, and the full shard ID for
each of `key_splits * rclock_splits` shards tiling the cross product, ordered
key-major — which is also the lexicographic order of their IDs. Callers that
want only one dimension pass 1 for the other.

The copies' `if i == 0` / `if i == count-1` end guards go away: computing widths
in `u64` over a `u32::MAX + 1` space makes the tiling exact at both ends on its
own, for any count. A snapshot test pins the concrete IDs and ranges, and a loop
asserts the general property.

Behavior-preserving at every call site.
A `SessionLoop`'s descriptor now hands its directory over. The serve loop removes
it on every exit path — graceful stop, session error, and failed open alike —
always after the RocksDB is torn down (`shard::rocksdb::OwnedDir`). An absent
descriptor gets a tempdir the shard makes and owns identically, so ownership is
unconditional and a shard is drop-safe: unwinding at any point takes its storage
with it.

Go's `removeRecoveryDir` is then a no-op wherever a descriptor was sent. What
remains are the cases with no handover: derive-sqlite, which keeps its directory
and reaches Rust through `Task.sqlite_vfs_uri`, and a task which died before its
SessionLoop was consumed.

The cost is that a controller can no longer open a shard's RocksDB by path once
its stream ends — which is how `flowctl preview` seeded `--initial-state` and read
back `--output-state`. Both move onto the protocol:

- `SessionLoop.initial_connector_state_json` establishes a base document before
  any recovery scan can observe it.
- `SessionLoop.report_final_state` attaches the shard's reduced connector state
  to each `Stopped` it sends its controller.

A harness now names no path at all and takes the built-in tempdir handling.
`Stopped` gains a field and so loses `Copy`, which is the churn in the leader and
shard actors.

`shard/service.rs` also states the rule its controllers depend on: only the
`spawn_*` adapters put an `Err` on a response stream, and only once `serve` has
returned, so a stream's `Err` is the whole loop's outcome rather than a
recoverable per-session hiccup.

`flowctl raw preview-next` does not build at this commit; the next commit
extracts that layer and adopts all of this.
`flowctl raw preview-next` and the forthcoming catalog-test runner need the same
thing — run a runtime-next task locally with synthetic shards, no Gazette broker,
no etcd, no Go consumer — and differ only in the three runtime-next host seams.

Moved out of crates/flowctl/src/raw/preview_next/: the three drivers, the
run-scoped services, the shard topology, and the segment-writing half of
fixture.rs (as segments.rs). flowctl is migrated onto the crate here, in the same
commit, so no divergent copy ever exists.

Two seams had to be generalized so the layer could stop knowing about preview:

- `Controls` is now generic over the publisher and logger factories.
- `start_with_shuffle_leader` takes a `build_shuffle` callback instead of a
  `fixture: bool`. This is what keeps journal *authorization* in flowctl: reading
  live journals needs a logged-in user token, and only flowctl has one.

The split of fixture.rs follows the same line: flowctl keeps the file format and
session planning, runtime-local takes the writer. Clock policy stays with the
caller — `write_transaction_for_bindings` accepts each document's clock — because
preview must keep reproducing the legacy harness's `3600 * ordinal + index`
stamping while a test runner paces clocks against its own synthetic time. That
routine also tags documents per *binding* rather than per collection, which the
catalog-test harness needs to honor per-transform read delays.

The synthetic topology is rebuilt on shared pieces: ranges from
`labels::shard::even_splits`, and shard IDs from `assemble::shard_id_prefix`
under generation zero, so a local run's shards take the shape they would carry in
production rather than `preview-derive/<task>/000`. Each driver likewise names
its runtime task after the task's own catalog name, which is what ops logs
attribute against.

`Controls` carries `--initial-state` and `--output-state` on shard zero's
`SessionLoop` rather than reaching into RocksDB by path, so no `Run` hosts a
RocksDB directory and each driver returns the final state its shard zero
reported at `Stopped`. `tests/connector_state.rs` covers that round trip.

Otherwise behavior-preserving, but for one defect the move made legible. `Run`
documented that its server task aborts on drop, but `_server_task` is a plain
`JoinHandle`, and dropping one *detaches* its task rather than aborting it — so
the tonic server outlived every `Run`. Add the `Drop` the comment assumed.
@jgraettinger
jgraettinger force-pushed the johnny/v2-tests-2-runtime-local branch from bf1c380 to 8426df8 Compare September 1, 2026 15:34
@jgraettinger
jgraettinger merged commit 73aaf5f into master Sep 1, 2026
11 checks passed
@github-actions github-actions Bot added pending:agent Merged, in the control-plane-agent image, and not yet rolled to flow-agent pending:flowctl Merged, changes the flowctl binary, and not in a published release pending:agent-api Merged, ships via Deploy agent-api, and not yet deployed labels Sep 1, 2026
@jgraettinger
jgraettinger deleted the johnny/v2-tests-2-runtime-local branch September 1, 2026 16:19
@github-actions github-actions Bot removed pending:agent-api Merged, ships via Deploy agent-api, and not yet deployed pending:agent Merged, in the control-plane-agent image, and not yet rolled to flow-agent labels Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pending:flowctl Merged, changes the flowctl binary, and not in a published release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants