From c9edba9fa68ffd6af97c6a98d0480ca608517e49 Mon Sep 17 00:00:00 2001 From: Parker Timmerman Date: Sat, 15 Aug 2026 20:44:15 +0000 Subject: [PATCH] start, emit an empty 'RecordedUpdates' in the columnar 'ValDistributor', when the input consolidates to nothing --- .../src/columnar/collection/exchange.rs | 17 ++++ differential-dataflow/tests/columnar.rs | 80 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 differential-dataflow/tests/columnar.rs diff --git a/differential-dataflow/src/columnar/collection/exchange.rs b/differential-dataflow/src/columnar/collection/exchange.rs index ff71b55a6..a6f5173a9 100644 --- a/differential-dataflow/src/columnar/collection/exchange.rs +++ b/differential-dataflow/src/columnar/collection/exchange.rs @@ -22,6 +22,7 @@ pub struct ValDistributor { marker: std::marker::PhantomData, hashfunc: H, pre_lens: Vec, + worker: usize, } impl FnMut(columnar::Ref<'a, U::Key>)->u64> Distributor> for ValDistributor { @@ -68,6 +69,21 @@ impl FnMut(columnar::Ref<'a, U::Key>)->u64> Distributor 0 { + let mut recorded = RecordedUpdates:: { + updates: Default::default(), + records: total_records, + consolidated: true, + }; + // Push the empty update to the worker that produced the original + // values so the send stays local. Not needed for correctness, but + // a reasonable choice. + Message::push_at(&mut recorded, time.clone(), &mut pushers[self.worker % pushers.len()]); + return; + } + let mut first_records = total_records.saturating_sub(non_empty.saturating_sub(1)); for (pusher, output) in pushers.iter_mut().zip(outputs) { if !output.keys.values.is_empty() { @@ -108,6 +124,7 @@ where marker: std::marker::PhantomData, hashfunc: self.hashfunc, pre_lens: Vec::new(), + worker: worker.index(), }; (Exchange::new(senders, distributor), LogPuller::new(receiver, worker.index(), identifier, logging.clone())) } diff --git a/differential-dataflow/tests/columnar.rs b/differential-dataflow/tests/columnar.rs new file mode 100644 index 000000000..84288ad56 --- /dev/null +++ b/differential-dataflow/tests/columnar.rs @@ -0,0 +1,80 @@ +use std::time::{Duration, Instant}; + +use timely::dataflow::operators::{Input, Probe}; +use timely::dataflow::{InputHandle, ProbeHandle}; +use timely::Config; + +use differential_dataflow::columnar::collection; +use differential_dataflow::columnar::trace::{Batcher, Builder, Chunker, Spine}; +use differential_dataflow::operators::arrange::arrangement::arrange_core; + +type Upd = (u64, (), u64, i64); + +/// Feed the dataflow with a single (key, val) at the provided `diffs` the +/// advance the frontier to 1. +/// +/// Returns false if the dataflow hangs for longer than 5s. +fn arrange_reaches_one(config: Config, diffs: &'static [i64]) -> bool { + let guards = timely::execute(config, move |worker| { + let index = worker.index(); + let mut probe = ProbeHandle::new(); + let mut input = >>::new_with_builder(); + + worker.dataflow::(|scope| { + let stream = scope.input_from(&mut input); + let pact = collection::Pact { + hashfunc: |k: columnar::Ref<'_, u64>| *k, + }; + + arrange_core::< + _, + _, + Chunker, + Batcher, + Builder, + Spine, + >(stream, pact, "Arrange") + .stream + .probe_with(&mut probe); + }); + + for &diff in diffs { + input.send((index as u64, (), 0, diff)); + } + input.advance_to(1); + input.flush(); + + let start = Instant::now(); + while probe.less_than(&1) { + worker.step(); + if start.elapsed() > Duration::from_secs(5) { + for id in worker.installed_dataflows() { + worker.drop_dataflow(id); + } + return false; + } + } + true + }) + .expect("timely execute"); + + guards.join().into_iter().all(|r| r.unwrap_or(false)) +} + +#[test] +fn columnar_exchange_net_empty_container() { + // [+1, -1] diffs should consolidate to nothing, but the exchange must + // still deliver a message with `records = 2`. + // + // Previously the exchange did not emit a message and the dataflow froze. + assert!( + arrange_reaches_one(Config::process(2), &[1, -1]), + "frontier stalled: record count lost for an all-cancelled container" + ); +} + +#[test] +fn columnar_exchange_consolidated_container() { + assert!(arrange_reaches_one(Config::process(2), &[1, 1, 1])); + assert!(arrange_reaches_one(Config::process(3), &[2, -1, 1])); +}