Merge gh/main (ec5112cf) into dbsp/source-trait

This commit is contained in:
Claude (dbsp-sources)
2026-10-10 18:51:36 -07:00
committed by Edward Langley
25 changed files with 2782 additions and 99 deletions
+20
View File
@@ -172,3 +172,23 @@ host, and the old reply then arrives. Rival: the `left` check removed.
every restart and swap. Rival: the `left` check removed fails it. The older-generation guard on its own is
not separable from `left` in the histories the property reaches, so it is held by
`a_late_window_reply_from_an_older_generation_does_not_regress` alone.
## The left-epoch livelock
### a_late_reply_from_a_host_the_client_never_joined_cannot_strand_it
The property test failed with `Exhausted` on a shrunk history of two restarts and four writes. Cause: the
client was on host 3, never having joined host 2; a late host-2 reply was applied (an unknown epoch is
accepted), which wrote host 3 into `left`, and every later answer from the live host 3 was then refused as
a duplicate. The test replays that history by hand: three hosts, the host-2 reply lost in flight, the client
settling on host 3, the late reply applied, the connection lost, and a sync that must settle on host 3's
grid. Rival: `apply_answer` treated like `apply` (the `left.retain` removed) fails this test. **The random
property does not catch it**: it passed 11 of 11 runs under that fault, and again after the late reply was
made an explicit event (a stashed reply from a host the client never joined, replayed before a lost
connection), because the property rarely produces a reply from a host the client never joined. This
test is the pin. Rival: the left-epoch guard removed fails
`a_reply_held_across_a_host_restart_cannot_pull_the_client_back` and the property, as before.
The property's invariant changed with it. "The watermark never returns to an epoch it left" was false by
design once an unknown-epoch reply can move the client, so the property now checks the narrower, true
rule: a replayed reply from an epoch the client was in and has left must be a duplicate.
+55
View File
@@ -166,3 +166,58 @@ A job outlives its circuit; sixty-four new circuits (some at the old address) al
Retyping `ZsKeepFn`'s return in the header fails. The `unary_op!` signatures are now read
from the macro body rather than written into the test.
## Transactions (improvise-bw4p.5)
Rivals were injected one at a time in `engine.rs` and watched fail by name; Miri passes on `abi`.
### a_rollback_after_a_failed_step_revives_the_circuit_in_its_old_state
An integrate held at `i64::MAX`; inside a transaction a step overflows (`ZS_ERR_OVERFLOW`, then
`ZS_ERR_POISONED`), `commit` of it is refused as poisoned, and `rollback` revives the circuit with
the integral where it was: `-1` then gives `MAX - 1`, with neither the failed `+1` nor the key
added beside it. Rivals: a rollback that leaves the poison on, and one that does not roll the
circuit back, fail this test and the callback one; a `commit` that does not check the poison
fails this test.
### a_committed_transaction_keeps_its_steps_and_a_new_one_may_follow
`commit` keeps the step and its sink, and a later transaction can be opened and rolled back.
### a_failed_callback_step_is_rolled_back_too
A failure the callback only reports (the step returns, the fault is noted) is rolled back like a
panic: the sink holds nothing after the rollback.
### transaction_misuse_is_a_code_and_changes_nothing
`ZS_ERR_TRANSACTION` for `commit` or `rollback` with none open, a second `begin`, and a `take`
while one is open (nothing written to the out-pointer). A rollback after a poison from outside any
transaction is also refused and the circuit stays poisoned. Rivals: `take` allowed in a
transaction, and a second `begin` allowed, each fail this test.
## Review of #355 at 51438911
### f1_rebuild_begun_before_a_txn_continued_inside_and_rolled_back
Through the ABI, a job continued inside a rolled-back transaction returned 0 with `done` 1 and
a partial load. It is now `ZS_ERR_INTERRUPTED`.
### f4_a_failed_rebuild_step_in_a_txn_then_rollback_then_resume
A job whose first chunk failed inside the transaction resumed without that chunk; now
`ZS_ERR_INTERRUPTED`.
### f2_a_rollback_from_inside_a_callback_is_refused_and_the_txn_stays_open
`ZS_ERR_POISONED` from the callback's rollback, and the transaction still commits.
### f3_begin_on_a_circuit_poisoned_outside_a_txn_is_poisoned
`begin` is `ZS_ERR_POISONED`; `commit` and `rollback` are `ZS_ERR_TRANSACTION`.
### f5_a_chunk_whose_push_overflows_in_a_txn_then_rollback_is_interrupted
The same path through the ABI: `zs_rebuild_step` returns `ZS_ERR_OVERFLOW` from the chunk's
push, `zs_circuit_rollback` succeeds, and the next `zs_rebuild_step` was a silent success; it is
now `ZS_ERR_INTERRUPTED`. Rival: the mark before the push removed fails `[f5]`.
+167
View File
@@ -429,3 +429,170 @@ A second job's first chunk is a tick, so the first job's next call panics.
### a_finished_rebuild_and_an_interrupted_check_do_not_trip_on_later_ticks
A finished job is never interrupted, and steps afterwards do not make it panic.
## `crates/zset/tests/{cost,rollback,rebuild}.rs`: rollback, the borrowed piece, the bound rebuild (improvise-bw4p.5, .11, .12)
Every rival below was injected one at a time and watched fail by test name (and tag where one
exists) before being reverted; the whole suite was re-run clean afterwards. Costs are counted
in key clones and live keys of a `Counted` key type, never in time, so they hold on any machine.
### an_eval_borrows_its_input_instead_of_cloning_it
bw4p.11. `input -> map(|k| k.0) -> output` over 100 keys makes **0** key clones in a step; before
the change it made 200 (the source's whole-tick copy and `piece()`'s clone). Rival: `Unary::eval`
clones the piece again fails `[piece]`.
### an_integrate_stream_holds_its_integral_twice_when_the_tick_is_whole
bw4p.11. After one step an `input -> integrate` pipeline holds 3N live keys (the input's
piece, the integral, the integrate stream's piece), where it held 5N before: the whole-tick copy
that duplicated the piece is kept only while a tick is split (`Slot::whole` is an `Option`).
Rival: a publish that always stores `whole = Some(v.clone())` fails `[whole]` (and the clone test).
### a_transaction_costs_the_keys_a_step_touches_and_not_the_size_of_the_circuit
bw4p.5. A pipeline with `distinct`, `delay`, `join_indexed`, `aggregate` and three sinks is
loaded with 10, then 300 keys, then given a two-key delta in each of two inputs. The extra key
clones of `begin; step; commit` over the same step outside a transaction are **16** at both
sizes (22 for `rollback`; the plain step is 20). Rival: `DistinctState::begin` clones what it
holds (a snapshot of the world) fails `[cost]` with 28 against 318. A `delay` clones what it
holds, the previous tick's input, so the test steps a small delta after the load. Review of
#355: the pipeline now includes an `integrate`; a rival `Acc::begin` that clones the integral
fails `[cost]`. (An `integrate` whose output is sunk is left out: its output is the integral,
so the sink logs all of it, which is the delta and not an overhead.)
### a_rolled_back_step_leaves_the_circuit_as_it_was_and_the_ticks_after_unchanged
bw4p.5, the property. A rig with every stateful operator (integrate, delay, differentiate,
distinct, join, aggregate, plus and eight sinks) runs a random prefix, then a key preloaded at
`i64::MAX - 1`. The failing step adds `(0,0): 1` and then `BIG: 5`, so operators have applied
the first key when the second overflows (the test asserts the step panicked iff it was meant
to). After `rollback`, every stream's `current()` and every sink equal what they were (`[rollback]`),
and the ticks after equal a reference circuit that never saw the step (`[post]`). The same test
rolls back a step that succeeds. `carry` is pending at `begin` and must stay pending.
Rivals, each failing this test: rollback that skips every node (`[rollback]`, and
`an_operator_built_inside_a_transaction_goes_back_to_empty`); an integrate log not undone
(`[post]`); a stream slot not restored (`[rollback]`); a `delay`'s held value not restored
(`[post]`); a source's pending delta not restored (`[post]`); a sink not undone (`[rollback]`);
`DistinctState` not undone (`[post]`); a trace not undone (`[post]`); an undo that drops the sign
(`[post]`); a log that records nothing (`[rollback]`, `[built]`).
### a_committed_transaction_equals_the_same_steps_outside_one
bw4p.5. A circuit that commits some of its steps equals one that never opened a transaction,
state and output, tick by tick. Rival: `commit` rolls back fails `[commit]`.
### rollback_undoes_the_clock
bw4p.5. A rolled-back step does not count as a tick. Rival: `ticks` not restored fails `[ticks]`.
### an_operator_built_inside_a_transaction_goes_back_to_empty
bw4p.5. An `integrate` built after `begin`, stepped, then rolled back, forgets the step
(`[built]`). Without the "a node built in a transaction starts recording" rule it kept it.
### rollback_undoes_newest_first_so_no_undo_leaves_i64
bw4p.5. A key at `i64::MAX` lowered by one and raised by one in one transaction. Undone oldest
first the first undo would reach `MAX + 1` and panic; undone newest first nothing leaves
`i64`. Rival: the log replayed oldest first fails this test (and no other: the property above
cannot see it, since its keys never sit at the boundary).
### a_minimum_weight_applied_to_every_kind_of_state_is_taken_back
bw4p.5. `i64::MIN` has no negation, so a trace undoes it as `i64::MAX` then `1`. Rival: the
second application dropped fails `[min]`.
### a_second_begin_is_refused_loudly
bw4p.5, with `a_rollback_with_nothing_open_is_refused_loudly` and
`a_commit_with_nothing_open_is_refused_loudly`: misuse panics, as an overflow does.
### a_rebuild_stepped_on_a_foreign_circuit_is_refused_loudly
bw4p.12. A job made from circuit A's input, stepped on circuit B, used to report Done and land
its chunk in B's next tick. It now panics. Rival: the check removed fails this test.
### a_rebuild_knows_the_circuit_its_input_came_from
bw4p.12. `Rebuild::belongs_to`, the question a host asks before the panic. Rival: always true
fails `[owner]`.
### an_interrupted_rebuild_stays_interrupted_and_a_new_job_loads_the_snapshot
bw4p.12. Documents, and pins, that an interrupted job can never finish: later steps never
clear it, and a new job over the same snapshot loads it. It did not fail before: the behaviour
was right and unstated.
## Review of #355 at 51438911 (improvise-bw4p.5, .12)
An adversarial review found a rebuild across a rollback losing data, a panicking `Clone`
mutating unlogged state, and two cost claims that did not hold. Its reproductions are adopted
as tests; each rival below was injected and watched fail by name.
### r1_rebuild_begun_before_txn_continued_inside_then_rolled_back_reports_interrupted
A job begun outside a transaction and continued inside, then rolled back, was not interrupted;
finishing it gave `{1,3,4}` for the snapshot `{1,2,3,4}`. Rival: `rollback` does not count the
undone step fails this and r2 to r4.
### r2_rebuild_finished_inside_a_rolled_back_txn_is_reported_done_with_nothing_loaded
A job that finished inside a rolled-back transaction was Done with an empty integral and not
interrupted.
### r3_an_interrupted_rebuild_resurrects_after_rollback_and_a_later_step
`rollback` rewinds `ticks`, so a later `step` made a dead job's recorded tick current again and
it continued without its first chunk.
### r4_rebuild_whose_first_chunk_panics_in_a_txn_then_rolled_back
A first chunk that panicked left `began_at` unset, and the job resumed without it. The count is
now recorded before the chunk runs. Rival: recording it never (the `get_or_insert` removed)
fails all four; a rival recording it after the chunk was not run separately.
### b1_a_closure_that_reads_a_stream_during_eval_does_not_panic
The borrowed `piece()` does not conflict with a closure that reads streams during an eval,
including its own source, inside a transaction. It passed before and after.
### c1_a_clone_panic_anywhere_in_a_step_is_rolled_back
Sweeps a panic over every key clone of one transactional step through `integrate`, `distinct`,
`join`, `delay`, `aggregate` and sinks; 138 panic points left state that differed after the
rollback. The post-rollback tick now pushes the keys the failed step touched, because a leaked
key is only visible when it is touched again (a rival that mutated `DistinctState` before
cloning its entry survived the first version). Rivals, each failing it: `add_logged`,
`DistinctState::step` and `Undoable::apply` each mutating before cloning the entry.
### a_panicking_value_clone_leaves_a_memtrace_unchanged
`MemTrace::apply` inserted an empty group before cloning the value. Rival: that order restored
fails `[c2]`.
### reviewer_rollback_matches_a_circuit_that_never_ran_the_step
The reviewer's model-based property (random circuits over every public operator, a custom
panicking trace, random push/step/continue/begin/commit/rollback histories, operators built in
a transaction), at the default 256 cases here (30,000 passed in review). Rival: the continued
path saving the slot without its `whole` fails it.
### a_rollback_across_a_split_tick_restores_the_whole_value
The tick is split before the transaction opens, so every edge holds a `whole` when the first
continued piece saves it. Rival: that save dropping `whole` fails `[split]` (an earlier version
split the tick inside the transaction and never reached the save).
### a_split_tick_in_a_transaction_costs_the_size_of_the_edge
Pins the limit `zset.md` states: a first continued piece in a transaction clones the edge's
whole value, growing with the state.
### p1_a_chunk_whose_push_panics_in_a_txn_is_counted
Delta review of #355 at b2445fed. `Rebuild::step` takes the chunk from its iterator and pushes
it before stepping; a push that overflows the pending delta panicked with no step run, so
`undone` was not bumped and the resumed job silently dropped the chunk (key 1's `+1`). The step
is now marked before the push. Rival: that mark removed fails `[p1]`.