From 5d9d046d728467c44d5b807f6e4df376c0216f88 Mon Sep 17 00:00:00 2001 From: Frank McSherry Date: Fri, 23 Oct 2026 23:55:47 -0400 Subject: [PATCH 1/3] DDIR: every import is read as of the time its reader starts A program installed after a published trace had compacted read the trace's history as stored. Before the compaction frontier a batch can hold a retraction advanced to the frontier beside the insertion it retracts, which is a count no time had. An iterative scope seeded from such an import need not converge, and `tick` hung: 3 of 620 differential test seeds with late installs. `Server::import_as_of()` is the epoch before the current one, which is also the frontier `tick` compacts every published trace to. Every import reads as of it: - the corgi arrangement `Arr` carries `since`, and a join or reduce of an import reads through DD's `BackendFrontier` (a join of two imports reads as of the join of their times); - a collection read from an import has its copied time lanes advanced; - programs, bindings, snapshots and tails all import through the same path. The half join reads the arrangement as stored: with the stream at or beyond the as-of time and published times at the root, the rows it meets and the times it produces are the same. An arrangement the program built is read as it is, and an import as of the minimum constructs no wrapper. The differential tests run with late installs (`LATE_INSTALLS`), and `reinstall` with them. `late_install_regressions` runs the three seeds that hung (40000075, 40000160, 40000519) under `split`, `bind` and `reinstall`; without the as-of reads it does not finish. `server_late_import.rs` reads imports as containers, as rows, and as arrangements read by a join, a reduce and a loop, installed late. Co-Authored-By: Claude Fable 5.1 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_019XsakwFGwtESwZ1gSYwG4o --- interactive/server/src/loop_.rs | 2 +- interactive/src/backend/corgi.rs | 98 +++++++++++---- interactive/src/server/bind.rs | 7 +- interactive/src/server/install.rs | 14 ++- interactive/src/server/mod.rs | 20 +++- interactive/src/server/read.rs | 7 +- interactive/tests/difftest.rs | 26 +++- interactive/tests/server_late_import.rs | 151 ++++++++++++++++++++++++ 8 files changed, 285 insertions(+), 40 deletions(-) create mode 100644 interactive/tests/server_late_import.rs diff --git a/interactive/server/src/loop_.rs b/interactive/server/src/loop_.rs index 12f39cbf1..fdca2291c 100644 --- a/interactive/server/src/loop_.rs +++ b/interactive/server/src/loop_.rs @@ -477,7 +477,7 @@ fn start_tail( let tag = reqid.to_string(); let mut probe = ProbeHandle::new(); let shutdown = interactive::server::host_dataflow(worker, |_, scope| { - let (containers, shutdown) = trace.import_containers(scope.clone(), "TailImport"); + let (containers, shutdown) = trace.import_containers(scope, "TailImport", server.import_as_of()); let pending = Arc::new(AtomicUsize::new(0)); let (mut upper, mut dropped) = (None::, false); containers diff --git a/interactive/src/backend/corgi.rs b/interactive/src/backend/corgi.rs index 0d797aa44..5a38936c5 100644 --- a/interactive/src/backend/corgi.rs +++ b/interactive/src/backend/corgi.rs @@ -10,7 +10,7 @@ use timely::dataflow::Scope; use timely::dataflow::channels::pact::Pipeline; use timely::dataflow::operators::generic::Operator; -use timely::progress::Antichain; +use timely::progress::{Antichain, Timestamp}; use differential_dataflow::AsCollection; use differential_dataflow::Collection; @@ -34,7 +34,7 @@ use crate::runtime::half_join::{CorgiHalfJoinBackend, HalfJoinBatcher}; use crate::runtime::join::CorgiJoinBackend; use differential_dogs3::operators::half_join::half_join_with_tactic; use crate::runtime::reduce::CorgiReduceBackend; -use differential_dataflow::operators::int_proxy::{ProxyJoinTactic, ProxyReduceTactic}; +use differential_dataflow::operators::int_proxy::{BackendFrontier, ProxyJoinTactic, ProxyReduceTactic}; use differential_dogs3::operators::half_join::proxy::ProxyHalfJoinTactic; use crate::lambda::{compile_flatmap, compile_join_function, compile_predicate, compile_projection, compile_scalar}; use corgi::{Graph, NumOp, Shape}; @@ -230,7 +230,29 @@ fn apply_ops(mut c: CC, ops: &[LinearOp], level: usize, plans: &[Plan]) -> CC { /// The corgi backend's arrangement: one a program built, or a published export it imported. /// Both are the same trace type at the same time, so every operator reads either in place. -pub(crate) type Arr<'scope> = Arranged<'scope, TraceAgent>; +/// +/// An import is read as of `since`, the time its reader started: a published trace has compacted +/// to that time, from which on its accumulations are correct, and before which its batches can +/// hold counts no time had. Every reader advances the times it draws to `since`: a collection as +/// it flattens the chunks, a join or reduce through DD's `BackendFrontier`. An arrangement the +/// program built is read as it is, which `since` at the minimum says. +/// +/// One time, not a frontier: the server compacts every published trace by the one-element +/// frontier `{(epoch - 1, [])}` and a reader starts at that same element, so the frontier a +/// reader reads as of is `{since}`, which is beyond the frontier its trace compacted by, as DD's +/// `BackendFrontier` requires. +#[derive(Clone)] +pub(crate) struct Arr<'scope> { + pub(crate) arranged: Arranged<'scope, TraceAgent>, + pub(crate) since: Time, +} + +impl<'scope> Arr<'scope> { + /// An arrangement the program built, read as it is. + fn built(arranged: Arranged<'scope, TraceAgent>) -> Self { + Arr { arranged, since: Time::minimum() } + } +} /// The time each coordinate of `t` less one, at least zero: the half join's compaction of its /// arrangement. Every later stream time is at or beyond some such frontier element, and advancing @@ -241,16 +263,20 @@ fn step_back(t: &Time, out: &mut Antichain