join: do not drop in-flight batches overtaken by advance_upper (#801) - #802
Merged
frankmcsherry merged 4 commits intoJul 15, 2026
Merged
Conversation
Bump @babel/core to 7.29.7 and brace-expansion to 5.0.7, clearing the open dependabot advisories. vite was already at the patched 8.0.16. npm audit reports 0 vulnerabilities; the console builds clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
join_with_tactic skipped arriving batches unless `acknowledged <= batch.lower()`, treating everything below `acknowledged` as pre-loaded data already joined at start-up. But `acknowledged` also advances via `advance_upper`, which walks the shared trace's merged, compacted batches: a region whose updates consolidate to zero once logical compaction equates their times reads as empty there, so `acknowledged` could leap over batches still in flight on the input stream. Their raw, non-empty forms then failed the guard on arrival and were silently dropped, and their cross-products against the opposing trace were never computed. The trace's emptiness over such a region is a valid summary only for readers at or beyond the compaction frontier. Iterative dataflows read finer times than that: in propagate_core's rotated loop the reduce accumulates proposals at coordinates that remain open long after the join's input frontiers have moved past them, so the dropped cross-products left the loop inconsistent with its own history. The loop's self-correction then either damped, terminating with output that disagrees with a sequential oracle, or entered an undamped correction cycle that never converges, with interesting-times work growing until livelock. These are the wrong-output and hang modes of TimelyDataflow#801; the trigger is batch-boundary placement racing trace merges, which contention perturbs, hence the apparent scheduling nondeterminism (a single worker with delayed self-delivery suffices). Instead: snapshot the acknowledged frontiers after start-up loading, and skip only batches wholly at or before them (the pre-loaded data replay). Every other non-empty arriving batch is joined against the opposing trace, even when `acknowledged` has overtaken it, and `acknowledged` never regresses. Each batch is still joined exactly once: `acknowledged` can only overtake an in-flight batch via `advance_upper`, which requires the region to have consolidated to empty in the trace, so every opposing work item saw zero contribution from that region regardless of interleaving. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reinstates the prioritized SCC variant removed in anticipation of exactly the propagate_core nondeterminism fixed in the previous commit, and routes the scc_at tests through the library's strongly_connected_at against the sequential oracle. Under multi-worker execution with CPU contention these tests exercised the dropped-batch bug at a 15-40% failure rate per run (wrong output and hangs); with the fix they pass reliably. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
frankmcsherry
force-pushed
the
dd-801-join-fix
branch
from
July 15, 2026 19:15
b59f826 to
e2d89d9
Compare
Both frontiers an arriving batch is compared against are drawn from the lattice of stream batch boundaries: `preload_upper` is the trace upper at construction, and `acknowledged` holds received batch uppers or `advance_upper` targets, which are uppers of trace merges of whole stream batches (physical compaction held at `acknowledged` keeps merges from crossing it). A batch spanning either admits no safe continuation: the preload case has already partially double-counted at start-up, and the acknowledged case is mis-accounted by whichever branch it takes. Hard asserts rather than debug: the checks are two small antichain comparisons per batch, and continuing with known-incorrect accounting is worse than aborting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
frankmcsherry
force-pushed
the
dd-801-join-fix
branch
from
July 15, 2026 20:35
276df85 to
f8f1645
Compare
antiguru
added a commit
to antiguru/materialize
that referenced
this pull request
Jul 15, 2026
`mz_join_core` skipped incoming batches by testing `batch.lower()` against the mutable `acknowledged` frontier. `advance_upper` can move `acknowledged` past an in-flight batch when trace merges consolidate the batch's updates away, for example an add/remove pair collapsing once logical compaction equates their times. The trace is legitimately empty there only for readers at or beyond the compaction frontier, but our consumers may read finer times where the batch's updates are still real and unaccounted for. Testing against `acknowledged` therefore dropped those batches and silently lost updates. Capture fixed `preload_upper` frontiers at start-up and gate the skip on them instead, and only advance `acknowledged` forward when a batch sits at or beyond it. This mirrors the upstream fix in TimelyDataflow/differential-dataflow#802 (issue #801), which our fork of `JoinCore::join_core` had inherited. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
antiguru
added a commit
to MaterializeInc/materialize
that referenced
this pull request
Jul 15, 2026
Our `mz_join_core` is a fork of differential's `JoinCore::join_core` and inherited a correctness bug fixed upstream in TimelyDataflow/differential-dataflow#802 (issue #801). The operator skipped incoming batches by testing `batch.lower()` against the mutable `acknowledged` frontier. `advance_upper` can advance `acknowledged` past an in-flight batch when trace merges consolidate the batch's updates away, e.g. an add/remove pair collapsing once logical compaction equates their times. The trace is legitimately empty there only for readers at or beyond the compaction frontier, while our consumers may read finer times where the batch's updates are still real. Testing against `acknowledged` therefore dropped such batches and silently lost updates. The fix captures fixed `preload_upper` frontiers at start-up and gates the skip on them, and advances `acknowledged` only when a batch sits at or beyond it. This is a one-to-one port of the upstream change. No regression test: deterministically reproducing the race requires trace merges to consolidate an add/remove pair while a batch is in flight and `advance_upper` to then jump ahead, which is timing-dependent and not reliable in testdrive/sqllogictest. Upstream #802 added no direct join test either. Correctness rests on mirroring the upstream reasoning. Note: the alternate `DifferentialDataflow` join implementation calls upstream `join_core` from crates.io `differential-dataflow` 0.24.0, which still carries the bug and will be fixed by a future dependency bump. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 15, 2026
frankmcsherry
added a commit
to frankmcsherry/differential-dataflow
that referenced
this pull request
Jul 23, 2026
…at proxy joins Audit of whether the int-proxy join carries the bug fixed in TimelyDataflow#802 (in-flight batches overtaken by advance_upper, silently dropped): * The bug lived in join_with_tactic's driver — the acknowledged- frontier admission guard — which both tactics share, and this branch is based past the fix (59dfd3d, f8f1645); its only diffs to join.rs are a derive and module visibility. The proxy path inherits the fix. * The tactic-level assumptions survive the fix's new admission rule: `meet` is the timely message capability, which lower-bounds the fresh batch's times on the overtaken path exactly as elsewhere (overtaking changes which batches are admitted and what the opposing trace reads through, not the capability relationship), so the backend's advance-by-lower algebra and the exactly-once argument (driver-level, tactic-agnostic) are unaffected. * Empirically: SCC_STAGGER=1 switches both the proxy trim and the oracle to the prioritized strongly_connected_at shape (labels introduced in rounds — the TimelyDataflow#801 trigger), and the oracle-validated runs at the regression suite's sizes (10/20 x 1000 single-edge rounds, 100/200 x 10, 100/2000) with three workers all pass: exact agreement at every time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
join_with_tacticmaintains per-inputacknowledgedfrontiers and skips arrivingstream batches unless
acknowledged ≤ batch.lower(), on the theory that anythingbelow
acknowledgedis "pre-loaded data" already joined at operator start-up.But
acknowledgedis also advanced byTraceReader::advance_upper, which walks theshared trace's current batches — a merged, logically compacted view. A batch region
whose updates consolidate to zero under compaction (e.g. an add/remove pair whose
distinct times collapse to one once the compaction frontier passes both) reads as an
empty batch there, so
advance_upperleaps it — while the raw, non-empty streambatches for that region are still in flight. On arrival they fail the guard and were
silently dropped, and their cross-products against the other input were never computed.
Dropping them is sound only for consumers reading at or beyond the compaction
frontier. Iterative dataflows read finer times than that: in
propagate_core'srotated loop, the reduce accumulates proposals at (outer, round) coordinates that
remain open long after the join's inputs' frontiers (which drive compaction) have
moved past them. A dropped batch there leaves the loop's state inconsistent with its
own history; the self-correcting reduce then chases the discrepancy around the loop.
When the chase damps, the run terminates with output that disagrees with a sequential
oracle at a few (late) times; when it does not damp, it becomes a period-2 limit cycle
that never converges — pending "interesting times" pile up across every live outer
round, each retire gets more expensive, and the computation livelocks. These are the
"wrong-complete" and "hang" modes of #801, one bug.
The trigger requires only batch-boundary placement, not multi-worker races: a
single-worker deterministic simulation reproduces both modes. Under real execution it
surfaces as schedule-dependence (CPU contention perturbing delivery), hence the
apparent nondeterminism.
The fix
preload_upper1/2after the start-up loading loops; arriving batcheswholly at-or-before them are the replayed pre-loaded data, and are still skipped.
its current
acknowledged, even when this input'sacknowledgedhas overtaken it.acknowledgedis never regressed: it advances via received uppers when they arebeyond it, and via
advance_upperas before.Each batch is still joined exactly once:
acknowledgedcan only overtake anin-flight batch via
advance_upper, which requires the region to have consolidatedto empty in the trace — so every opposing work item that consulted the trace saw zero
contribution from that region, regardless of interleaving, and processing the raw
batch on arrival contributes each cross-product exactly once.
Validation
strongly_connected_at(rotatedpropagate_at) SCC tests, 3 workers, under heavyCPU contention: previously ~15-40% wrong-output/hang per run; now clean across
40+ contended executions of the full test binary.
message delivery, atop the timely
Simulationwork): previously 100% of seedslivelocked at 1000 input rounds and diverged (updates at SCC iteration 100+,
propagate round 1400+, on a 10-node graph); now all seeds complete and match the
sequential oracle, including the chaos-free schedules that previously diverged.
differential-dataflowanddogsdogsdogstest suites pass.Notes
reduce_with_tactic) tolerates late batches by construction —its retirement upper joins in the input frontier, which in-flight messages hold
back, so it cannot skip content the same way. There is a theoretical path to a
transiently malformed retirement interval via late empty batches regressing
upper_limitwhen merges misalign withadvance_upper's exact-boundary check;not observed, flagged for a follow-up audit.
count/thresholduseadvance_upperonly to advance compaction, and processevery received batch; they do not have the skip guard.
Fixes #801.
🤖 Generated with Claude Code