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