Skip to main content

mz_txn_wal/
operator.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Timely operators for the crate
11
12use std::any::Any;
13use std::fmt::Debug;
14use std::future::Future;
15use std::rc::Rc;
16use std::sync::mpsc::TryRecvError;
17use std::sync::{Arc, mpsc};
18use std::time::Duration;
19
20use differential_dataflow::Hashable;
21use differential_dataflow::difference::Monoid;
22use differential_dataflow::lattice::Lattice;
23use mz_dyncfg::{Config, ConfigSet, ParameterScope};
24use mz_ore::cast::CastFrom;
25use mz_persist_client::cfg::RetryParameters;
26use mz_persist_client::operators::shard_source::{
27    ErrorHandler, FilterResult, SnapshotMode, shard_source,
28};
29use mz_persist_client::{Diagnostics, PersistClient, ShardId};
30use mz_persist_types::codec_impls::{StringSchema, UnitSchema};
31use mz_persist_types::txn::TxnsCodec;
32use mz_persist_types::{Codec, Codec64, StepForward};
33use mz_timely_util::activator::ArcActivator;
34use mz_timely_util::builder_async::{PressOnDropButton, button};
35use timely::dataflow::channels::pact::Pipeline;
36#[cfg(test)]
37use timely::dataflow::operators::Input;
38use timely::dataflow::operators::capture::Event;
39use timely::dataflow::operators::generic::OutputBuilder;
40use timely::dataflow::operators::generic::builder_rc::OperatorBuilder as OperatorBuilderRc;
41use timely::dataflow::operators::vec::{Broadcast, Map};
42use timely::dataflow::operators::{Capture, Leave, Probe};
43use timely::dataflow::{ProbeHandle, Scope, StreamVec};
44use timely::order::TotalOrder;
45use timely::progress::{Antichain, Timestamp};
46use timely::worker::Worker;
47use timely::{PartialOrder, WorkerConfig};
48use tracing::debug;
49
50use crate::TxnsCodecDefault;
51use crate::txn_cache::TxnsCache;
52use crate::txn_read::{DataRemapEntry, TxnsRead};
53
54/// An operator for translating physical data shard frontiers into logical ones.
55///
56/// A data shard in the txns set logically advances its upper each time a txn is
57/// committed, but the upper is not physically advanced unless that data shard
58/// was involved in the txn. This means that a shard_source (or any read)
59/// pointed at a data shard would appear to stall at the time of the most recent
60/// write. We fix this for shard_source by flowing its output through a new
61/// `txns_progress` dataflow operator, which ensures that the
62/// frontier/capability is advanced as the txns shard progresses, as long as the
63/// shard_source is up to date with the latest committed write to that data
64/// shard.
65///
66/// Example:
67///
68/// - A data shard has most recently been written to at 3.
69/// - The txns shard's upper is at 6.
70/// - We render a dataflow containing a shard_source with an as_of of 5.
71/// - A txn NOT involving the data shard is committed at 7.
72/// - A txn involving the data shard is committed at 9.
73///
74/// How it works:
75///
76/// - The shard_source operator is rendered. Its single output is hooked up as a
77///   _disconnected_ input to txns_progress. The txns_progress single output is
78///   a stream of the same type, which is used by downstream operators. This
79///   txns_progress operator is targeted at one data_shard; rendering a
80///   shard_source for a second data shard requires a second txns_progress
81///   operator.
82/// - The shard_source operator emits data through 3 and advances the frontier.
83/// - The txns_progress operator passes through these writes and frontier
84///   advancements unchanged. (Recall that it's always correct to read a data
85///   shard "normally", it just might stall.) Because the txns_progress operator
86///   knows there are no writes in `[3,5]`, it then downgrades its own
87///   capability past 5 (to 6). Because the input is disconnected, this means
88///   the overall frontier of the output is downgraded to 6.
89/// - The txns_progress operator learns about the write at 7 (the upper is now
90///   8). Because it knows that the data shard was not involved in this, it's
91///   free to downgrade its capability to 8.
92/// - The txns_progress operator learns about the write at 9 (the upper is now
93///   10). It knows that the data shard _WAS_ involved in this, so it forwards
94///   on data from its input until the input has progressed to 10, at which
95///   point it can itself downgrade to 10.
96pub fn txns_progress<'scope, K, V, T, D, P, C, F>(
97    passthrough: StreamVec<'scope, T, P>,
98    name: &str,
99    ctx: &TxnsContext,
100    client_fn: impl Fn() -> F,
101    txns_id: ShardId,
102    data_id: ShardId,
103    as_of: T,
104    until: Antichain<T>,
105    data_key_schema: Arc<K::Schema>,
106    data_val_schema: Arc<V::Schema>,
107) -> (StreamVec<'scope, T, P>, Vec<PressOnDropButton>)
108where
109    K: Debug + Codec + Send + Sync,
110    V: Debug + Codec + Send + Sync,
111    T: Timestamp + Lattice + TotalOrder + StepForward + Codec64 + Sync,
112    D: Debug + Clone + 'static + Monoid + Ord + Codec64 + Send + Sync,
113    P: Debug + Clone + 'static,
114    C: TxnsCodec + 'static,
115    F: Future<Output = PersistClient> + Send + 'static,
116{
117    let unique_id = (name, passthrough.scope().addr()).hashed();
118    let (remap, source_button) = txns_progress_source_global::<K, V, T, D, P, C>(
119        passthrough.scope(),
120        name,
121        ctx.clone(),
122        client_fn(),
123        txns_id,
124        data_id,
125        as_of,
126        data_key_schema,
127        data_val_schema,
128        unique_id,
129    );
130    // Each of the `txns_frontiers` workers wants the full copy of the remap
131    // information.
132    let remap = remap.broadcast();
133    let (passthrough, frontiers_button) = txns_progress_frontiers::<K, V, T, D, P, C>(
134        remap,
135        passthrough,
136        name,
137        data_id,
138        until,
139        unique_id,
140    );
141    (passthrough, vec![source_button, frontiers_button])
142}
143
144/// Event sent from the subscribe Tokio task to the sync `txns_progress_source`
145/// operator. The task owns the persist resources and the `data_subscribe`
146/// receiver. The operator owns the output capability and drives the frontier.
147enum SourceEvent<T> {
148    /// A `DataRemapEntry` read from the data shard subscription.
149    Remap(DataRemapEntry<T>),
150    /// The subscription closed cleanly. The operator drops its capability and
151    /// treats a later channel disconnect as expected rather than a task panic.
152    Finished,
153}
154
155/// TODO: I'd much prefer the communication protocol between the two operators
156/// to be exactly remap as defined in the [reclocking design doc]. However, we
157/// can't quite recover exactly the information necessary to construct that at
158/// the moment. Seems worth doing, but in the meantime, intentionally make this
159/// look fairly different (`Stream` of `DataRemapEntry` instead of
160/// `Collection<FromTime>`) to hopefully minimize confusion. As a performance
161/// optimization, we only re-emit this when the _physical_ upper has changed,
162/// which means that the frontier of the `Stream<DataRemapEntry<T>>` indicates
163/// updates to the logical_upper of the most recent `DataRemapEntry` (i.e. the
164/// one with the largest physical_upper).
165///
166/// [reclocking design doc]:
167///     https://github.com/MaterializeInc/materialize/blob/main/doc/developer/design/20210714_reclocking.md
168fn txns_progress_source_global<'scope, K, V, T, D, P, C>(
169    scope: Scope<'scope, T>,
170    name: &str,
171    ctx: TxnsContext,
172    client: impl Future<Output = PersistClient> + Send + 'static,
173    txns_id: ShardId,
174    data_id: ShardId,
175    as_of: T,
176    data_key_schema: Arc<K::Schema>,
177    data_val_schema: Arc<V::Schema>,
178    unique_id: u64,
179) -> (StreamVec<'scope, T, DataRemapEntry<T>>, PressOnDropButton)
180where
181    K: Debug + Codec + Send + Sync,
182    V: Debug + Codec + Send + Sync,
183    T: Timestamp + Lattice + TotalOrder + StepForward + Codec64 + Sync,
184    D: Debug + Clone + 'static + Monoid + Ord + Codec64 + Send + Sync,
185    P: Debug + Clone + 'static,
186    C: TxnsCodec + 'static,
187{
188    let worker_idx = scope.index();
189    let chosen_worker = usize::cast_from(name.hashed()) % scope.peers();
190    let name = format!("txns_progress_source({})", name);
191    let mut builder = OperatorBuilderRc::new(name.clone(), scope.clone());
192    let info = builder.operator_info();
193    let name = format!("{} [{}] {:.9}", name, unique_id, data_id.to_string());
194    let (remap_output, remap_stream) = builder.new_output::<Vec<DataRemapEntry<T>>>();
195    let mut remap_output = OutputBuilder::from(remap_output);
196
197    let (mut shutdown_handle, shutdown_button) = button(scope.clone(), Rc::clone(&info.address));
198
199    builder.build_reschedule(move |capabilities| {
200        // The output capability's time tracks the `logical_upper` we've advanced
201        // to. `None` indicates that we've dropped the capability to shut down.
202        let [cap]: [_; 1] = capabilities.try_into().expect("one capability per output");
203        let mut capability = Some(cap);
204
205        // The most recently observed physical upper. We emit a `DataRemapEntry`
206        // only when the physical upper changes.
207        let mut physical_upper = T::minimum();
208
209        // Per-worker state. Only the chosen worker subscribes to the data shard
210        // (via a Tokio task that owns the blocking persist I/O) and produces
211        // output. Non-chosen workers drop their capability immediately and only
212        // participate in the shutdown handshake below. `Some` holds the receiver
213        // of `SourceEvent`s, the activation ack, and the task handle, kept alive
214        // so the task is aborted when the operator is dropped.
215        let mut chosen_state = if worker_idx == chosen_worker {
216            let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel::<SourceEvent<T>>();
217            let (activator, activation_ack) = ArcActivator::new(scope, &info);
218
219            let task_name = name.clone();
220            let task = mz_ore::task::spawn(|| name.clone(), async move {
221                let client = client.await;
222                let txns_read = ctx.get_or_init::<T, C>(&client, txns_id).await;
223
224                let _ = txns_read.update_gt(as_of.clone()).await;
225                let data_write = client
226                    .open_writer::<K, V, T, D>(
227                        data_id,
228                        Arc::clone(&data_key_schema),
229                        Arc::clone(&data_val_schema),
230                        Diagnostics::from_purpose("data read physical upper"),
231                    )
232                    .await
233                    .expect("schema shouldn't change");
234                let mut rx = txns_read
235                    .data_subscribe(data_id, as_of.clone(), data_write)
236                    .await;
237                debug!("{} starting as_of={:?}", task_name, as_of);
238
239                while let Some(remap) = rx.recv().await {
240                    if event_tx.send(SourceEvent::Remap(remap)).is_err() {
241                        // The operator is gone. Stop.
242                        return;
243                    }
244                    activator.activate();
245                }
246                // The subscription closed. Signal the operator so it drops its
247                // output capability.
248                let _ = event_tx.send(SourceEvent::Finished);
249                activator.activate();
250            })
251            .abort_on_drop();
252
253            Some((event_rx, activation_ack, task))
254        } else {
255            // Non-chosen workers contribute nothing to the output frontier.
256            capability = None;
257            None
258        };
259
260        // Whether we've observed `SourceEvent::Finished`, so a subsequent
261        // channel disconnect is expected rather than a task panic.
262        let mut finished = false;
263
264        move |_frontiers| {
265            // On a local shutdown press, hold the capability and stay scheduled
266            // until all workers have pressed, then release. Dropping the
267            // capability on the local press alone would let the downstream
268            // frontier advance during cross-worker teardown skew, past times
269            // whose input this worker has already discarded.
270            if shutdown_handle.local_pressed() {
271                return if shutdown_handle.all_pressed() {
272                    capability = None;
273                    // Drop the receiver, ack, and task handle, aborting the task.
274                    chosen_state = None;
275                    false
276                } else {
277                    true
278                };
279            }
280
281            let Some((event_rx, activation_ack, _task)) = chosen_state.as_mut() else {
282                // Non-chosen worker: nothing to do. Stay alive (the button
283                // channel reschedules us) for the shutdown handshake above.
284                return false;
285            };
286            // Acknowledge the activation so the Tokio task can activate us again.
287            activation_ack.ack();
288
289            let mut output = remap_output.activate();
290            loop {
291                match event_rx.try_recv() {
292                    Ok(SourceEvent::Remap(remap)) => {
293                        let Some(cap) = capability.as_mut() else {
294                            // Already shut down, so drop any straggling events.
295                            continue;
296                        };
297                        assert!(physical_upper <= remap.physical_upper);
298                        assert!(physical_upper < remap.logical_upper);
299
300                        let logical_upper = remap.logical_upper.clone();
301                        // Emit at the pre-downgrade capability, then downgrade.
302                        if remap.physical_upper != physical_upper {
303                            physical_upper = remap.physical_upper.clone();
304                            debug!("{} emitting {:?}", name, remap);
305                            output.session(&*cap).give(remap);
306                        } else {
307                            debug!("{} not emitting {:?}", name, remap);
308                        }
309                        cap.downgrade(&logical_upper);
310                    }
311                    Ok(SourceEvent::Finished) => {
312                        // Subscription closed cleanly. Drop the capability.
313                        finished = true;
314                        capability = None;
315                    }
316                    Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
317                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
318                        // A task panic aborts the process via the enhanced panic
319                        // handler, so this assert is only a safety net for
320                        // environments that do not abort. On the panic path the
321                        // task never calls `activate()`, so it fires only if the
322                        // operator is rescheduled for another reason.
323                        assert!(finished, "txns_progress_source task unexpectedly gone");
324                        break;
325                    }
326                }
327            }
328
329            false
330        }
331    });
332
333    (remap_stream, shutdown_button.press_on_drop())
334}
335
336/// The block ordering inside the schedule closure is load-bearing: pending
337/// passthrough input is emitted at the pre-activation capability BEFORE any
338/// capability downgrade, which keeps the differential invariant `send_time <=
339/// record_time` and avoids dropping in-flight rows when the passthrough
340/// frontier crosses `until` in the same activation (SQL-299). Do not reorder.
341fn txns_progress_frontiers<'scope, K, V, T, D, P, C>(
342    remap: StreamVec<'scope, T, DataRemapEntry<T>>,
343    passthrough: StreamVec<'scope, T, P>,
344    name: &str,
345    data_id: ShardId,
346    until: Antichain<T>,
347    unique_id: u64,
348) -> (StreamVec<'scope, T, P>, PressOnDropButton)
349where
350    K: Debug + Codec,
351    V: Debug + Codec,
352    T: Timestamp + Lattice + TotalOrder + StepForward + Codec64,
353    D: Clone + 'static + Monoid + Codec64 + Send + Sync,
354    P: Debug + Clone + 'static,
355    C: TxnsCodec,
356{
357    let scope = passthrough.scope();
358    let name = format!("txns_progress_frontiers({})", name);
359    let mut builder = OperatorBuilderRc::new(name.clone(), scope.clone());
360    let info = builder.operator_info();
361    let name = format!(
362        "{} [{}] {}/{} {:.9}",
363        name,
364        unique_id,
365        scope.index(),
366        scope.peers(),
367        data_id.to_string(),
368    );
369    let (passthrough_output, passthrough_stream) = builder.new_output::<Vec<P>>();
370    let mut passthrough_output = OutputBuilder::from(passthrough_output);
371    // Both inputs are disconnected from the output: capability advancement is
372    // driven manually based on the remap stream and the passthrough frontier.
373    // NB: the output is created BEFORE the inputs on purpose. `new_output`
374    // connects to whatever inputs already exist (here, none); the `[]`
375    // connection arg below records the input-to-output summary but does not by
376    // itself disconnect the output. Creating an input before the output would
377    // silently connect them and break the manual capability management.
378    let mut remap_input = builder.new_input_connection(remap, Pipeline, []);
379    let mut passthrough_input = builder.new_input_connection(passthrough, Pipeline, []);
380
381    let (mut shutdown_handle, shutdown_button) = button(scope, info.address);
382
383    builder.build_reschedule(move |capabilities| {
384        // The output capability's time tracks how far we've progressed in
385        // copying along the passthrough input. `None` indicates that we've
386        // dropped the capability to shut down.
387        let [cap]: [_; 1] = capabilities.try_into().expect("one capability per output");
388        let mut capability = Some(cap);
389        // The most recently observed remap state. Retained even after the remap
390        // input closes so we can still advance the output capability to the
391        // last known `logical_upper` while the passthrough input is draining.
392        // This deliberately diverges from the async impl, which dropped the
393        // entry on close and stalled (PER-4).
394        let mut remap = DataRemapEntry {
395            physical_upper: T::minimum(),
396            logical_upper: T::minimum(),
397        };
398        // Whether the remap input has reached the empty antichain.
399        let mut remap_closed = false;
400
401        move |frontiers| {
402            // If our worker pressed the button we stop producing data and
403            // frontier updates downstream, but mirror `builder_async`: hold the
404            // capability and stop draining the inputs until ALL workers have
405            // pressed. Dropping the capability on the local press alone would
406            // let the downstream frontier advance during cross-worker teardown
407            // skew, past times whose data this worker has discarded, while
408            // other workers' operator instances still feed downstream.
409            if shutdown_handle.local_pressed() {
410                return if shutdown_handle.all_pressed() {
411                    // All workers pressed: drop the capability and drain the
412                    // inputs so teardown does not stall the dataflow.
413                    capability = None;
414                    remap_input.for_each(|_input_cap, _data| {});
415                    passthrough_input.for_each(|_input_cap, _data| {});
416                    false
417                } else {
418                    // Wedge: keep the capability, leave the inputs undrained
419                    // (their pending messages hold the frontier), and ask to be
420                    // rescheduled until the remaining workers press.
421                    true
422                };
423            }
424
425            // Fold new DataRemapEntries, keeping the one with the largest
426            // logical_upper. The ordering of incoming entries is not assumed.
427            remap_input.for_each(|_input_cap, data| {
428                for x in data.drain(..) {
429                    debug!("{} got remap {:?}", name, x);
430                    if remap.logical_upper < x.logical_upper {
431                        assert!(
432                            remap.physical_upper <= x.physical_upper,
433                            "previous remap physical upper {:?} is ahead of new remap physical upper {:?}",
434                            remap.physical_upper,
435                            x.physical_upper,
436                        );
437                        // TODO: If the physical upper has advanced, that's a very
438                        // strong hint that the data shard is about to be written to.
439                        // Because the data shard's upper advances sparsely (on write,
440                        // but not on passage of time) which invalidates the "every 1s"
441                        // assumption of the default tuning, we've had to de-tune the
442                        // listen sleeps on the paired persist_source. Maybe we use "one
443                        // state" to wake it up in case pubsub doesn't and remove the
444                        // listen polling entirely? (NB: This would have to happen in
445                        // each worker so that it's guaranteed to happen in each
446                        // process.)
447                        remap = x;
448                    }
449                }
450            });
451
452            // Apply the remap input's frontier as a `logical_upper` bump. We do
453            // not discard `remap` on the empty antichain: the last observed
454            // entry remains valid and lets the capability still advance past
455            // `physical_upper` while the passthrough input drains.
456            if let Some(logical_upper) = frontiers[0].frontier().as_option() {
457                if remap.logical_upper < *logical_upper {
458                    remap.logical_upper = logical_upper.clone();
459                }
460            } else {
461                remap_closed = true;
462            }
463
464            debug!("{} remap {:?} remap_closed={}", name, remap, remap_closed);
465
466            // Pass through any data the passthrough input has pending, at the
467            // current (pre-downgrade) capability, BEFORE any downgrade below.
468            // `cap.time()` here equals the pre-activation frontier, which is
469            // `<=` every pending record's time, so the differential invariant
470            // `send_time <= record_time` holds. Doing this before the
471            // `until`-driven drop is the SQL-299 fix. NB: nothing to do for
472            // `until` because the shard_source (before) and mfp_and_decode
473            // (after) filter.
474            if let Some(cap) = capability.as_ref() {
475                let mut output = passthrough_output.activate();
476                passthrough_input.for_each(|_input_cap, data| {
477                    debug!("{} emitting data {:?}", name, data);
478                    output.session(cap).give_container(data);
479                });
480            } else {
481                // Still drain to avoid stalling the dataflow.
482                passthrough_input.for_each(|_input_cap, _data| {});
483            }
484
485            // Only consult the passthrough frontier when not waiting on remap to
486            // push `physical_upper` past the capability. While `physical_upper
487            // <= cap.time()` and the remap input is open, the next expected
488            // event is a remap update that jumps `cap` to `logical_upper`, not a
489            // passthrough advance. Consulting the passthrough frontier then can
490            // drop the capability prematurely (e.g. `SELECT AS OF MAX`, where no
491            // remap update ever arrives and the passthrough side reports the
492            // empty antichain). Once remap is closed, the passthrough frontier
493            // is the only remaining driver.
494            let waiting_for_remap = match capability.as_ref() {
495                Some(cap) => !remap_closed && remap.physical_upper.less_equal(cap.time()),
496                None => false,
497            };
498            if !waiting_for_remap {
499                // Apply the passthrough input's frontier.
500                //
501                // If `until.less_equal(pass_frontier)`, it means that all
502                // subsequent batches will contain only times greater or equal
503                // to `until`, which means they can be dropped in their entirety.
504                //
505                // Ideally this check would live in `txns_progress_source`, but
506                // that turns out to be much more invasive (requires replacing
507                // lots of `T`s with `Antichain<T>`s). Given that we've been
508                // thinking about reworking the operators, do the easy but more
509                // wasteful thing for now.
510                let pass_frontier = frontiers[1].frontier();
511                if PartialOrder::less_equal(&until.borrow(), &pass_frontier) {
512                    debug!(
513                        "{} progress {:?} has passed until {:?}",
514                        name,
515                        pass_frontier,
516                        until.elements(),
517                    );
518                    capability = None;
519                } else if let Some(new_progress) = pass_frontier.as_option() {
520                    // Recall that any reads of the data shard are always
521                    // correct, so given that we've passed through any data from
522                    // the input, that means we're free to pass through frontier
523                    // updates too.
524                    if let Some(cap) = capability.as_mut() {
525                        if cap.time() < new_progress {
526                            debug!("{} downgrading cap to {:?}", name, new_progress);
527                            cap.downgrade(new_progress);
528                        }
529                    }
530                } else {
531                    // Reached the empty frontier; shut down.
532                    capability = None;
533                }
534            }
535
536            // If we've copied passthrough data to at least `physical_upper`, we
537            // can artificially advance the output to `logical_upper`. By the
538            // emptiness of `[physical_upper, logical_upper)`, no record still in
539            // flight lies below `logical_upper`, so this never strands data.
540            if let Some(cap) = capability.as_mut() {
541                assert!(remap.physical_upper <= remap.logical_upper);
542                let phys_reached = remap.physical_upper.less_equal(cap.time());
543                let logical_ahead = cap.time() < &remap.logical_upper;
544                if phys_reached && logical_ahead {
545                    cap.downgrade(&remap.logical_upper);
546                }
547            }
548
549            false
550        }
551    });
552
553    (passthrough_stream, shutdown_button.press_on_drop())
554}
555
556/// The process global [`TxnsRead`] that any operator can communicate with.
557#[derive(Default, Debug, Clone)]
558pub struct TxnsContext {
559    read: Arc<tokio::sync::OnceCell<Box<dyn Any + Send + Sync>>>,
560}
561
562impl TxnsContext {
563    async fn get_or_init<T, C>(&self, client: &PersistClient, txns_id: ShardId) -> TxnsRead<T>
564    where
565        T: Timestamp + Lattice + Codec64 + TotalOrder + StepForward + Sync,
566        C: TxnsCodec + 'static,
567    {
568        let read = self
569            .read
570            .get_or_init(|| {
571                let client = client.clone();
572                async move {
573                    let read: Box<dyn Any + Send + Sync> =
574                        Box::new(TxnsRead::<T>::start::<C>(client, txns_id).await);
575                    read
576                }
577            })
578            .await
579            .downcast_ref::<TxnsRead<T>>()
580            .expect("timestamp types should match");
581        // We initially only have one txns shard in the system.
582        assert_eq!(&txns_id, read.txns_id());
583        read.clone()
584    }
585}
586
587// Existing configs use the prefix "persist_txns_" for historical reasons. New
588// configs should use the prefix "txn_wal_".
589
590pub(crate) const DATA_SHARD_RETRYER_INITIAL_BACKOFF: Config<Duration> = Config::new(
591    "persist_txns_data_shard_retryer_initial_backoff",
592    Duration::from_millis(1024),
593    "The initial backoff when polling for new batches from a txns data shard persist_source.",
594    ParameterScope::Environment,
595);
596
597pub(crate) const DATA_SHARD_RETRYER_MULTIPLIER: Config<u32> = Config::new(
598    "persist_txns_data_shard_retryer_multiplier",
599    2,
600    "The backoff multiplier when polling for new batches from a txns data shard persist_source.",
601    ParameterScope::Environment,
602);
603
604pub(crate) const DATA_SHARD_RETRYER_CLAMP: Config<Duration> = Config::new(
605    "persist_txns_data_shard_retryer_clamp",
606    Duration::from_secs(16),
607    "The backoff clamp duration when polling for new batches from a txns data shard persist_source.",
608    ParameterScope::Environment,
609);
610
611/// Retry configuration for txn-wal data shard override of
612/// `next_listen_batch`.
613pub fn txns_data_shard_retry_params(cfg: &ConfigSet) -> RetryParameters {
614    RetryParameters {
615        fixed_sleep: Duration::ZERO,
616        initial_backoff: DATA_SHARD_RETRYER_INITIAL_BACKOFF.get(cfg),
617        multiplier: DATA_SHARD_RETRYER_MULTIPLIER.get(cfg),
618        clamp: DATA_SHARD_RETRYER_CLAMP.get(cfg),
619    }
620}
621
622/// A helper for subscribing to a data shard using the timely operators.
623///
624/// This could instead be a wrapper around a [Subscribe], but it's only used in
625/// tests and maelstrom, so do it by wrapping the timely operators to get
626/// additional coverage. For the same reason, hardcode the K, V, T, D types.
627///
628/// [Subscribe]: mz_persist_client::read::Subscribe
629pub struct DataSubscribe {
630    pub(crate) as_of: u64,
631    pub(crate) worker: Worker,
632    data: ProbeHandle<u64>,
633    txns: ProbeHandle<u64>,
634    capture: mpsc::Receiver<Event<u64, Vec<(String, u64, i64)>>>,
635    output: Vec<(String, u64, i64)>,
636
637    _tokens: Vec<PressOnDropButton>,
638}
639
640impl std::fmt::Debug for DataSubscribe {
641    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
642        let DataSubscribe {
643            as_of,
644            worker: _,
645            data,
646            txns,
647            capture: _,
648            output,
649            _tokens: _,
650        } = self;
651        f.debug_struct("DataSubscribe")
652            .field("as_of", as_of)
653            .field("data", data)
654            .field("txns", txns)
655            .field("output", output)
656            .finish_non_exhaustive()
657    }
658}
659
660impl DataSubscribe {
661    /// Creates a new [DataSubscribe].
662    pub fn new(
663        name: &str,
664        client: PersistClient,
665        txns_id: ShardId,
666        data_id: ShardId,
667        as_of: u64,
668        until: Antichain<u64>,
669    ) -> Self {
670        let mut worker = Worker::new(
671            WorkerConfig::default(),
672            timely::communication::Allocator::Thread(
673                timely::communication::allocator::Thread::default(),
674            ),
675            Some(std::time::Instant::now()),
676        );
677        let (data, txns, capture, tokens) = worker.dataflow::<u64, _, _>(|outer| {
678            let (data_stream, shard_source_token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
679                let client = client.clone();
680                let (data_stream, token) = shard_source::<String, (), u64, i64, _, _, _>(
681                    outer,
682                    scope,
683                    name,
684                    move || std::future::ready(client.clone()),
685                    data_id,
686                    Some(Antichain::from_elem(as_of)),
687                    SnapshotMode::Include,
688                    until.clone(),
689                    false.then_some(|_, _, _| unreachable!()),
690                    Arc::new(StringSchema),
691                    Arc::new(UnitSchema),
692                    FilterResult::keep_all,
693                    false.then_some(|| unreachable!()),
694                    async {},
695                    ErrorHandler::Halt("data_subscribe"),
696                );
697                (data_stream.leave(outer), token)
698            });
699            let (data, txns) = (ProbeHandle::new(), ProbeHandle::new());
700            let data_stream = data_stream.flat_map(|part| {
701                let part = part.parse();
702                part.part.map(|((k, ()), t, d)| (k, t, d))
703            });
704            let data_stream = data_stream.probe_with(&data);
705            let (data_stream, mut txns_progress_token) =
706                txns_progress::<String, (), u64, i64, _, TxnsCodecDefault, _>(
707                    data_stream,
708                    name,
709                    &TxnsContext::default(),
710                    || std::future::ready(client.clone()),
711                    txns_id,
712                    data_id,
713                    as_of,
714                    until,
715                    Arc::new(StringSchema),
716                    Arc::new(UnitSchema),
717                );
718            let data_stream = data_stream.probe_with(&txns);
719            let mut tokens = shard_source_token;
720            tokens.append(&mut txns_progress_token);
721            (data, txns, data_stream.capture(), tokens)
722        });
723        Self {
724            as_of,
725            worker,
726            data,
727            txns,
728            capture,
729            output: Vec::new(),
730            _tokens: tokens,
731        }
732    }
733
734    /// Returns the exclusive progress of the dataflow.
735    pub fn progress(&self) -> u64 {
736        self.txns
737            .with_frontier(|f| *f.as_option().unwrap_or(&u64::MAX))
738    }
739
740    /// Steps the dataflow, capturing output.
741    pub fn step(&mut self) {
742        self.worker.step();
743        self.capture_output()
744    }
745
746    pub(crate) fn capture_output(&mut self) {
747        loop {
748            let event = match self.capture.try_recv() {
749                Ok(x) => x,
750                Err(TryRecvError::Empty) | Err(TryRecvError::Disconnected) => break,
751            };
752            match event {
753                Event::Progress(_) => {}
754                Event::Messages(_, mut msgs) => self.output.append(&mut msgs),
755            }
756        }
757    }
758
759    /// Steps the dataflow past the given time, capturing output.
760    #[cfg(test)]
761    pub async fn step_past(&mut self, ts: u64) {
762        while self.txns.less_equal(&ts) {
763            tracing::trace!(
764                "progress at {:?}",
765                self.txns.with_frontier(|x| x.to_owned()).elements()
766            );
767            self.step();
768            tokio::task::yield_now().await;
769        }
770    }
771
772    /// Returns captured output.
773    pub fn output(&self) -> &Vec<(String, u64, i64)> {
774        &self.output
775    }
776}
777
778/// A handle to a [DataSubscribe] running in a task.
779#[derive(Debug)]
780pub struct DataSubscribeTask {
781    /// Carries step requests. A `None` timestamp requests one step, a
782    /// `Some(ts)` requests stepping until we progress beyond `ts`.
783    tx: std::sync::mpsc::Sender<(
784        Option<u64>,
785        tokio::sync::oneshot::Sender<(Vec<(String, u64, i64)>, u64)>,
786    )>,
787    task: mz_ore::task::JoinHandle<Vec<(String, u64, i64)>>,
788    output: Vec<(String, u64, i64)>,
789    progress: u64,
790}
791
792impl DataSubscribeTask {
793    /// Creates a new [DataSubscribeTask].
794    pub async fn new(
795        client: PersistClient,
796        txns_id: ShardId,
797        data_id: ShardId,
798        as_of: u64,
799    ) -> Self {
800        let cache = TxnsCache::open(&client, txns_id, Some(data_id)).await;
801        let (tx, rx) = std::sync::mpsc::channel();
802        let task = mz_ore::task::spawn_blocking(
803            || "data_subscribe task",
804            move || Self::task(client, cache, data_id, as_of, rx),
805        );
806        DataSubscribeTask {
807            tx,
808            task,
809            output: Vec::new(),
810            progress: 0,
811        }
812    }
813
814    #[cfg(test)]
815    async fn step(&mut self) {
816        self.send(None).await;
817    }
818
819    /// Steps the dataflow past the given time, capturing output.
820    pub async fn step_past(&mut self, ts: u64) -> u64 {
821        self.send(Some(ts)).await;
822        self.progress
823    }
824
825    /// Returns captured output.
826    pub fn output(&self) -> &Vec<(String, u64, i64)> {
827        &self.output
828    }
829
830    async fn send(&mut self, ts: Option<u64>) {
831        let (tx, rx) = tokio::sync::oneshot::channel();
832        self.tx.send((ts, tx)).expect("task should be running");
833        let (mut new_output, new_progress) = rx.await.expect("task should be running");
834        self.output.append(&mut new_output);
835        assert!(self.progress <= new_progress);
836        self.progress = new_progress;
837    }
838
839    /// Signals for the task to exit, and then waits for this to happen.
840    ///
841    /// _All_ output from the lifetime of the task (not just what was previously
842    /// captured) is returned.
843    pub async fn finish(self) -> Vec<(String, u64, i64)> {
844        // Closing the channel signals the task to exit.
845        drop(self.tx);
846        self.task.await
847    }
848
849    fn task(
850        client: PersistClient,
851        cache: TxnsCache<u64>,
852        data_id: ShardId,
853        as_of: u64,
854        rx: std::sync::mpsc::Receiver<(
855            Option<u64>,
856            tokio::sync::oneshot::Sender<(Vec<(String, u64, i64)>, u64)>,
857        )>,
858    ) -> Vec<(String, u64, i64)> {
859        let mut subscribe = DataSubscribe::new(
860            "DataSubscribeTask",
861            client.clone(),
862            cache.txns_id(),
863            data_id,
864            as_of,
865            Antichain::new(),
866        );
867        let mut output = Vec::new();
868        loop {
869            let (ts, tx) = match rx.try_recv() {
870                Ok(x) => x,
871                Err(TryRecvError::Empty) => {
872                    // No requests, continue stepping so nothing deadlocks.
873                    subscribe.step();
874                    continue;
875                }
876                Err(TryRecvError::Disconnected) => {
877                    // All done! Return our output.
878                    return output;
879                }
880            };
881            // Always step at least once.
882            subscribe.step();
883            // If we got a ts, make sure to step past it.
884            if let Some(ts) = ts {
885                while subscribe.progress() <= ts {
886                    subscribe.step();
887                }
888            }
889            let new_output = std::mem::take(&mut subscribe.output);
890            output.extend(new_output.iter().cloned());
891            let _ = tx.send((new_output, subscribe.progress()));
892        }
893    }
894}
895
896#[cfg(test)]
897mod tests {
898    use itertools::{Either, Itertools};
899
900    use crate::tests::writer;
901    use crate::txns::TxnsHandle;
902
903    use super::*;
904
905    /// One scripted action applied to the operator's two inputs.
906    #[derive(Debug, Clone)]
907    enum Action {
908        /// Send a `DataRemapEntry` on the remap input.
909        Remap {
910            physical_upper: u64,
911            logical_upper: u64,
912        },
913        /// Advance the remap input frontier to `ts` (empty antichain if `None`).
914        RemapFrontier(Option<u64>),
915        /// Send passthrough data records (as `(payload, time)`), then leave them buffered.
916        Pass { records: Vec<(i64, u64)> },
917        /// Advance the passthrough input frontier to `ts` (empty antichain if `None`).
918        PassFrontier(Option<u64>),
919        /// Step the worker once.
920        Step,
921    }
922
923    /// Runs `schedule` against the operator built by `build`, returning the
924    /// captured output events and the final exclusive output frontier. Each
925    /// event is shaped as `(payload, time, count)`, where `count` is synthesized
926    /// as `1` so the output looks like a differential collection.
927    fn run_schedule(
928        build: impl for<'a> Fn(
929            StreamVec<'a, u64, DataRemapEntry<u64>>,
930            StreamVec<'a, u64, i64>,
931            Antichain<u64>,
932        ) -> (StreamVec<'a, u64, i64>, PressOnDropButton),
933        until: Antichain<u64>,
934        schedule: &[Action],
935    ) -> (Vec<(i64, u64, i64)>, u64) {
936        let mut worker = Worker::new(
937            WorkerConfig::default(),
938            timely::communication::Allocator::Thread(
939                timely::communication::allocator::Thread::default(),
940            ),
941            Some(std::time::Instant::now()),
942        );
943
944        // The button must outlive the run: dropping it presses the shutdown
945        // handle, which makes the operator drop its capability on the next
946        // activation. Hold it until after the drain loop completes.
947        let (remap_handle, pass_handle, probe, capture, _button) =
948            worker.dataflow::<u64, _, _>(|scope| {
949                let (remap_handle, remap_stream) = scope.new_input::<Vec<DataRemapEntry<u64>>>();
950                let (pass_handle, pass_stream) = scope.new_input::<Vec<i64>>();
951                let (out, button) = build(remap_stream, pass_stream, until.clone());
952                let probe = ProbeHandle::new();
953                let out = out.probe_with(&probe);
954                (remap_handle, pass_handle, probe, out.capture(), button)
955            });
956
957        // timely input handles can only `advance_to` forward in time. Track the
958        // last time used on each input so we can fail loudly with a useful
959        // message instead of panicking deep inside timely on a decreasing time.
960        let mut last_remap_ts = 0u64;
961        let mut last_pass_ts = 0u64;
962        // Held in `Option`s so a `*Frontier(None)` action can `take` and drop the
963        // handle, which closes the input to the empty antichain. Advancing to
964        // `u64::MAX` is NOT equivalent: it leaves the input's frontier at
965        // `Some(u64::MAX)`, which the operator (correctly) treats as a finite
966        // `logical_upper`/passthrough advance rather than a closed input.
967        let mut remap_handle = Some(remap_handle);
968        let mut pass_handle = Some(pass_handle);
969        for action in schedule {
970            match action.clone() {
971                // `Remap` is a `send` at the handle's current time, so it carries
972                // no explicit time and needs no monotonicity assert.
973                Action::Remap {
974                    physical_upper,
975                    logical_upper,
976                } => remap_handle
977                    .as_mut()
978                    .expect("remap input still open")
979                    .send(DataRemapEntry {
980                        physical_upper,
981                        logical_upper,
982                    }),
983                Action::RemapFrontier(Some(ts)) => {
984                    assert!(
985                        ts >= last_remap_ts,
986                        "Action::RemapFrontier time {ts} < previous remap time {last_remap_ts}; per-input times must be non-decreasing"
987                    );
988                    last_remap_ts = ts;
989                    remap_handle
990                        .as_mut()
991                        .expect("remap input still open")
992                        .advance_to(ts);
993                }
994                // Drop the handle to close the input to the empty antichain.
995                Action::RemapFrontier(None) => {
996                    last_remap_ts = u64::MAX;
997                    drop(remap_handle.take());
998                }
999                Action::Pass { records } => {
1000                    let handle = pass_handle.as_mut().expect("passthrough input still open");
1001                    for (payload, time) in records {
1002                        assert!(
1003                            time >= last_pass_ts,
1004                            "Action::Pass time {time} < previous passthrough time {last_pass_ts}; per-input times must be non-decreasing"
1005                        );
1006                        last_pass_ts = time;
1007                        // `advance_to` is what makes each record's time visible to
1008                        // the operator; the subsequent `send` emits the payload at
1009                        // that time. Both impls consume the identical schedule, so
1010                        // the exact send mechanics need only be self-consistent.
1011                        handle.advance_to(time);
1012                        handle.send(payload);
1013                    }
1014                }
1015                Action::PassFrontier(Some(ts)) => {
1016                    assert!(
1017                        ts >= last_pass_ts,
1018                        "Action::PassFrontier time {ts} < previous passthrough time {last_pass_ts}; per-input times must be non-decreasing"
1019                    );
1020                    last_pass_ts = ts;
1021                    pass_handle
1022                        .as_mut()
1023                        .expect("passthrough input still open")
1024                        .advance_to(ts);
1025                }
1026                // Drop the handle to close the input to the empty antichain.
1027                Action::PassFrontier(None) => {
1028                    last_pass_ts = u64::MAX;
1029                    drop(pass_handle.take());
1030                }
1031                Action::Step => {
1032                    worker.step();
1033                }
1034            }
1035        }
1036        // Drain: flush inputs and step until the output probe frontier stops
1037        // advancing. A hard cap PANICS so a buggy operator that never settles
1038        // fails loudly instead of silently returning partial results.
1039        if let Some(handle) = remap_handle.as_mut() {
1040            handle.flush();
1041        }
1042        if let Some(handle) = pass_handle.as_mut() {
1043            handle.flush();
1044        }
1045        let mut last = probe.with_frontier(|f| f.to_owned());
1046        let mut stable = 0;
1047        for step in 0.. {
1048            assert!(
1049                step < 4096,
1050                "run_schedule did not quiesce within 4096 steps"
1051            );
1052            worker.step();
1053            let now = probe.with_frontier(|f| f.to_owned());
1054            if now == last {
1055                stable += 1;
1056                // Require a few consecutive no-change steps so in-flight messages flush.
1057                if stable >= 8 {
1058                    break;
1059                }
1060            } else {
1061                stable = 0;
1062                last = now;
1063            }
1064        }
1065
1066        let frontier = probe.with_frontier(|f| *f.as_option().unwrap_or(&u64::MAX));
1067        let mut output = Vec::new();
1068        while let Ok(event) = capture.try_recv() {
1069            if let Event::Messages(time, msgs) = event {
1070                for payload in msgs {
1071                    output.push((payload, time, 1));
1072                }
1073            }
1074        }
1075        (output, frontier)
1076    }
1077
1078    impl<K, V, T, D, C> TxnsHandle<K, V, T, D, C>
1079    where
1080        K: Debug + Codec,
1081        V: Debug + Codec,
1082        T: Timestamp + Lattice + TotalOrder + StepForward + Codec64 + Sync,
1083        D: Debug + Monoid + Ord + Codec64 + Send + Sync,
1084        C: TxnsCodec,
1085    {
1086        async fn subscribe_task(
1087            &self,
1088            client: &PersistClient,
1089            data_id: ShardId,
1090            as_of: u64,
1091        ) -> DataSubscribeTask {
1092            DataSubscribeTask::new(client.clone(), self.txns_id(), data_id, as_of).await
1093        }
1094    }
1095
1096    #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1097    #[cfg_attr(miri, ignore)] // too slow
1098    async fn data_subscribe() {
1099        async fn step(subs: &mut Vec<DataSubscribeTask>) {
1100            for sub in subs.iter_mut() {
1101                sub.step().await;
1102            }
1103        }
1104
1105        let client = PersistClient::new_for_tests().await;
1106        let mut txns = TxnsHandle::expect_open(client.clone()).await;
1107        let log = txns.new_log();
1108        let d0 = ShardId::new();
1109
1110        // Start a subscription before the shard gets registered.
1111        let mut subs = Vec::new();
1112        subs.push(txns.subscribe_task(&client, d0, 5).await);
1113        step(&mut subs).await;
1114
1115        // Now register the shard. Also start a new subscription and step the
1116        // previous one (plus repeat this for every later step).
1117        txns.register(1, [writer(&client, d0).await]).await.unwrap();
1118        subs.push(txns.subscribe_task(&client, d0, 5).await);
1119        step(&mut subs).await;
1120
1121        // Now write something unrelated.
1122        let d1 = txns.expect_register(2).await;
1123        txns.expect_commit_at(3, d1, &["nope"], &log).await;
1124        subs.push(txns.subscribe_task(&client, d0, 5).await);
1125        step(&mut subs).await;
1126
1127        // Now write to our shard before.
1128        txns.expect_commit_at(4, d0, &["4"], &log).await;
1129        subs.push(txns.subscribe_task(&client, d0, 5).await);
1130        step(&mut subs).await;
1131
1132        // Now write to our shard at the as_of.
1133        txns.expect_commit_at(5, d0, &["5"], &log).await;
1134        subs.push(txns.subscribe_task(&client, d0, 5).await);
1135        step(&mut subs).await;
1136
1137        // Now write to our shard past the as_of.
1138        txns.expect_commit_at(6, d0, &["6"], &log).await;
1139        subs.push(txns.subscribe_task(&client, d0, 5).await);
1140        step(&mut subs).await;
1141
1142        // Now write something unrelated again.
1143        txns.expect_commit_at(7, d1, &["nope"], &log).await;
1144        subs.push(txns.subscribe_task(&client, d0, 5).await);
1145        step(&mut subs).await;
1146
1147        // Verify that the dataflows can progress to the expected point and that
1148        // we read the right thing no matter when the dataflow started.
1149        for mut sub in subs {
1150            let progress = sub.step_past(7).await;
1151            assert_eq!(progress, 8);
1152            log.assert_eq(d0, 5, 8, sub.finish().await);
1153        }
1154    }
1155
1156    #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1157    #[cfg_attr(miri, ignore)] // too slow
1158    async fn subscribe_shard_finalize() {
1159        let client = PersistClient::new_for_tests().await;
1160        let mut txns = TxnsHandle::expect_open(client.clone()).await;
1161        let log = txns.new_log();
1162        let d0 = txns.expect_register(1).await;
1163
1164        // Start the operator as_of the register ts.
1165        let mut sub = txns.read_cache().expect_subscribe(&client, d0, 1);
1166        sub.step_past(1).await;
1167
1168        // Write to it via txns.
1169        txns.expect_commit_at(2, d0, &["foo"], &log).await;
1170        sub.step_past(2).await;
1171
1172        // Unregister it.
1173        txns.forget(3, [d0]).await.unwrap();
1174        sub.step_past(3).await;
1175
1176        // TODO: Hard mode, see if we can get the rest of this test to work even
1177        // _without_ the txns shard advancing.
1178        txns.begin().commit_at(&mut txns, 7).await.unwrap();
1179
1180        // The operator should continue to emit data written directly even
1181        // though it's no longer in the txns set.
1182        let mut d0_write = writer(&client, d0).await;
1183        let key = "bar".to_owned();
1184        crate::small_caa(|| "test", &mut d0_write, &[((&key, &()), &5, 1)], 4, 6)
1185            .await
1186            .unwrap();
1187        log.record((d0, key, 5, 1));
1188        sub.step_past(4).await;
1189
1190        // Now finalize the shard to writes.
1191        let () = d0_write
1192            .compare_and_append_batch(&mut [], Antichain::from_elem(6), Antichain::new(), true)
1193            .await
1194            .unwrap()
1195            .unwrap();
1196        while sub.txns.less_than(&u64::MAX) {
1197            sub.step();
1198            tokio::task::yield_now().await;
1199        }
1200
1201        // Make sure we read the correct things.
1202        log.assert_eq(d0, 1, u64::MAX, sub.output().clone());
1203
1204        // Also make sure that we can read the right things if we start up after
1205        // the forget but before the direct write and ditto after the direct
1206        // write.
1207        log.assert_subscribe(d0, 4, u64::MAX).await;
1208        log.assert_subscribe(d0, 6, u64::MAX).await;
1209    }
1210
1211    #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1212    #[cfg_attr(miri, ignore)] // too slow
1213    async fn subscribe_shard_register_forget() {
1214        let client = PersistClient::new_for_tests().await;
1215        let mut txns = TxnsHandle::expect_open(client.clone()).await;
1216        let d0 = ShardId::new();
1217
1218        // Start a subscription on the data shard.
1219        let mut sub = txns.read_cache().expect_subscribe(&client, d0, 0);
1220        assert_eq!(sub.progress(), 0);
1221
1222        // Register the shard at 10.
1223        txns.register(10, [writer(&client, d0).await])
1224            .await
1225            .unwrap();
1226        sub.step_past(10).await;
1227        assert!(
1228            sub.progress() > 10,
1229            "operator should advance past 10 when shard is registered"
1230        );
1231
1232        // Forget the shard at 20.
1233        txns.forget(20, [d0]).await.unwrap();
1234        sub.step_past(20).await;
1235        assert!(
1236            sub.progress() > 20,
1237            "operator should advance past 20 when shard is forgotten"
1238        );
1239    }
1240
1241    #[mz_ore::test(tokio::test)]
1242    #[cfg_attr(miri, ignore)] // too slow
1243    async fn as_of_until() {
1244        let client = PersistClient::new_for_tests().await;
1245        let mut txns = TxnsHandle::expect_open(client.clone()).await;
1246        let log = txns.new_log();
1247
1248        let d0 = txns.expect_register(1).await;
1249        txns.expect_commit_at(2, d0, &["2"], &log).await;
1250        txns.expect_commit_at(3, d0, &["3"], &log).await;
1251        txns.expect_commit_at(4, d0, &["4"], &log).await;
1252        txns.expect_commit_at(5, d0, &["5"], &log).await;
1253        txns.expect_commit_at(6, d0, &["6"], &log).await;
1254        txns.expect_commit_at(7, d0, &["7"], &log).await;
1255
1256        let until = 5;
1257        let mut sub = DataSubscribe::new(
1258            "as_of_until",
1259            client,
1260            txns.txns_id(),
1261            d0,
1262            3,
1263            Antichain::from_elem(until),
1264        );
1265        // Manually step the dataflow, instead of going through the
1266        // `DataSubscribe` helper because we're interested in all captured
1267        // events.
1268        while sub.txns.less_equal(&5) {
1269            sub.worker.step();
1270            tokio::task::yield_now().await;
1271            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1272        }
1273        let (actual_progresses, actual_events): (Vec<_>, Vec<_>) =
1274            sub.capture.into_iter().partition_map(|event| match event {
1275                Event::Progress(progress) => Either::Left(progress),
1276                Event::Messages(ts, data) => Either::Right((ts, data)),
1277            });
1278        // Aggregate the captured records, ignoring the stream-level
1279        // timestamp on each batch. The operator emits each container at
1280        // whatever capability it currently holds (which is determined by
1281        // its scheduling cadence and the upstream frontiers it has
1282        // observed), so the per-batch `ts` is not deterministic and not
1283        // part of the operator's contract. Per-record `(key, time, diff)`
1284        // tuples are what callers see, and the differential invariant
1285        // (stream `ts <= record time`) is checked separately below.
1286        let mut actual_records: Vec<(String, u64, i64)> = actual_events
1287            .iter()
1288            .flat_map(|(_ts, data)| data.iter().cloned())
1289            .collect();
1290        actual_records.sort();
1291        let expected_records: Vec<(String, u64, i64)> = vec![
1292            ("2".to_owned(), 3, 1),
1293            ("3".to_owned(), 3, 1),
1294            ("4".to_owned(), 4, 1),
1295        ];
1296        assert_eq!(actual_records, expected_records);
1297
1298        // Verify the differential invariant: each batch's stream
1299        // timestamp `ts` must be `<= record_time` for every record it
1300        // carries. The operator's contract requires this so that
1301        // downstream differential operators can integrate the records
1302        // at their declared times.
1303        for (ts, data) in &actual_events {
1304            for (_key, record_ts, _diff) in data {
1305                assert!(
1306                    ts <= record_ts,
1307                    "differential invariant violated: stream ts {ts} > record time {record_ts}",
1308                );
1309            }
1310        }
1311
1312        // The number and contents of progress messages is not guaranteed and
1313        // depends on the downgrade behavior. The only thing we can assert is
1314        // the max progress timestamp, if there is one, is less than the until.
1315        if let Some(max_progress_ts) = actual_progresses
1316            .into_iter()
1317            .flatten()
1318            .map(|(ts, _diff)| ts)
1319            .max()
1320        {
1321            assert!(max_progress_ts < until, "{max_progress_ts} < {until}");
1322        }
1323    }
1324
1325    /// Builds the sync operator for the harness.
1326    fn build_sync<'a>(
1327        remap: StreamVec<'a, u64, DataRemapEntry<u64>>,
1328        pass: StreamVec<'a, u64, i64>,
1329        until: Antichain<u64>,
1330    ) -> (StreamVec<'a, u64, i64>, PressOnDropButton) {
1331        txns_progress_frontiers::<String, (), u64, i64, i64, TxnsCodecDefault>(
1332            remap,
1333            pass,
1334            "test",
1335            ShardId::new(),
1336            until,
1337            0,
1338        )
1339    }
1340
1341    /// Generates a random schedule for the no-data-loss fuzz test. Interleaves
1342    /// remap entries/frontiers with passthrough data/frontiers. Payloads are
1343    /// unique and increasing so a single dropped or duplicated record is
1344    /// detectable; per-input times are non-decreasing (the harness requires
1345    /// this). The schedule never closes the passthrough input, and the test
1346    /// uses `until = ∅`, so the operator never has a legitimate reason to shut
1347    /// down and must pass through every record it is given.
1348    ///
1349    /// Schedules are intentionally NOT constrained to respect the remap
1350    /// "[physical_upper, logical_upper) is empty" contract. The no-data-loss
1351    /// property must hold under arbitrary interleavings, so feeding
1352    /// contract-violating schedules only strengthens the test.
1353    fn gen_schedule(seed: u64) -> Vec<Action> {
1354        // Simple xorshift RNG for determinism without extra deps.
1355        let mut state = seed.wrapping_add(0x9E3779B97F4A7C15).max(1);
1356        let mut next = || {
1357            state ^= state << 13;
1358            state ^= state >> 7;
1359            state ^= state << 17;
1360            state
1361        };
1362
1363        let mut schedule = Vec::new();
1364        let mut physical = 0u64;
1365        let mut logical = 0u64;
1366        let mut pass_frontier = 0u64;
1367        let mut payload = 0i64;
1368        let mut remap_closed = false;
1369        let steps = 8 + (next() % 16);
1370        for _ in 0..steps {
1371            match next() % 5 {
1372                0 if !remap_closed => {
1373                    physical += next() % 3;
1374                    logical = logical.max(physical) + (next() % 4);
1375                    schedule.push(Action::Remap {
1376                        physical_upper: physical,
1377                        logical_upper: logical,
1378                    });
1379                }
1380                1 if !remap_closed => {
1381                    if next() % 8 == 0 {
1382                        remap_closed = true;
1383                        schedule.push(Action::RemapFrontier(None));
1384                    } else {
1385                        logical += next() % 3;
1386                        schedule.push(Action::RemapFrontier(Some(logical)));
1387                    }
1388                }
1389                2 => {
1390                    let t = pass_frontier + (next() % 3);
1391                    pass_frontier = t;
1392                    payload += 1;
1393                    schedule.push(Action::Pass {
1394                        records: vec![(payload, t)],
1395                    });
1396                }
1397                3 => {
1398                    pass_frontier += next() % 3;
1399                    schedule.push(Action::PassFrontier(Some(pass_frontier)));
1400                }
1401                _ => schedule.push(Action::Step),
1402            }
1403            schedule.push(Action::Step);
1404        }
1405        schedule
1406    }
1407
1408    /// Fuzz: under any random interleaving, the deasynced operator must emit
1409    /// every passthrough record it is given (no loss, no duplication) and must
1410    /// not prematurely shut down. With `until = ∅` and no passthrough close, the
1411    /// operator never legitimately drops its capability, so the output frontier
1412    /// must stay finite.
1413    #[mz_ore::test]
1414    #[cfg_attr(miri, ignore)] // too slow
1415    fn frontiers_fuzz_no_data_loss() {
1416        for seed in 0..500u64 {
1417            let schedule = gen_schedule(seed);
1418            let mut sent: Vec<i64> = schedule
1419                .iter()
1420                .flat_map(|a| match a {
1421                    Action::Pass { records } => records.iter().map(|(p, _)| *p).collect(),
1422                    _ => Vec::new(),
1423                })
1424                .collect();
1425            let (out, frontier) = run_schedule(build_sync, Antichain::new(), &schedule);
1426            let mut emitted: Vec<i64> = out.iter().map(|(p, _, _)| *p).collect();
1427            sent.sort();
1428            emitted.sort();
1429            assert_eq!(
1430                emitted, sent,
1431                "seed {seed}: operator lost or duplicated data\nschedule={schedule:?}\nout={out:?}"
1432            );
1433            assert_ne!(
1434                frontier,
1435                u64::MAX,
1436                "seed {seed}: operator prematurely shut down (empty output frontier)\nschedule={schedule:?}"
1437            );
1438        }
1439    }
1440
1441    #[mz_ore::test]
1442    #[cfg_attr(miri, ignore)] // too slow
1443    fn frontiers_sql_299_up_to_no_tail_loss() {
1444        // until = 0. A remap entry with physical_upper = 5 keeps the operator
1445        // out of the `waiting_for_remap` state (5 > cap.time() = 0), so the
1446        // until check actually fires. Buffer a record at time 0 (payload 4) and
1447        // leave it pending. In the single activation, the operator sees both the
1448        // buffered record and the passthrough frontier at 0, which already
1449        // satisfies `until <= pass_frontier` and drops the capability. The
1450        // record must be emitted before that drop, not discarded. Buffering at
1451        // time 0 (the cap's time) is what makes the record and the
1452        // until-crossing land in the same activation — with the ordered
1453        // `new_input` handle, advancing the passthrough frontier past the record
1454        // would deliver the record in an earlier activation and mask the bug.
1455        let schedule = vec![
1456            Action::Remap {
1457                physical_upper: 5,
1458                logical_upper: 5,
1459            },
1460            Action::RemapFrontier(Some(5)),
1461            Action::Pass {
1462                records: vec![(4, 0)],
1463            },
1464            Action::PassFrontier(None),
1465            Action::Step,
1466        ];
1467        let (output, _frontier) = run_schedule(build_sync, Antichain::from_elem(0), &schedule);
1468        let payloads: Vec<i64> = output.iter().map(|(p, _, _)| *p).collect();
1469        assert!(
1470            payloads.contains(&4),
1471            "buffered record at time 0 must be emitted before until-driven shutdown, got {output:?}"
1472        );
1473    }
1474
1475    #[mz_ore::test]
1476    #[cfg_attr(miri, ignore)] // too slow
1477    fn frontiers_per4_advance_after_remap_close() {
1478        // Emit a remap entry whose logical_upper (10) exceeds its physical_upper
1479        // (5). Close the remap input while the passthrough frontier is still
1480        // below physical_upper (so the capability has NOT yet advanced to
1481        // logical_upper), then advance the passthrough frontier up to
1482        // physical_upper (5). The capability must still advance to logical_upper
1483        // (10) using the remap entry retained across the close, not stall at the
1484        // passthrough frontier (5). The async impl dropped the entry on close and
1485        // stalled here (PER-4).
1486        let schedule = vec![
1487            Action::Remap {
1488                physical_upper: 5,
1489                logical_upper: 10,
1490            },
1491            Action::RemapFrontier(Some(10)),
1492            Action::Step,
1493            // Close remap before the passthrough frontier reaches physical_upper.
1494            Action::RemapFrontier(None),
1495            Action::Step,
1496            // Only now does the passthrough frontier reach physical_upper.
1497            Action::PassFrontier(Some(5)),
1498            Action::Step,
1499        ];
1500        let (_output, frontier) = run_schedule(build_sync, Antichain::new(), &schedule);
1501        assert_eq!(
1502            frontier, 10,
1503            "capability must advance to logical_upper after remap close, got {frontier}"
1504        );
1505    }
1506
1507    #[mz_ore::test]
1508    #[cfg_attr(miri, ignore)] // too slow
1509    fn frontiers_select_as_of_max_blocks() {
1510        // Mimic `SELECT AS OF MAX`: a remap entry exists with physical_upper == 0
1511        // (so physical_upper <= cap.time() and the operator waits for remap), no
1512        // further remap update arrives, and the passthrough frontier reaches the
1513        // empty antichain. The operator must NOT drop its capability (must keep
1514        // blocking), so the output frontier stays finite (0), not u64::MAX.
1515        let schedule = vec![
1516            Action::Remap {
1517                physical_upper: 0,
1518                logical_upper: 0,
1519            },
1520            Action::RemapFrontier(Some(0)),
1521            Action::PassFrontier(None),
1522            Action::Step,
1523        ];
1524        let (_output, frontier) = run_schedule(build_sync, Antichain::new(), &schedule);
1525        assert_eq!(
1526            frontier, 0,
1527            "operator must block (retain capability) while waiting for remap, got {frontier}"
1528        );
1529    }
1530}