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