Skip to content

Stamps at the batch-to-records seams, leave_dynamic under multi-element stamps, and unconsolidated feedback - #855

Open
frankmcsherry wants to merge 10 commits into
master-nextfrom
dd-dynamic-stamps
Open

frankmcsherry wants to merge 10 commits into
master-nextfrom
dd-dynamic-stamps

Conversation

@frankmcsherry

@frankmcsherry frankmcsherry commented Sep 2, 2026

Copy link
Copy Markdown
Member

Two bugs found while running DDIR programs in steady state, the seam that let the first one through, and the driver edit mode and corgi pin that surfaced them.

Where multi-element stamps come from, and where they were leaking. Since timely #813 a message's stamp is a multiset, and DD's batch-shipping operators make multi-element ones: arrange ships each batch under the set of capabilities it retires, reduce likewise, join forwards its input batch's set. In an iterative scope with two epochs in flight, an arrange holding (1, [18]) (epoch 1, round 18) and (2, []) (epoch 2, just entered) retires both in one batch when its input frontier passes both at once. Instrumenting the operators on the run that panicked (a DDIR program at 20k nodes, four workers, epochs fed without waiting) shows the chain: an arrange minted the first multi-element stamp, then a join, then a reduce, then an arrange whose batch reached the scope's exit as (1, [19]), (2, [1]).

The leak was the seam from batches to records: as_collection forwarded a batch's records under the batch's set, so a collection carried messages whose capability spanned two epochs though no record did, and anything reading a message's time as one, cap.time(), panicked with "expected a singleton stamp". StampRouter (collection.rs) now gives one capability per element of a message's stamp and routes each record to the first element at or before its time; it is applied at every seam that makes records from batches: the vec as_collection, count, threshold, the columnar as_recorded_updates, and DDIR's corgi as_collection. With that, collection streams carry one timestamp per message and only batch streams carry sets. Not yet routed: join_with_tactic's output, whose tactics build containers the operator cannot split (the fix belongs in the tactics, a builder per element), and as_container (opaque containers, no users found). tests/dynamic.rs checks the seam with a hand-built batch under two epochs' capabilities.

leave_dynamic itself. All three (DD's row and columnar collections, DDIR's corgi backend) read cap.time() to truncate it. Each now holds a capability per stamp element, truncated exactly as the records are, and opens its session on the set, so it is safe whatever reaches it; a test makes the two-epoch message directly, panics before the fix and passes with it. An audit of DD, DDIR, and dogsdogsdogs found no other InputCapability::time() reads.

Unconsolidated feedback. A DDIR var was set to its step unconsolidated. An iteration quiesces when its feedback carries no records, not when it carries nothing net, so a step whose additions and retractions cancel only logically (a negate under a concat) never quiesced: the vec backend ran forever on a program whose fixpoint was reached in one round. Every program in the suite fed its vars through a reduce, which consolidates by nature, so nothing had seen it. Each var is now set to its consolidated step (Backend::consolidate, through the arrangement by default); the suite's timings are unchanged. The regression test is DD's identifiers algorithm written as a DDIR program (interactive/tests/identifiers.rs): hash, min, negate, and a var give 32 records unique ids over 64 slots.

The ddir driver's rounds edit file-fed rows. With EDGES_FILE, the round loop used to churn synthetic rows the dataflow never saw. Each update now retracts one of the file's rows and re-inserts it with its last field rewired, so a round is an edit to data the dataflow holds.

corgi pinned at WIP 3c4b0d6f, where compare_idx and gather read sum discriminants in place (43s -> 23s on a 200k-node saturation).

Catching up to timely #817, #818, #819 (folded in from #874).
Since #817, InputCapability::time() requires TotalOrder and returns Option, retain is retain_least, and inspect is inspect_core.

  • Collection::inspect_batch hands the closure the batch's Stamp<T>; Collection::inspect_container is inspect_core.
  • Collection::delay no longer uses timely's delay_batch (removed in dogsdogsdogs: one lookup primitive under the delta-join operators #818). It delays each stamp element and each update's time through func and sends at once under the delayed capabilities, which works for partial orders; the T: Hash bound is gone.
  • capture's CDC progress operator holds a CapabilitySet from retain_stamp; upsert takes retain_least if present; the diagnostics loggers take least() if present.
  • dogsdogsdogs half_join inserts each element of retain_stamp.
  • dogsdogsdogs: one lookup primitive under the delta-join operators #818 removed timely's count and delay: the dogsdogsdogs examples count with DD's count, and graphs-static no longer delays records to their times (the reduce acts on record times).
  • Comments no longer describe the removed unary_notify pattern (Cursors migration #819).

Workspace tests (all targets), doctests, clippy, and the explain release test pass against timely master at 4eb3ee09.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SFY2AqDJTLX9TRGRuGR8dQ

frankmcsherry and others added 4 commits September 2, 2026 05:43
Both the row and the corgi `leave_dynamic` read `cap.time()`, which now
panics ("expected a singleton stamp") when a message carries timestamps
from two epochs — late iterations of one alongside early ones of the next,
which any steady-state run of an iterative program produces. Hold a
capability per stamp element, each truncated exactly as the records are,
and open the session on the set.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012k2GSwxmvD2LvckkoXi6GK
The round loop churned synthetic rows from `gen_row`, which with a file-fed
input meant retracting rows the dataflow never saw and inserting random
ones (duplicate ids, cycles). Each update now retracts one of the file's
rows and re-inserts it with its last field rewired below its first, so a
round is an edit to data the dataflow holds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012k2GSwxmvD2LvckkoXi6GK
compare_idx and gather read sum discriminants in place (WIP master
3c4b0d6f), which is what took the e-graph's 200k-node saturation from 43s
to 23s on the corgi backend.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012k2GSwxmvD2LvckkoXi6GK
…a program

An iteration quiesces when its feedback carries no records, so a var whose
step's additions and retractions cancel only logically — a `negate` under
a `concat` — never quiesced: the vec backend ran forever on a program
whose fixpoint was reached in one round. Every var is now set to its
consolidated step (`Backend::consolidate`, through the arrangement by
default). The suite's timings are unchanged; the programs there all fed
their vars through a reduce, which consolidated by nature.

tests/identifiers.rs writes DD's `identifiers` algorithm in DDIR: each
record proposes hash(round, record); among the proposers of an id the
least (round, record) wins and the losers try the next round; with hash,
min, negate and a var, over 64 slots for 32 records, every record gets
its own id (25 settle at round 0, the rest by round 4). It is the
primitive an e-graph rule needs to mint a node for a new term inside the
program.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012k2GSwxmvD2LvckkoXi6GK
@frankmcsherry frankmcsherry changed the title leave_dynamic under multi-element stamps, a driver edit mode, and the corgi pin Two DDIR footguns: leave_dynamic under multi-element stamps, and unconsolidated feedback Sep 2, 2026
frankmcsherry and others added 2 commits September 2, 2026 08:01
…messages come from

Instrumenting the batch-shipping operators on the run that panicked (a
DDIR program at 20k nodes, four workers, epochs fed without waiting) shows
the multi-element stamps being minted: an arrange retiring (1, [18]) and
(2, []) — epoch 1 at round 18 alongside epoch 2 just entered — in one
batch, then a join forwarding its input batch's set, then a reduce
retiring the same pair, and finally an arrange whose batch, forwarded by
as_collection under the retiring set and delayed element-wise by the
feedback, reached the scope's exit as (1, [19]), (2, [1]).

Which retirements coincide depends on scheduling, so the test makes the
message directly: an operator that ships its input under a capability for
epoch e round 3 and one for epoch e + 1 round 0, into leave_dynamic. It
panics with "expected a singleton stamp" before this branch's fix and
passes with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012k2GSwxmvD2LvckkoXi6GK
…ount, threshold route by time

A batch is shipped under the set of capabilities it retires, so a message
of batches carries several timestamps when two epochs retire together.
The operators that turn batches back into records forwarded the records
under that same set, which is how a multi-element stamp reached
`leave_dynamic` and anything else that reads a message's time as one.

`StampRouter` (collection.rs) gives one capability per element of a
message's stamp and routes each record to the first element at or before
its time (a batch's times are at or beyond one of the capabilities it
retired under, so every record has one). It is applied at every seam
that makes records from batches: the vec `as_collection`
(`flat_map_batches`), `count`, `threshold`, the columnar
`as_recorded_updates`, and DDIR's corgi `as_collection` (a chunk under one
capability goes whole, as before; under several it is split by row). With
that, a collection's messages carry one timestamp each; only streams of
batches carry sets.

Not routed: `join_with_tactic`'s output, whose tactic builds containers the
operator cannot split by time (the fix belongs in the tactics, a builder
per element of the fresh batch's set), and `as_container`, which forwards
opaque containers. `leave_dynamic` keeps holding a capability per element,
so it is safe either way.

tests/dynamic.rs: a hand-built batch under capabilities for epoch 0 round
3 and epoch 1 round 0, through `as_collection` into an operator that reads
each message's time as one, lands its records in their two epochs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012k2GSwxmvD2LvckkoXi6GK
@frankmcsherry frankmcsherry changed the title Two DDIR footguns: leave_dynamic under multi-element stamps, and unconsolidated feedback Stamps at the batch-to-records seams, leave_dynamic under multi-element stamps, and unconsolidated feedback Sep 2, 2026
frankmcsherry and others added 2 commits September 11, 2026 16:15
- Collection::inspect_batch / inspect_container: T: TotalOrder (they reveal one time).
- capture.rs CDC progress operator: hold a CapabilitySet via retain_stamp.
- dogsdogsdogs half_join: insert each element of retain_stamp.
- tests: dynamic.rs asserts stamp().len() == 1 explicitly; wcoj_partial_order uses inspect.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012k2GSwxmvD2LvckkoXi6GK
…delay_batch

upsert and the diagnostics loggers take the time if present and drop the message
otherwise. Collection::delay no longer uses timely's delay_batch (now TotalOrder
bounded): it delays each stamp element and each update's time by func and sends
immediately under the delayed capability set, which works for partial orders.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012k2GSwxmvD2LvckkoXi6GK
frankmcsherry and others added 2 commits September 11, 2026 18:10
…y_notify is gone

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012k2GSwxmvD2LvckkoXi6GK
The dogsdogsdogs examples count with DD's count rather than timely's.
graphs-static no longer delays records to their times; the reduce acts on record times.
A doctest drops its unused BranchWhen import.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFY2AqDJTLX9TRGRuGR8dQ
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