Skip to main content

mz_compute/
compute_state.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//! Worker-local state for compute timely instances.
7
8use std::any::Any;
9use std::cell::RefCell;
10use std::cmp::Ordering;
11use std::collections::{BTreeMap, BTreeSet};
12use std::num::NonZeroUsize;
13use std::rc::Rc;
14use std::sync::Arc;
15use std::time::{Duration, Instant};
16
17use bytesize::ByteSize;
18use differential_dataflow::Hashable;
19use differential_dataflow::lattice::Lattice;
20use differential_dataflow::trace::implementations::BatchContainer;
21use differential_dataflow::trace::{Cursor, TraceReader};
22use mz_compute_client::logging::LoggingConfig;
23use mz_compute_client::protocol::command::{
24    ComputeCommand, ComputeParameters, InstanceConfig, Peek, PeekTarget,
25};
26use mz_compute_client::protocol::history::ComputeCommandHistory;
27use mz_compute_client::protocol::response::{
28    ComputeResponse, CopyToResponse, FrontiersResponse, PeekResponse, SubscribeResponse,
29};
30use mz_compute_types::dataflows::DataflowDescription;
31use mz_compute_types::dyncfgs::{
32    ENABLE_PEEK_RESPONSE_STASH, PEEK_RESPONSE_STASH_BATCH_MAX_RUNS,
33    PEEK_RESPONSE_STASH_THRESHOLD_BYTES, PEEK_STASH_BATCH_SIZE, PEEK_STASH_NUM_BATCHES,
34};
35use mz_compute_types::plan::render_plan::RenderPlan;
36use mz_dyncfg::ConfigSet;
37use mz_expr::row::RowCollection;
38use mz_expr::{RowComparator, SafeMfpPlan};
39use mz_ore::cast::{CastFrom, CastLossy};
40use mz_ore::collections::CollectionExt;
41use mz_ore::metrics::{MetricsRegistry, UIntGauge};
42use mz_ore::now::EpochMillis;
43use mz_ore::soft_panic_or_log;
44use mz_ore::task::AbortOnDropHandle;
45use mz_ore::tracing::{OpenTelemetryContext, TracingHandle};
46use mz_persist_client::Diagnostics;
47use mz_persist_client::cache::PersistClientCache;
48use mz_persist_client::cfg::USE_CRITICAL_SINCE_SNAPSHOT;
49use mz_persist_client::read::ReadHandle;
50use mz_persist_types::PersistLocation;
51use mz_persist_types::codec_impls::UnitSchema;
52use mz_repr::fixed_length::ExtendDatums;
53use mz_repr::{DatumVec, Diff, GlobalId, Row, RowArena, Timestamp};
54use mz_storage_operators::stats::StatsCursor;
55use mz_storage_types::StorageDiff;
56use mz_storage_types::controller::CollectionMetadata;
57use mz_storage_types::dyncfgs::ORE_OVERFLOWING_BEHAVIOR;
58use mz_storage_types::sources::SourceData;
59use mz_storage_types::time_dependence::TimeDependence;
60use mz_txn_wal::operator::TxnsContext;
61use mz_txn_wal::txn_cache::TxnsCache;
62use timely::dataflow::operators::probe;
63use timely::order::PartialOrder;
64use timely::progress::frontier::Antichain;
65use timely::worker::Worker as TimelyWorker;
66use tokio::sync::{oneshot, watch};
67use tracing::{Level, debug, error, info, span, trace, warn};
68use uuid::Uuid;
69
70use crate::arrangement::manager::{TraceBundle, TraceManager};
71use crate::logging;
72use crate::logging::compute::{CollectionLogging, ComputeEvent, PeekEvent};
73use crate::logging::initialize::LoggingTraces;
74use crate::metrics::{CollectionMetrics, WorkerMetrics};
75use crate::render::{LinearJoinSpec, StartSignal};
76use crate::server::{ComputeInstanceContext, ResponseSender};
77
78mod peek_result_iterator;
79mod peek_stash;
80
81/// Worker-local state that is maintained across dataflows.
82///
83/// This state is restricted to the COMPUTE state, the deterministic, idempotent work
84/// done between data ingress and egress.
85pub struct ComputeState {
86    /// State kept for each installed compute collection.
87    ///
88    /// Each collection has exactly one frontier.
89    /// How the frontier is communicated depends on the collection type:
90    ///  * Frontiers of indexes are equal to the frontier of their corresponding traces in the
91    ///    `TraceManager`.
92    ///  * Persist sinks store their current frontier in `CollectionState::sink_write_frontier`.
93    ///  * Subscribes report their frontiers through the `subscribe_response_buffer`.
94    pub collections: BTreeMap<GlobalId, CollectionState>,
95    /// The traces available for sharing across dataflows.
96    pub traces: TraceManager,
97    /// Shared buffer with SUBSCRIBE operator instances by which they can respond.
98    ///
99    /// The entries are pairs of sink identifier (to identify the subscribe instance)
100    /// and the response itself.
101    pub subscribe_response_buffer: Rc<RefCell<Vec<(GlobalId, SubscribeResponse)>>>,
102    /// Shared buffer with S3 oneshot operator instances by which they can respond.
103    ///
104    /// The entries are pairs of sink identifier (to identify the s3 oneshot instance)
105    /// and the response itself.
106    pub copy_to_response_buffer: Rc<RefCell<Vec<(GlobalId, CopyToResponse)>>>,
107    /// Peek commands that are awaiting fulfillment.
108    pub pending_peeks: BTreeMap<Uuid, PendingPeek>,
109    /// The persist location where we can stash large peek results.
110    pub peek_stash_persist_location: Option<PersistLocation>,
111    /// The logger, from Timely's logging framework, if logs are enabled.
112    pub compute_logger: Option<logging::compute::Logger>,
113    /// A process-global cache of (blob_uri, consensus_uri) -> PersistClient.
114    /// This is intentionally shared between workers.
115    pub persist_clients: Arc<PersistClientCache>,
116    /// Context necessary for rendering txn-wal operators.
117    pub txns_ctx: TxnsContext,
118    /// History of commands received by this workers and all its peers.
119    pub command_history: ComputeCommandHistory<UIntGauge>,
120    /// Max size in bytes of any result.
121    max_result_size: u64,
122    /// Specification for rendering linear joins.
123    pub linear_join_spec: LinearJoinSpec,
124    /// Metrics for this worker.
125    pub metrics: WorkerMetrics,
126    /// A process-global handle to tracing configuration.
127    tracing_handle: Arc<TracingHandle>,
128    /// Other configuration for compute
129    pub context: ComputeInstanceContext,
130    /// Per-worker dynamic configuration.
131    ///
132    /// This is separate from the process-global `ConfigSet` and contains config options that need
133    /// to be applied consistently with compute command order.
134    ///
135    /// For example, for options that influence dataflow rendering it is important that all workers
136    /// render the same dataflow with the same options. If these options were stored in a global
137    /// `ConfigSet`, we couldn't guarantee that all workers observe changes to them at the same
138    /// point in the stream of compute commands. Storing per-worker configuration ensures that
139    /// because each worker's configuration is only updated once that worker observes the
140    /// respective `UpdateConfiguration` command.
141    ///
142    /// Reference-counted to avoid cloning for `Context`.
143    pub worker_config: Rc<ConfigSet>,
144
145    /// The process-global metrics registry.
146    pub metrics_registry: MetricsRegistry,
147
148    /// The number of timely workers per process.
149    pub workers_per_process: usize,
150
151    /// Collections awaiting schedule instruction by the controller.
152    ///
153    /// Each entry stores a reference to a token that can be dropped to unsuspend the collection's
154    /// dataflow. Multiple collections can reference the same token if they are exported by the
155    /// same dataflow.
156    suspended_collections: BTreeMap<GlobalId, Rc<dyn Any>>,
157
158    /// Interval at which to perform server maintenance tasks. Set to a zero interval to
159    /// perform maintenance with every `step_or_park` invocation.
160    pub server_maintenance_interval: Duration,
161
162    /// The [`mz_ore::now::SYSTEM_TIME`] at which the replica was started.
163    ///
164    /// Used to compute `replica_expiration`.
165    pub init_system_time: EpochMillis,
166
167    /// The maximum time for which the replica is expected to live. If not empty, dataflows in the
168    /// replica can drop diffs associated with timestamps beyond the replica expiration.
169    /// The replica will panic if such dataflows are not dropped before the replica has expired.
170    pub replica_expiration: Antichain<Timestamp>,
171
172    /// The storage worker forwards its introspection logs to the compute worker.
173    pub storage_log_reader: Option<crate::server::StorageTimelyLogReader>,
174}
175
176impl ComputeState {
177    /// Construct a new `ComputeState`.
178    pub fn new(
179        persist_clients: Arc<PersistClientCache>,
180        txns_ctx: TxnsContext,
181        metrics: WorkerMetrics,
182        tracing_handle: Arc<TracingHandle>,
183        context: ComputeInstanceContext,
184        metrics_registry: MetricsRegistry,
185        workers_per_process: usize,
186        storage_log_reader: Option<crate::server::StorageTimelyLogReader>,
187    ) -> Self {
188        let traces = TraceManager::new(metrics.clone());
189        let command_history = ComputeCommandHistory::new(metrics.for_history());
190
191        Self {
192            collections: Default::default(),
193            traces,
194            subscribe_response_buffer: Default::default(),
195            copy_to_response_buffer: Default::default(),
196            pending_peeks: Default::default(),
197            peek_stash_persist_location: None,
198            compute_logger: None,
199            persist_clients,
200            txns_ctx,
201            command_history,
202            max_result_size: u64::MAX,
203            linear_join_spec: Default::default(),
204            metrics,
205            tracing_handle,
206            context,
207            worker_config: mz_dyncfgs::all_dyncfgs().into(),
208            metrics_registry,
209            workers_per_process,
210            suspended_collections: Default::default(),
211            server_maintenance_interval: Duration::ZERO,
212            init_system_time: mz_ore::now::SYSTEM_TIME(),
213            replica_expiration: Antichain::default(),
214            storage_log_reader,
215        }
216    }
217
218    /// Return a mutable reference to the identified collection.
219    ///
220    /// Panics if the collection doesn't exist.
221    pub fn expect_collection_mut(&mut self, id: GlobalId) -> &mut CollectionState {
222        self.collections
223            .get_mut(&id)
224            .expect("collection must exist")
225    }
226
227    /// Construct a new frontier probe for the given input and add it to the state of the given
228    /// collections.
229    ///
230    /// The caller is responsible for attaching the returned probe handle to the respective
231    /// dataflow input stream.
232    pub fn input_probe_for(
233        &mut self,
234        input_id: GlobalId,
235        collection_ids: impl Iterator<Item = GlobalId>,
236    ) -> probe::Handle<Timestamp> {
237        let probe = probe::Handle::default();
238        for id in collection_ids {
239            if let Some(collection) = self.collections.get_mut(&id) {
240                collection.input_probes.insert(input_id, probe.clone());
241            }
242        }
243        probe
244    }
245
246    /// Apply the current `worker_config` to the compute state.
247    fn apply_worker_config(&mut self) {
248        use mz_compute_types::dyncfgs::*;
249
250        let config = &self.worker_config;
251
252        self.linear_join_spec = LinearJoinSpec::from_config(config);
253
254        if ENABLE_LGALLOC.get(config) {
255            if let Some(path) = &self.context.scratch_directory {
256                let clear_bytes = LGALLOC_SLOW_CLEAR_BYTES.get(config);
257                let eager_return = ENABLE_LGALLOC_EAGER_RECLAMATION.get(config);
258                let file_growth_dampener = LGALLOC_FILE_GROWTH_DAMPENER.get(config);
259                let interval = LGALLOC_BACKGROUND_INTERVAL.get(config);
260                let local_buffer_bytes = LGALLOC_LOCAL_BUFFER_BYTES.get(config);
261                info!(
262                    ?path,
263                    backgrund_interval=?interval,
264                    clear_bytes,
265                    eager_return,
266                    file_growth_dampener,
267                    local_buffer_bytes,
268                    "enabling lgalloc"
269                );
270                let background_worker_config = lgalloc::BackgroundWorkerConfig {
271                    interval,
272                    clear_bytes,
273                };
274                lgalloc::lgalloc_set_config(
275                    lgalloc::LgAlloc::new()
276                        .enable()
277                        .with_path(path.clone())
278                        .with_background_config(background_worker_config)
279                        .eager_return(eager_return)
280                        .file_growth_dampener(file_growth_dampener)
281                        .local_buffer_bytes(local_buffer_bytes),
282                );
283            } else {
284                debug!("not enabling lgalloc, scratch directory not specified");
285            }
286        } else {
287            info!("disabling lgalloc");
288            lgalloc::lgalloc_set_config(lgalloc::LgAlloc::new().disable());
289        }
290
291        // Pager backend selection follows scratch-directory availability:
292        // a scratch dir means the file backend; no scratch dir means swap.
293        // `set_scratch_dir` and `set_backend` are both idempotent, so calling
294        // on every `apply_worker_config` tick is safe. The pager module is
295        // only compiled on Unix targets (`mz_ore::pager` is `cfg(unix)`).
296        #[cfg(unix)]
297        if let Some(path) = &self.context.scratch_directory {
298            mz_ore::pager::set_scratch_dir(path.clone());
299            mz_ore::pager::set_backend(mz_ore::pager::Backend::File);
300        } else {
301            mz_ore::pager::set_backend(mz_ore::pager::Backend::Swap);
302        }
303
304        crate::memory_limiter::apply_limiter_config(config);
305
306        mz_ore::region::ENABLE_LGALLOC_REGION.store(
307            ENABLE_COLUMNATION_LGALLOC.get(config),
308            std::sync::atomic::Ordering::Relaxed,
309        );
310
311        // NB: arrangement dictionary compression is deliberately NOT applied here. Unlike the
312        // settings above, it is captured once at replica creation (see `handle_create_instance`
313        // and `InstanceConfig::arrangement_dictionary_compression`) and held fixed, so that
314        // flipping the flag does not retroactively change arrangements on existing replicas.
315
316        // Apply column-paged-batcher configuration. Routes through
317        // `apply_tiered_config`, which reuses a process-wide `TieredPolicy`
318        // singleton — operator-driven tunes mutate the existing atomics
319        // rather than installing a fresh policy with a fresh budget atomic
320        // that would orphan in-flight resident tickets.
321        //
322        // Backend selection mirrors the lower-level `mz_ore::pager`
323        // already configured above: file when a scratch directory is
324        // available, swap otherwise.
325        {
326            use mz_ore::pager::Backend;
327            use mz_timely_util::column_pager::{Codec, apply_tiered_config};
328
329            let enabled = ENABLE_COLUMN_PAGED_BATCHER_SPILL.get(config);
330            let codec = COLUMN_PAGED_BATCHER_LZ4.get(config).then_some(Codec::Lz4);
331            let swap_pageout = COLUMN_PAGED_BATCHER_SWAP_PAGEOUT.get(config);
332
333            // Budget derivation: fraction × announced memory limit, with a
334            // 128 MiB floor so the no-pressure case doesn't page per chunk.
335            // Falls back to a 4 GiB assumption if no limit was announced
336            // (e.g. dev environments).
337            const MIB: usize = 1024 * 1024;
338            const DEFAULT_MEM_LIMIT: usize = 4 * 1024 * MIB;
339            let mem_limit = crate::memory_limiter::get_memory_limit().unwrap_or(DEFAULT_MEM_LIMIT);
340            let fraction = COLUMN_PAGED_BATCHER_BUDGET_FRACTION.get(config).max(0.0);
341            let total = usize::cast_lossy(f64::cast_lossy(mem_limit) * fraction).max(128 * MIB);
342
343            let backend = if self.context.scratch_directory.is_some() {
344                Backend::File
345            } else {
346                Backend::Swap
347            };
348
349            debug!(
350                enabled,
351                ?backend,
352                ?codec,
353                swap_pageout,
354                fraction,
355                mem_limit,
356                budget_bytes = total,
357                "column-paged batcher: applying tiered config",
358            );
359            apply_tiered_config(enabled, total, backend, codec, swap_pageout);
360        }
361
362        // Remember the maintenance interval locally to avoid reading it from the config set on
363        // every server iteration.
364        self.server_maintenance_interval = COMPUTE_SERVER_MAINTENANCE_INTERVAL.get(config);
365
366        let overflowing_behavior = ORE_OVERFLOWING_BEHAVIOR.get(config);
367        match overflowing_behavior.parse() {
368            Ok(behavior) => mz_ore::overflowing::set_behavior(behavior),
369            Err(err) => {
370                error!(
371                    err,
372                    overflowing_behavior, "Invalid value for ore_overflowing_behavior"
373                );
374            }
375        }
376    }
377
378    /// Apply the provided replica expiration `offset` by converting it to a frontier relative to
379    /// the replica's initialization system time.
380    ///
381    /// Only expected to be called once when creating the instance. Guards against calling it
382    /// multiple times by checking if the local expiration time is set.
383    pub fn apply_expiration_offset(&mut self, offset: Duration) {
384        if self.replica_expiration.is_empty() {
385            let offset: EpochMillis = offset
386                .as_millis()
387                .try_into()
388                .expect("duration must fit within u64");
389            let replica_expiration_millis = self.init_system_time + offset;
390            let replica_expiration = Timestamp::from(replica_expiration_millis);
391
392            info!(
393                offset = %offset,
394                replica_expiration_millis = %replica_expiration_millis,
395                replica_expiration_utc = %mz_ore::now::to_datetime(replica_expiration_millis),
396                "setting replica expiration",
397            );
398            self.replica_expiration = Antichain::from_elem(replica_expiration);
399
400            // Record the replica expiration in the metrics.
401            self.metrics
402                .replica_expiration_timestamp_seconds
403                .set(replica_expiration.into());
404        }
405    }
406
407    /// Returns the cc or non-cc version of "dataflow_max_inflight_bytes", as
408    /// appropriate to this replica.
409    pub fn dataflow_max_inflight_bytes(&self) -> Option<usize> {
410        use mz_compute_types::dyncfgs::{
411            DATAFLOW_MAX_INFLIGHT_BYTES, DATAFLOW_MAX_INFLIGHT_BYTES_CC,
412        };
413
414        if self.persist_clients.cfg.is_cc_active {
415            DATAFLOW_MAX_INFLIGHT_BYTES_CC.get(&self.worker_config)
416        } else {
417            DATAFLOW_MAX_INFLIGHT_BYTES.get(&self.worker_config)
418        }
419    }
420}
421
422/// A wrapper around [ComputeState] with a live timely worker and response channel.
423pub(crate) struct ActiveComputeState<'a> {
424    /// The underlying Timely worker.
425    pub timely_worker: &'a mut TimelyWorker,
426    /// The compute state itself.
427    pub compute_state: &'a mut ComputeState,
428    /// The channel over which frontier information is reported.
429    pub response_tx: &'a mut ResponseSender,
430}
431
432/// A token that keeps a sink alive.
433pub struct SinkToken(#[allow(dead_code)] Box<dyn Any>);
434
435impl SinkToken {
436    /// Create a new `SinkToken`.
437    pub fn new(t: Box<dyn Any>) -> Self {
438        Self(t)
439    }
440}
441
442impl<'a> ActiveComputeState<'a> {
443    /// Entrypoint for applying a compute command.
444    #[mz_ore::instrument(level = "debug")]
445    pub fn handle_compute_command(&mut self, cmd: ComputeCommand) {
446        use ComputeCommand::*;
447
448        self.compute_state.command_history.push(cmd.clone());
449
450        // Record the command duration, per worker and command kind.
451        let timer = self
452            .compute_state
453            .metrics
454            .handle_command_duration_seconds
455            .for_command(&cmd)
456            .start_timer();
457
458        match cmd {
459            Hello { .. } => panic!("Hello must be captured before"),
460            CreateInstance(instance_config) => self.handle_create_instance(*instance_config),
461            InitializationComplete => (),
462            UpdateConfiguration(params) => self.handle_update_configuration(*params),
463            CreateDataflow(dataflow) => self.handle_create_dataflow(*dataflow),
464            Schedule(id) => self.handle_schedule(id),
465            AllowCompaction { id, frontier } => self.handle_allow_compaction(id, frontier),
466            Peek(peek) => {
467                peek.otel_ctx.attach_as_parent();
468                self.handle_peek(*peek)
469            }
470            CancelPeek { uuid } => self.handle_cancel_peek(uuid),
471            AllowWrites(id) => {
472                self.handle_allow_writes(id);
473            }
474        }
475
476        timer.observe_duration();
477    }
478
479    fn handle_create_instance(&mut self, config: InstanceConfig) {
480        // Seed the worker configuration with the controller's snapshot before applying it, so
481        // create-time setup observes controller-synced values rather than dyncfg defaults. The
482        // same values arrive again in the following `UpdateConfiguration`, which applies globally
483        // and keeps the configuration current. An empty snapshot leaves the defaults in place.
484        config
485            .initial_config
486            .apply(&self.compute_state.worker_config);
487
488        // Ensure the state is consistent with the config before we initialize anything.
489        self.compute_state.apply_worker_config();
490
491        // Apply dictionary compression exactly once, here at instance creation, from the value the
492        // controller captured when the replica was created. We deliberately do NOT re-apply it on
493        // `handle_update_configuration`, so flipping the flag does not retroactively change this
494        // replica's arrangements. `DICTIONARY_COMPRESSION` is process-global and a replica process
495        // hosts a single instance, so this single store covers all of the replica's arrangements.
496        mz_row_spine::DICTIONARY_COMPRESSION.store(
497            config.arrangement_dictionary_compression,
498            std::sync::atomic::Ordering::Relaxed,
499        );
500
501        if let Some(offset) = config.expiration_offset {
502            self.compute_state.apply_expiration_offset(offset);
503        }
504
505        let storage_log_reader = self.compute_state.storage_log_reader.take();
506        self.initialize_logging(config.logging, storage_log_reader);
507
508        self.compute_state.peek_stash_persist_location = Some(config.peek_stash_persist_location);
509    }
510
511    fn handle_update_configuration(&mut self, params: ComputeParameters) {
512        debug!("Applying configuration update: {params:?}");
513
514        let ComputeParameters {
515            workload_class,
516            max_result_size,
517            tracing,
518            grpc_client: _grpc_client,
519            dyncfg_updates,
520        } = params;
521
522        if let Some(v) = workload_class {
523            self.compute_state.metrics.set_workload_class(v);
524        }
525        if let Some(v) = max_result_size {
526            self.compute_state.max_result_size = v;
527        }
528
529        tracing.apply(self.compute_state.tracing_handle.as_ref());
530
531        dyncfg_updates.apply(&self.compute_state.worker_config);
532        self.compute_state
533            .persist_clients
534            .cfg()
535            .apply_from(&dyncfg_updates);
536
537        // Note: We're only updating mz_metrics from the compute state here, but not from the
538        // equivalent storage state. This is because they're running on the same process and
539        // share the metrics.
540        mz_metrics::update_dyncfg(&dyncfg_updates);
541
542        self.compute_state.apply_worker_config();
543    }
544
545    fn handle_create_dataflow(
546        &mut self,
547        dataflow: DataflowDescription<RenderPlan, CollectionMetadata>,
548    ) {
549        let dataflow_index = Rc::new(self.timely_worker.next_dataflow_index());
550        let as_of = dataflow.as_of.clone().unwrap();
551
552        let dataflow_expiration = dataflow
553            .time_dependence
554            .as_ref()
555            .map(|time_dependence| {
556                self.determine_dataflow_expiration(time_dependence, &dataflow.until)
557            })
558            .unwrap_or_default();
559
560        // Add the dataflow expiration to `until`.
561        let until = dataflow.until.meet(&dataflow_expiration);
562
563        if dataflow.is_transient() {
564            debug!(
565                name = %dataflow.debug_name,
566                import_ids = %dataflow.display_import_ids(),
567                export_ids = %dataflow.display_export_ids(),
568                as_of = ?as_of.elements(),
569                time_dependence = ?dataflow.time_dependence,
570                expiration = ?dataflow_expiration.elements(),
571                expiration_datetime = ?dataflow_expiration
572                    .as_option()
573                    .map(|t| mz_ore::now::to_datetime(t.into())),
574                plan_until = ?dataflow.until.elements(),
575                until = ?until.elements(),
576                "creating dataflow",
577            );
578        } else {
579            info!(
580                name = %dataflow.debug_name,
581                import_ids = %dataflow.display_import_ids(),
582                export_ids = %dataflow.display_export_ids(),
583                as_of = ?as_of.elements(),
584                time_dependence = ?dataflow.time_dependence,
585                expiration = ?dataflow_expiration.elements(),
586                expiration_datetime = ?dataflow_expiration
587                    .as_option()
588                    .map(|t| mz_ore::now::to_datetime(t.into())),
589                plan_until = ?dataflow.until.elements(),
590                until = ?until.elements(),
591                "creating dataflow",
592            );
593        };
594
595        let subscribe_copy_ids: BTreeSet<_> = dataflow
596            .subscribe_ids()
597            .chain(dataflow.copy_to_ids())
598            .collect();
599
600        // Initialize compute and logging state for each object.
601        for object_id in dataflow.export_ids() {
602            let is_subscribe_or_copy = subscribe_copy_ids.contains(&object_id);
603            let metrics = self.compute_state.metrics.for_collection(object_id);
604            let mut collection = CollectionState::new(
605                Rc::clone(&dataflow_index),
606                is_subscribe_or_copy,
607                as_of.clone(),
608                metrics,
609            );
610
611            if let Some(logger) = self.compute_state.compute_logger.clone() {
612                let logging = CollectionLogging::new(
613                    object_id,
614                    logger,
615                    *dataflow_index,
616                    dataflow.import_ids(),
617                );
618                collection.logging = Some(logging);
619            }
620
621            collection.reset_reported_frontiers(ReportedFrontier::NotReported {
622                lower: as_of.clone(),
623            });
624
625            let existing = self.compute_state.collections.insert(object_id, collection);
626            if existing.is_some() {
627                error!(
628                    id = ?object_id,
629                    "existing collection for newly created dataflow",
630                );
631            }
632        }
633
634        let (start_signal, suspension_token) = StartSignal::new();
635        for id in dataflow.export_ids() {
636            self.compute_state
637                .suspended_collections
638                .insert(id, Rc::clone(&suspension_token));
639        }
640
641        crate::render::build_compute_dataflow(
642            self.timely_worker,
643            self.compute_state,
644            dataflow,
645            start_signal,
646            until,
647            dataflow_expiration,
648        );
649    }
650
651    fn handle_schedule(&mut self, id: GlobalId) {
652        // A `Schedule` command instructs us to begin dataflow computation for a collection, so
653        // we should unsuspend it by dropping the corresponding suspension token. Note that a
654        // dataflow can export multiple collections and they all share one suspension token, so the
655        // computation of a dataflow will only start once all its exported collections have been
656        // scheduled.
657        let suspension_token = self.compute_state.suspended_collections.remove(&id);
658        drop(suspension_token);
659    }
660
661    fn handle_allow_compaction(&mut self, id: GlobalId, frontier: Antichain<Timestamp>) {
662        if frontier.is_empty() {
663            // Indicates that we may drop `id`, as there are no more valid times to read.
664            self.drop_collection(id);
665        } else {
666            self.compute_state
667                .traces
668                .allow_compaction(id, frontier.borrow());
669        }
670    }
671
672    #[mz_ore::instrument(level = "debug")]
673    fn handle_peek(&mut self, peek: Peek) {
674        let pending = match &peek.target {
675            PeekTarget::Index { id } => {
676                // Acquire a copy of the trace suitable for fulfilling the peek.
677                let trace_bundle = self.compute_state.traces.get(id).unwrap().clone();
678                PendingPeek::index(peek, trace_bundle)
679            }
680            PeekTarget::Persist { metadata, .. } => {
681                let metadata = metadata.clone();
682                PendingPeek::persist(
683                    peek,
684                    Arc::clone(&self.compute_state.persist_clients),
685                    metadata,
686                    usize::cast_from(self.compute_state.max_result_size),
687                    self.timely_worker,
688                )
689            }
690        };
691
692        // Log the receipt of the peek.
693        if let Some(logger) = self.compute_state.compute_logger.as_mut() {
694            logger.log(&pending.as_log_event(true));
695        }
696
697        self.process_peek(&mut Antichain::new(), pending);
698    }
699
700    fn handle_cancel_peek(&mut self, uuid: Uuid) {
701        if let Some(peek) = self.compute_state.pending_peeks.remove(&uuid) {
702            self.send_peek_response(peek, PeekResponse::Canceled);
703        }
704    }
705
706    fn handle_allow_writes(&mut self, id: GlobalId) {
707        // Enable persist compaction on any allow-writes command. We
708        // assume persist only compacts after making durable changes,
709        // such as appending a batch or advancing the upper.
710        self.compute_state.persist_clients.cfg().enable_compaction();
711
712        if let Some(collection) = self.compute_state.collections.get_mut(&id) {
713            collection.allow_writes();
714        } else {
715            soft_panic_or_log!("allow writes for unknown collection {id}");
716        }
717    }
718
719    /// Drop the given collection.
720    fn drop_collection(&mut self, id: GlobalId) {
721        let collection = self
722            .compute_state
723            .collections
724            .remove(&id)
725            .expect("dropped untracked collection");
726
727        // If this collection is an index, remove its trace.
728        self.compute_state.traces.remove(&id);
729        // If the collection is unscheduled, remove it from the list of waiting collections.
730        self.compute_state.suspended_collections.remove(&id);
731
732        // Drop the dataflow, if all its exports have been dropped.
733        if let Ok(index) = Rc::try_unwrap(collection.dataflow_index) {
734            self.timely_worker.drop_dataflow(index);
735        }
736
737        // The compute protocol requires us to send a `Frontiers` response with empty frontiers
738        // when a collection was dropped, unless:
739        //  * The frontier was already reported as empty previously, or
740        //  * The collection is a subscribe or copy-to.
741        if !collection.is_subscribe_or_copy {
742            let reported = collection.reported_frontiers;
743            let write_frontier = (!reported.write_frontier.is_empty()).then(Antichain::new);
744            let input_frontier = (!reported.input_frontier.is_empty()).then(Antichain::new);
745            let output_frontier = (!reported.output_frontier.is_empty()).then(Antichain::new);
746
747            let frontiers = FrontiersResponse {
748                write_frontier,
749                input_frontier,
750                output_frontier,
751            };
752            if frontiers.has_updates() {
753                self.send_compute_response(ComputeResponse::Frontiers(id, frontiers));
754            }
755        }
756    }
757
758    /// Initializes timely dataflow logging and publishes as a view.
759    pub fn initialize_logging(
760        &mut self,
761        config: LoggingConfig,
762        storage_log_reader: Option<crate::server::StorageTimelyLogReader>,
763    ) {
764        if self.compute_state.compute_logger.is_some() {
765            panic!("dataflow server has already initialized logging");
766        }
767
768        let LoggingTraces {
769            traces,
770            dataflow_index,
771            compute_logger: logger,
772        } = logging::initialize(
773            self.timely_worker,
774            &config,
775            self.compute_state.metrics_registry.clone(),
776            Rc::clone(&self.compute_state.worker_config),
777            self.compute_state.workers_per_process,
778            storage_log_reader,
779        );
780
781        let dataflow_index = Rc::new(dataflow_index);
782        let mut log_index_ids = config.index_logs;
783        for (log, trace) in traces {
784            // Install trace as maintained index.
785            let id = log_index_ids
786                .remove(&log)
787                .expect("`logging::initialize` does not invent logs");
788            self.compute_state.traces.set(id, trace);
789
790            // Initialize compute and logging state for the logging index.
791            let is_subscribe_or_copy = false;
792            let as_of = Antichain::from_elem(Timestamp::MIN);
793            let metrics = self.compute_state.metrics.for_collection(id);
794            let mut collection = CollectionState::new(
795                Rc::clone(&dataflow_index),
796                is_subscribe_or_copy,
797                as_of,
798                metrics,
799            );
800
801            let logging =
802                CollectionLogging::new(id, logger.clone(), *dataflow_index, std::iter::empty());
803            collection.logging = Some(logging);
804
805            let existing = self.compute_state.collections.insert(id, collection);
806            if existing.is_some() {
807                error!(
808                    id = ?id,
809                    "existing collection for newly initialized logging export",
810                );
811            }
812        }
813
814        // Sanity check.
815        assert!(
816            log_index_ids.is_empty(),
817            "failed to create requested logging indexes: {log_index_ids:?}",
818        );
819
820        self.compute_state.compute_logger = Some(logger);
821    }
822
823    /// Send progress information to the controller.
824    pub fn report_frontiers(&mut self) {
825        let mut responses = Vec::new();
826
827        // Maintain a single allocation for `new_frontier` to avoid allocating on every iteration.
828        let mut new_frontier = Antichain::new();
829
830        for (&id, collection) in self.compute_state.collections.iter_mut() {
831            // The compute protocol does not allow `Frontiers` responses for subscribe and copy-to
832            // collections (database-issues#4701).
833            if collection.is_subscribe_or_copy {
834                continue;
835            }
836
837            let reported = collection.reported_frontiers();
838
839            // Collect the write frontier and check for progress.
840            new_frontier.clear();
841            if let Some(traces) = self.compute_state.traces.get_mut(&id) {
842                assert!(
843                    collection.sink_write_frontier.is_none(),
844                    "collection {id} has multiple frontiers"
845                );
846                traces.oks_mut().read_upper(&mut new_frontier);
847            } else if let Some(frontier) = &collection.sink_write_frontier {
848                new_frontier.clone_from(&frontier.borrow());
849            } else {
850                error!(id = ?id, "collection without write frontier");
851                continue;
852            }
853            let new_write_frontier = reported
854                .write_frontier
855                .allows_reporting(&new_frontier)
856                .then(|| new_frontier.clone());
857
858            // Collect the output frontier and check for progress.
859            //
860            // By default, the output frontier equals the write frontier (which is still stored in
861            // `new_frontier`). If the collection provides a compute frontier, we construct the
862            // output frontier by taking the meet of write and compute frontier, to avoid:
863            //  * reporting progress through times we have not yet written
864            //  * reporting progress through times we have not yet fully processed, for
865            //    collections that jump their write frontiers into the future
866            //
867            // As a special case, in read-only mode we don't take the write frontier into account.
868            // The dataflow doesn't have the ability to push it forward, so it can't be used as a
869            // measure of dataflow progress.
870            if let Some(probe) = &collection.compute_probe {
871                if *collection.read_only_rx.borrow() {
872                    new_frontier.clear();
873                }
874                probe.with_frontier(|frontier| new_frontier.extend(frontier.iter().copied()));
875            }
876            let new_output_frontier = reported
877                .output_frontier
878                .allows_reporting(&new_frontier)
879                .then(|| new_frontier.clone());
880
881            // Collect the input frontier and check for progress.
882            new_frontier.clear();
883            for probe in collection.input_probes.values() {
884                probe.with_frontier(|frontier| new_frontier.extend(frontier.iter().copied()));
885            }
886            let new_input_frontier = reported
887                .input_frontier
888                .allows_reporting(&new_frontier)
889                .then(|| new_frontier.clone());
890
891            if let Some(frontier) = &new_write_frontier {
892                collection
893                    .set_reported_write_frontier(ReportedFrontier::Reported(frontier.clone()));
894            }
895            if let Some(frontier) = &new_input_frontier {
896                collection
897                    .set_reported_input_frontier(ReportedFrontier::Reported(frontier.clone()));
898            }
899            if let Some(frontier) = &new_output_frontier {
900                collection
901                    .set_reported_output_frontier(ReportedFrontier::Reported(frontier.clone()));
902            }
903
904            let response = FrontiersResponse {
905                write_frontier: new_write_frontier,
906                input_frontier: new_input_frontier,
907                output_frontier: new_output_frontier,
908            };
909            if response.has_updates() {
910                responses.push((id, response));
911            }
912        }
913
914        for (id, frontiers) in responses {
915            self.send_compute_response(ComputeResponse::Frontiers(id, frontiers));
916        }
917    }
918
919    /// Report per-worker metrics.
920    pub(crate) fn report_metrics(&self) {
921        if let Some(expiration) = self.compute_state.replica_expiration.as_option() {
922            let now = Duration::from_millis(mz_ore::now::SYSTEM_TIME()).as_secs_f64();
923            let expiration = Duration::from_millis(<u64>::from(expiration)).as_secs_f64();
924            let remaining = expiration - now;
925            self.compute_state
926                .metrics
927                .replica_expiration_remaining_seconds
928                .set(remaining)
929        }
930    }
931
932    /// Either complete the peek (and send the response) or put it in the pending set.
933    fn process_peek(&mut self, upper: &mut Antichain<Timestamp>, mut peek: PendingPeek) {
934        let response = match &mut peek {
935            PendingPeek::Index(peek) => {
936                let start = Instant::now();
937
938                let peek_stash_eligible = peek
939                    .peek
940                    .finishing
941                    .is_streamable(peek.peek.result_desc.arity());
942
943                let peek_stash_enabled = {
944                    let enabled = ENABLE_PEEK_RESPONSE_STASH.get(&self.compute_state.worker_config);
945                    let peek_persist_stash_available =
946                        self.compute_state.peek_stash_persist_location.is_some();
947                    if !peek_persist_stash_available && enabled {
948                        error!("missing peek_stash_persist_location but peek stash is enabled");
949                    }
950                    enabled && peek_persist_stash_available
951                };
952
953                let peek_stash_threshold_bytes =
954                    PEEK_RESPONSE_STASH_THRESHOLD_BYTES.get(&self.compute_state.worker_config);
955
956                let metrics = IndexPeekMetrics {
957                    seek_fulfillment_seconds: &self
958                        .compute_state
959                        .metrics
960                        .index_peek_seek_fulfillment_seconds,
961                    frontier_check_seconds: &self
962                        .compute_state
963                        .metrics
964                        .index_peek_frontier_check_seconds,
965                    error_scan_seconds: &self.compute_state.metrics.index_peek_error_scan_seconds,
966                    cursor_setup_seconds: &self
967                        .compute_state
968                        .metrics
969                        .index_peek_cursor_setup_seconds,
970                    row_iteration_seconds: &self
971                        .compute_state
972                        .metrics
973                        .index_peek_row_iteration_seconds,
974                    result_sort_seconds: &self.compute_state.metrics.index_peek_result_sort_seconds,
975                    row_collection_seconds: &self
976                        .compute_state
977                        .metrics
978                        .index_peek_row_collection_seconds,
979                };
980
981                let status = peek.seek_fulfillment(
982                    upper,
983                    self.compute_state.max_result_size,
984                    peek_stash_enabled && peek_stash_eligible,
985                    peek_stash_threshold_bytes,
986                    &metrics,
987                );
988
989                self.compute_state
990                    .metrics
991                    .index_peek_total_seconds
992                    .observe(start.elapsed().as_secs_f64());
993
994                match status {
995                    PeekStatus::Ready(result) => Some(result),
996                    PeekStatus::NotReady => None,
997                    PeekStatus::UsePeekStash => {
998                        let _span =
999                            span!(parent: &peek.span, Level::DEBUG, "process_stash_peek").entered();
1000
1001                        let peek_stash_batch_max_runs = PEEK_RESPONSE_STASH_BATCH_MAX_RUNS
1002                            .get(&self.compute_state.worker_config);
1003
1004                        let stash_task = peek_stash::StashingPeek::start_upload(
1005                            Arc::clone(&self.compute_state.persist_clients),
1006                            self.compute_state
1007                                .peek_stash_persist_location
1008                                .as_ref()
1009                                .expect("verified above"),
1010                            peek.peek.clone(),
1011                            peek.trace_bundle.clone(),
1012                            peek_stash_batch_max_runs,
1013                        );
1014
1015                        self.compute_state
1016                            .pending_peeks
1017                            .insert(peek.peek.uuid, PendingPeek::Stash(stash_task));
1018                        return;
1019                    }
1020                }
1021            }
1022            PendingPeek::Persist(peek) => peek.result.try_recv().ok().map(|(result, duration)| {
1023                self.compute_state
1024                    .metrics
1025                    .persist_peek_seconds
1026                    .observe(duration.as_secs_f64());
1027                result
1028            }),
1029            PendingPeek::Stash(stashing_peek) => {
1030                let num_batches = PEEK_STASH_NUM_BATCHES.get(&self.compute_state.worker_config);
1031                let batch_size = PEEK_STASH_BATCH_SIZE.get(&self.compute_state.worker_config);
1032                stashing_peek.pump_rows(num_batches, batch_size);
1033
1034                if let Ok((response, duration)) = stashing_peek.result.try_recv() {
1035                    self.compute_state
1036                        .metrics
1037                        .stashed_peek_seconds
1038                        .observe(duration.as_secs_f64());
1039                    trace!(?stashing_peek.peek, ?duration, "finished stashing peek response in persist");
1040
1041                    Some(response)
1042                } else {
1043                    None
1044                }
1045            }
1046        };
1047
1048        if let Some(response) = response {
1049            let _span = span!(parent: peek.span(), Level::DEBUG, "process_peek_response").entered();
1050            self.send_peek_response(peek, response)
1051        } else {
1052            let uuid = peek.peek().uuid;
1053            self.compute_state.pending_peeks.insert(uuid, peek);
1054        }
1055    }
1056
1057    /// Scan pending peeks and attempt to retire each.
1058    pub fn process_peeks(&mut self) {
1059        let mut upper = Antichain::new();
1060        let pending_peeks = std::mem::take(&mut self.compute_state.pending_peeks);
1061        for (_uuid, peek) in pending_peeks {
1062            self.process_peek(&mut upper, peek);
1063        }
1064    }
1065
1066    /// Sends a response for this peek's resolution to the coordinator.
1067    ///
1068    /// Note that this function takes ownership of the `PendingPeek`, which is
1069    /// meant to prevent multiple responses to the same peek.
1070    #[mz_ore::instrument(level = "debug")]
1071    fn send_peek_response(&mut self, peek: PendingPeek, response: PeekResponse) {
1072        let log_event = peek.as_log_event(false);
1073        // Respond with the response.
1074        self.send_compute_response(ComputeResponse::PeekResponse(
1075            peek.peek().uuid,
1076            response,
1077            OpenTelemetryContext::obtain(),
1078        ));
1079
1080        // Log responding to the peek request.
1081        if let Some(logger) = self.compute_state.compute_logger.as_mut() {
1082            logger.log(&log_event);
1083        }
1084    }
1085
1086    /// Scan the shared subscribe response buffer, and forward results along.
1087    pub fn process_subscribes(&mut self) {
1088        let mut subscribe_responses = self.compute_state.subscribe_response_buffer.borrow_mut();
1089        for (sink_id, mut response) in subscribe_responses.drain(..) {
1090            // Update frontier logging for this subscribe.
1091            if let Some(collection) = self.compute_state.collections.get_mut(&sink_id) {
1092                let new_frontier = match &response {
1093                    SubscribeResponse::Batch(b) => b.upper.clone(),
1094                    SubscribeResponse::DroppedAt(_) => Antichain::new(),
1095                };
1096
1097                let reported = collection.reported_frontiers();
1098                assert!(
1099                    reported.write_frontier.allows_reporting(&new_frontier),
1100                    "subscribe write frontier regression: {:?} -> {:?}",
1101                    reported.write_frontier,
1102                    new_frontier,
1103                );
1104                assert!(
1105                    reported.input_frontier.allows_reporting(&new_frontier),
1106                    "subscribe input frontier regression: {:?} -> {:?}",
1107                    reported.input_frontier,
1108                    new_frontier,
1109                );
1110
1111                collection
1112                    .set_reported_write_frontier(ReportedFrontier::Reported(new_frontier.clone()));
1113                collection
1114                    .set_reported_input_frontier(ReportedFrontier::Reported(new_frontier.clone()));
1115                collection.set_reported_output_frontier(ReportedFrontier::Reported(new_frontier));
1116            } else {
1117                // Presumably tracking state for this subscribe was already dropped by
1118                // `drop_collection`. There is nothing left to do for logging.
1119            }
1120
1121            response
1122                .to_error_if_exceeds(usize::try_from(self.compute_state.max_result_size).unwrap());
1123            self.send_compute_response(ComputeResponse::SubscribeResponse(sink_id, response));
1124        }
1125    }
1126
1127    /// Scan the shared copy to response buffer, and forward results along.
1128    pub fn process_copy_tos(&self) {
1129        let mut responses = self.compute_state.copy_to_response_buffer.borrow_mut();
1130        for (sink_id, response) in responses.drain(..) {
1131            self.send_compute_response(ComputeResponse::CopyToResponse(sink_id, response));
1132        }
1133    }
1134
1135    /// Send a response to the coordinator.
1136    fn send_compute_response(&self, response: ComputeResponse) {
1137        // Ignore send errors because the coordinator is free to ignore our
1138        // responses. This happens during shutdown.
1139        let _ = self.response_tx.send(response);
1140    }
1141
1142    /// Checks for dataflow expiration. Panics if we're past the replica expiration time.
1143    pub(crate) fn check_expiration(&self) {
1144        let now = mz_ore::now::SYSTEM_TIME();
1145        if self.compute_state.replica_expiration.less_than(&now.into()) {
1146            let now_datetime = mz_ore::now::to_datetime(now);
1147            let expiration_datetime = self
1148                .compute_state
1149                .replica_expiration
1150                .as_option()
1151                .map(Into::into)
1152                .map(mz_ore::now::to_datetime);
1153
1154            // We error and assert separately to produce structured logs in anything that depends
1155            // on tracing.
1156            error!(
1157                now,
1158                now_datetime = ?now_datetime,
1159                expiration = ?self.compute_state.replica_expiration.elements(),
1160                expiration_datetime = ?expiration_datetime,
1161                "replica expired"
1162            );
1163
1164            // Repeat condition for better error message.
1165            assert!(
1166                !self.compute_state.replica_expiration.less_than(&now.into()),
1167                "replica expired. now: {now} ({now_datetime:?}), expiration: {:?} ({expiration_datetime:?})",
1168                self.compute_state.replica_expiration.elements(),
1169            );
1170        }
1171    }
1172
1173    /// Returns the dataflow expiration, i.e, the timestamp beyond which diffs can be
1174    /// dropped.
1175    ///
1176    /// Returns an empty timestamp if `replica_expiration` is unset or matches conditions under
1177    /// which dataflow expiration should be disabled.
1178    pub fn determine_dataflow_expiration(
1179        &self,
1180        time_dependence: &TimeDependence,
1181        until: &Antichain<Timestamp>,
1182    ) -> Antichain<Timestamp> {
1183        // Evaluate time dependence with respect to the expiration time.
1184        // * Step time forward to ensure the expiration time is different to the moment a dataflow
1185        //   can legitimately jump to.
1186        // * We cannot expire dataflow with an until that is less or equal to the expiration time.
1187        let iter = self
1188            .compute_state
1189            .replica_expiration
1190            .iter()
1191            .filter_map(|t| time_dependence.apply(*t))
1192            .filter_map(|t| Timestamp::try_step_forward(&t))
1193            .filter(|expiration| !until.less_equal(expiration));
1194        Antichain::from_iter(iter)
1195    }
1196}
1197
1198/// A peek against either an index or a Persist collection.
1199///
1200/// Note that `PendingPeek` intentionally does not implement or derive `Clone`,
1201/// as each `PendingPeek` is meant to be dropped after it's responded to.
1202pub enum PendingPeek {
1203    /// A peek against an index. (Possibly a temporary index created for the purpose.)
1204    Index(IndexPeek),
1205    /// A peek against a Persist-backed collection.
1206    Persist(PersistPeek),
1207    /// A peek against an index that is being stashed in the peek stash by an
1208    /// async background task.
1209    Stash(peek_stash::StashingPeek),
1210}
1211
1212impl PendingPeek {
1213    /// Produces a corresponding log event.
1214    pub fn as_log_event(&self, installed: bool) -> ComputeEvent {
1215        let peek = self.peek();
1216        let (id, peek_type) = match &peek.target {
1217            PeekTarget::Index { id } => (*id, logging::compute::PeekType::Index),
1218            PeekTarget::Persist { id, .. } => (*id, logging::compute::PeekType::Persist),
1219        };
1220        let uuid = peek.uuid.into_bytes();
1221        ComputeEvent::Peek(PeekEvent {
1222            id,
1223            time: peek.timestamp,
1224            uuid,
1225            peek_type,
1226            installed,
1227        })
1228    }
1229
1230    fn index(peek: Peek, mut trace_bundle: TraceBundle) -> Self {
1231        let empty_frontier = Antichain::new();
1232        let timestamp_frontier = Antichain::from_elem(peek.timestamp);
1233        trace_bundle
1234            .oks_mut()
1235            .set_logical_compaction(timestamp_frontier.borrow());
1236        trace_bundle
1237            .errs_mut()
1238            .set_logical_compaction(timestamp_frontier.borrow());
1239        trace_bundle
1240            .oks_mut()
1241            .set_physical_compaction(empty_frontier.borrow());
1242        trace_bundle
1243            .errs_mut()
1244            .set_physical_compaction(empty_frontier.borrow());
1245
1246        PendingPeek::Index(IndexPeek {
1247            peek,
1248            trace_bundle,
1249            span: tracing::Span::current(),
1250        })
1251    }
1252
1253    fn persist(
1254        peek: Peek,
1255        persist_clients: Arc<PersistClientCache>,
1256        metadata: CollectionMetadata,
1257        max_result_size: usize,
1258        timely_worker: &TimelyWorker,
1259    ) -> Self {
1260        let active_worker = {
1261            // Choose the worker that does the actual peek arbitrarily but consistently.
1262            let chosen_index = usize::cast_from(peek.uuid.hashed()) % timely_worker.peers();
1263            chosen_index == timely_worker.index()
1264        };
1265        let activator = timely_worker.sync_activator_for([].into());
1266        let peek_uuid = peek.uuid;
1267
1268        let (result_tx, result_rx) = oneshot::channel();
1269        let timestamp = peek.timestamp;
1270        let mfp_plan = peek.map_filter_project.clone();
1271        let max_results_needed = peek
1272            .finishing
1273            .limit
1274            .map(|l| usize::cast_from(u64::from(l)))
1275            .unwrap_or(usize::MAX)
1276            + peek.finishing.offset;
1277        let order_by = peek.finishing.order_by.clone();
1278
1279        // Persist peeks can include at most one literal constraint.
1280        let literal_constraint = peek
1281            .literal_constraints
1282            .clone()
1283            .map(|rows| rows.into_element());
1284
1285        let task_handle = mz_ore::task::spawn(|| "persist::peek", async move {
1286            let start = Instant::now();
1287            let result = if active_worker {
1288                PersistPeek::do_peek(
1289                    &persist_clients,
1290                    metadata,
1291                    timestamp,
1292                    literal_constraint,
1293                    mfp_plan,
1294                    max_result_size,
1295                    max_results_needed,
1296                )
1297                .await
1298            } else {
1299                Ok(vec![])
1300            };
1301            let result = match result {
1302                Ok(rows) => PeekResponse::Rows(vec![RowCollection::new(rows, &order_by)]),
1303                Err(e) => PeekResponse::Error(e.to_string()),
1304            };
1305            match result_tx.send((result, start.elapsed())) {
1306                Ok(()) => {}
1307                Err((_result, elapsed)) => {
1308                    debug!(duration =? elapsed, "dropping result for cancelled peek {peek_uuid}")
1309                }
1310            }
1311            match activator.activate() {
1312                Ok(()) => {}
1313                Err(_) => {
1314                    debug!("unable to wake timely after completed peek {peek_uuid}");
1315                }
1316            }
1317        });
1318        PendingPeek::Persist(PersistPeek {
1319            peek,
1320            _abort_handle: task_handle.abort_on_drop(),
1321            result: result_rx,
1322            span: tracing::Span::current(),
1323        })
1324    }
1325
1326    fn span(&self) -> &tracing::Span {
1327        match self {
1328            PendingPeek::Index(p) => &p.span,
1329            PendingPeek::Persist(p) => &p.span,
1330            PendingPeek::Stash(p) => &p.span,
1331        }
1332    }
1333
1334    pub(crate) fn peek(&self) -> &Peek {
1335        match self {
1336            PendingPeek::Index(p) => &p.peek,
1337            PendingPeek::Persist(p) => &p.peek,
1338            PendingPeek::Stash(p) => &p.peek,
1339        }
1340    }
1341}
1342
1343/// An in-progress Persist peek.
1344///
1345/// Note that `PendingPeek` intentionally does not implement or derive `Clone`,
1346/// as each `PendingPeek` is meant to be dropped after it's responded to.
1347pub struct PersistPeek {
1348    pub(crate) peek: Peek,
1349    /// A background task that's responsible for producing the peek results.
1350    /// If we're no longer interested in the results, we abort the task.
1351    _abort_handle: AbortOnDropHandle<()>,
1352    /// The result of the background task, eventually.
1353    result: oneshot::Receiver<(PeekResponse, Duration)>,
1354    /// The `tracing::Span` tracking this peek's operation
1355    span: tracing::Span,
1356}
1357
1358impl PersistPeek {
1359    async fn do_peek(
1360        persist_clients: &PersistClientCache,
1361        metadata: CollectionMetadata,
1362        as_of: Timestamp,
1363        literal_constraint: Option<Row>,
1364        mfp_plan: SafeMfpPlan,
1365        max_result_size: usize,
1366        mut limit_remaining: usize,
1367    ) -> Result<Vec<(Row, NonZeroUsize)>, String> {
1368        let client = persist_clients
1369            .open(metadata.persist_location)
1370            .await
1371            .map_err(|e| e.to_string())?;
1372
1373        let mut reader: ReadHandle<SourceData, (), Timestamp, StorageDiff> = client
1374            .open_leased_reader(
1375                metadata.data_shard,
1376                Arc::new(metadata.relation_desc.clone()),
1377                Arc::new(UnitSchema),
1378                Diagnostics::from_purpose("persist::peek"),
1379                USE_CRITICAL_SINCE_SNAPSHOT.get(client.dyncfgs()),
1380            )
1381            .await
1382            .map_err(|e| e.to_string())?;
1383
1384        // If we are using txn-wal for this collection, then the upper might
1385        // be advanced lazily and we have to go through txn-wal for reads.
1386        //
1387        // TODO: If/when we have a process-wide TxnsRead worker for clusterd,
1388        // use in here (instead of opening a new TxnsCache) to save a persist
1389        // reader registration and some txns shard read traffic.
1390        let mut txns_read = if let Some(txns_id) = metadata.txns_shard {
1391            Some(TxnsCache::open(&client, txns_id, Some(metadata.data_shard)).await)
1392        } else {
1393            None
1394        };
1395
1396        let metrics = client.metrics();
1397
1398        let mut cursor = StatsCursor::new(
1399            &mut reader,
1400            txns_read.as_mut(),
1401            metrics,
1402            &mfp_plan,
1403            &metadata.relation_desc,
1404            Antichain::from_elem(as_of),
1405        )
1406        .await
1407        .map_err(|since| {
1408            format!("attempted to peek at {as_of}, but the since has advanced to {since:?}")
1409        })?;
1410
1411        // Re-used state for processing and building rows.
1412        let mut result = vec![];
1413        let mut datum_vec = DatumVec::new();
1414        let mut row_builder = Row::default();
1415        let arena = RowArena::new();
1416        let mut total_size = 0usize;
1417
1418        let literal_len = match &literal_constraint {
1419            None => 0,
1420            Some(row) => row.iter().count(),
1421        };
1422
1423        'collect: while limit_remaining > 0 {
1424            let Some(batch) = cursor.next().await else {
1425                break;
1426            };
1427            for (data, _, d) in batch {
1428                let row = data.map_err(|e| e.to_string())?;
1429
1430                if let Some(literal) = &literal_constraint {
1431                    match row.iter().take(literal_len).cmp(literal.iter()) {
1432                        Ordering::Less => continue,
1433                        Ordering::Equal => {}
1434                        Ordering::Greater => break 'collect,
1435                    }
1436                }
1437
1438                let count: usize = d.try_into().map_err(|_| {
1439                    error!(
1440                        shard = %metadata.data_shard, diff = d, ?row,
1441                        "persist peek encountered negative multiplicities",
1442                    );
1443                    format!(
1444                        "Invalid data in source, \
1445                         saw retractions ({}) for row that does not exist: {:?}",
1446                        -d, row,
1447                    )
1448                })?;
1449                let Some(count) = NonZeroUsize::new(count) else {
1450                    continue;
1451                };
1452                let mut datum_local = datum_vec.borrow_with(&row);
1453                let eval_result = mfp_plan
1454                    .evaluate_into(&mut datum_local, &arena, &mut row_builder)
1455                    .map(|row| row.cloned())
1456                    .map_err(|e| e.to_string())?;
1457                if let Some(row) = eval_result {
1458                    total_size = total_size
1459                        .saturating_add(row.byte_len())
1460                        .saturating_add(std::mem::size_of::<NonZeroUsize>());
1461                    if total_size > max_result_size {
1462                        return Err(format!(
1463                            "result exceeds max size of {}",
1464                            ByteSize::b(u64::cast_from(max_result_size))
1465                        ));
1466                    }
1467                    result.push((row, count));
1468                    limit_remaining = limit_remaining.saturating_sub(count.get());
1469                    if limit_remaining == 0 {
1470                        break;
1471                    }
1472                }
1473            }
1474        }
1475
1476        Ok(result)
1477    }
1478}
1479
1480/// An in-progress index-backed peek, and data to eventually fulfill it.
1481pub struct IndexPeek {
1482    peek: Peek,
1483    /// The data from which the trace derives.
1484    trace_bundle: TraceBundle,
1485    /// The `tracing::Span` tracking this peek's operation
1486    span: tracing::Span,
1487}
1488
1489/// Histogram metrics for index peek phases.
1490///
1491/// This struct bundles references to the various histogram metrics used to
1492/// instrument the index peek processing pipeline.
1493pub(crate) struct IndexPeekMetrics<'a> {
1494    pub seek_fulfillment_seconds: &'a prometheus::Histogram,
1495    pub frontier_check_seconds: &'a prometheus::Histogram,
1496    pub error_scan_seconds: &'a prometheus::Histogram,
1497    pub cursor_setup_seconds: &'a prometheus::Histogram,
1498    pub row_iteration_seconds: &'a prometheus::Histogram,
1499    pub result_sort_seconds: &'a prometheus::Histogram,
1500    pub row_collection_seconds: &'a prometheus::Histogram,
1501}
1502
1503impl IndexPeek {
1504    /// Attempts to fulfill the peek and reports success.
1505    ///
1506    /// To produce output at `peek.timestamp`, we must be certain that
1507    /// it is no longer changing. A trace guarantees that all future
1508    /// changes will be greater than or equal to an element of `upper`.
1509    ///
1510    /// If an element of `upper` is less or equal to `peek.timestamp`,
1511    /// then there can be further updates that would change the output.
1512    /// If no element of `upper` is less or equal to `peek.timestamp`,
1513    /// then for any time `t` less or equal to `peek.timestamp` it is
1514    /// not the case that `upper` is less or equal to that timestamp,
1515    /// and so the result cannot further evolve.
1516    fn seek_fulfillment(
1517        &mut self,
1518        upper: &mut Antichain<Timestamp>,
1519        max_result_size: u64,
1520        peek_stash_eligible: bool,
1521        peek_stash_threshold_bytes: usize,
1522        metrics: &IndexPeekMetrics<'_>,
1523    ) -> PeekStatus {
1524        let method_start = Instant::now();
1525
1526        self.trace_bundle.oks_mut().read_upper(upper);
1527        if upper.less_equal(&self.peek.timestamp) {
1528            return PeekStatus::NotReady;
1529        }
1530        self.trace_bundle.errs_mut().read_upper(upper);
1531        if upper.less_equal(&self.peek.timestamp) {
1532            return PeekStatus::NotReady;
1533        }
1534
1535        let read_frontier = self.trace_bundle.compaction_frontier();
1536        if !read_frontier.less_equal(&self.peek.timestamp) {
1537            let error = format!(
1538                "Arrangement compaction frontier ({:?}) is beyond the time of the attempted read ({})",
1539                read_frontier.elements(),
1540                self.peek.timestamp,
1541            );
1542            return PeekStatus::Ready(PeekResponse::Error(error));
1543        }
1544
1545        metrics
1546            .frontier_check_seconds
1547            .observe(method_start.elapsed().as_secs_f64());
1548
1549        let result = self.collect_finished_data(
1550            max_result_size,
1551            peek_stash_eligible,
1552            peek_stash_threshold_bytes,
1553            metrics,
1554        );
1555
1556        metrics
1557            .seek_fulfillment_seconds
1558            .observe(method_start.elapsed().as_secs_f64());
1559
1560        result
1561    }
1562
1563    /// Collects data for a known-complete peek from the ok stream.
1564    fn collect_finished_data(
1565        &mut self,
1566        max_result_size: u64,
1567        peek_stash_eligible: bool,
1568        peek_stash_threshold_bytes: usize,
1569        metrics: &IndexPeekMetrics<'_>,
1570    ) -> PeekStatus {
1571        let error_scan_start = Instant::now();
1572
1573        // Check if there exist any errors and, if so, return whatever one we
1574        // find first.
1575        let (mut cursor, storage) = self.trace_bundle.errs_mut().cursor();
1576        while cursor.key_valid(&storage) {
1577            let mut copies = Diff::ZERO;
1578            cursor.map_times(&storage, |time, diff| {
1579                if time.less_equal(&self.peek.timestamp) {
1580                    copies += diff;
1581                }
1582            });
1583            if copies.is_negative() {
1584                let error = cursor.key(&storage);
1585                error!(
1586                    target = %self.peek.target.id(), diff = %copies, %error,
1587                    "index peek encountered negative multiplicities in error trace",
1588                );
1589                return PeekStatus::Ready(PeekResponse::Error(format!(
1590                    "Invalid data in source errors, \
1591                    saw retractions ({}) for row that does not exist: {}",
1592                    -copies, error,
1593                )));
1594            }
1595            if copies.is_positive() {
1596                return PeekStatus::Ready(PeekResponse::Error(cursor.key(&storage).to_string()));
1597            }
1598            cursor.step_key(&storage);
1599        }
1600
1601        metrics
1602            .error_scan_seconds
1603            .observe(error_scan_start.elapsed().as_secs_f64());
1604
1605        Self::collect_ok_finished_data(
1606            &self.peek,
1607            self.trace_bundle.oks_mut(),
1608            max_result_size,
1609            peek_stash_eligible,
1610            peek_stash_threshold_bytes,
1611            metrics,
1612        )
1613    }
1614
1615    /// Collects data for a known-complete peek from the ok stream.
1616    fn collect_ok_finished_data<Tr>(
1617        peek: &Peek,
1618        oks_handle: &mut Tr,
1619        max_result_size: u64,
1620        peek_stash_eligible: bool,
1621        peek_stash_threshold_bytes: usize,
1622        metrics: &IndexPeekMetrics<'_>,
1623    ) -> PeekStatus
1624    where
1625        for<'a> Tr: TraceReader<
1626                Key<'a>: ExtendDatums + Eq,
1627                KeyContainer: BatchContainer<Owned = Row>,
1628                Val<'a>: ExtendDatums,
1629                TimeGat<'a>: PartialOrder<Timestamp>,
1630                DiffGat<'a> = &'a Diff,
1631            >,
1632    {
1633        let max_result_size = usize::cast_from(max_result_size);
1634        let count_byte_size = size_of::<NonZeroUsize>();
1635
1636        // Cursor setup timing
1637        let cursor_setup_start = Instant::now();
1638
1639        // We clone `literal_constraints` here because we don't want to move the constraints
1640        // out of the peek struct, and don't want to modify in-place.
1641        let mut peek_iterator = peek_result_iterator::PeekResultIterator::new(
1642            peek.target.id().clone(),
1643            peek.map_filter_project.clone(),
1644            peek.timestamp,
1645            peek.literal_constraints.clone().as_deref_mut(),
1646            oks_handle,
1647        );
1648
1649        metrics
1650            .cursor_setup_seconds
1651            .observe(cursor_setup_start.elapsed().as_secs_f64());
1652
1653        // Accumulated `Vec<(row, count)>` results that we are likely to return.
1654        let mut results = Vec::new();
1655        let mut total_size: usize = 0;
1656
1657        // When set, a bound on the number of records we need to return.
1658        // The requirements on the records are driven by the finishing's
1659        // `order_by` field. Further limiting will happen when the results
1660        // are collected, so we don't need to have exactly this many results,
1661        // just at least those results that would have been returned.
1662        let max_results = peek.finishing.num_rows_needed();
1663
1664        let comparator = RowComparator::new(peek.finishing.order_by.as_slice());
1665
1666        // Row iteration timing
1667        let row_iteration_start = Instant::now();
1668        let mut sort_time_accum = Duration::ZERO;
1669
1670        while let Some(row) = peek_iterator.next() {
1671            let row: (Row, _) = match row {
1672                Ok(row) => row,
1673                Err(err) => return PeekStatus::Ready(PeekResponse::Error(err)),
1674            };
1675            let (row, copies) = row;
1676            let copies: NonZeroUsize = NonZeroUsize::try_from(copies).expect("fits into usize");
1677
1678            total_size = total_size
1679                .saturating_add(row.byte_len())
1680                .saturating_add(count_byte_size);
1681            if peek_stash_eligible && total_size > peek_stash_threshold_bytes {
1682                return PeekStatus::UsePeekStash;
1683            }
1684            if total_size > max_result_size {
1685                return PeekStatus::Ready(PeekResponse::Error(format!(
1686                    "result exceeds max size of {}",
1687                    ByteSize::b(u64::cast_from(max_result_size))
1688                )));
1689            }
1690
1691            results.push((row, copies));
1692
1693            // If we hold many more than `max_results` records, we can thin down
1694            // `results` using `self.finishing.ordering`.
1695            if let Some(max_results) = max_results {
1696                // We use a threshold twice what we intend, to amortize the work
1697                // across all of the insertions. We could tighten this, but it
1698                // works for the moment.
1699                if results.len() >= 2 * max_results {
1700                    if peek.finishing.order_by.is_empty() {
1701                        results.truncate(max_results);
1702                        metrics
1703                            .row_iteration_seconds
1704                            .observe(row_iteration_start.elapsed().as_secs_f64());
1705                        metrics
1706                            .result_sort_seconds
1707                            .observe(sort_time_accum.as_secs_f64());
1708                        let row_collection_start = Instant::now();
1709                        let collection = RowCollection::new(results, &peek.finishing.order_by);
1710                        metrics
1711                            .row_collection_seconds
1712                            .observe(row_collection_start.elapsed().as_secs_f64());
1713                        return PeekStatus::Ready(PeekResponse::Rows(vec![collection]));
1714                    } else {
1715                        // We can sort `results` and then truncate to `max_results`.
1716                        // This has an effect similar to a priority queue, without
1717                        // its interactive dequeueing properties.
1718                        // TODO: Had we left these as `Vec<Datum>` we would avoid
1719                        // the unpacking; we should consider doing that, although
1720                        // it will require a re-pivot of the code to branch on this
1721                        // inner test (as we prefer not to maintain `Vec<Datum>`
1722                        // in the other case).
1723                        let sort_start = Instant::now();
1724                        results.sort_by(|left, right| {
1725                            comparator.compare_rows(&left.0, &right.0, || left.0.cmp(&right.0))
1726                        });
1727                        sort_time_accum += sort_start.elapsed();
1728                        let dropped = results.drain(max_results..);
1729                        let dropped_size =
1730                            dropped
1731                                .into_iter()
1732                                .fold(0, |acc: usize, (row, _count): (Row, _)| {
1733                                    acc.saturating_add(
1734                                        row.byte_len().saturating_add(count_byte_size),
1735                                    )
1736                                });
1737                        total_size = total_size.saturating_sub(dropped_size);
1738                    }
1739                }
1740            }
1741        }
1742
1743        metrics
1744            .row_iteration_seconds
1745            .observe(row_iteration_start.elapsed().as_secs_f64());
1746        metrics
1747            .result_sort_seconds
1748            .observe(sort_time_accum.as_secs_f64());
1749
1750        let row_collection_start = Instant::now();
1751        let collection = RowCollection::new(results, &peek.finishing.order_by);
1752        metrics
1753            .row_collection_seconds
1754            .observe(row_collection_start.elapsed().as_secs_f64());
1755        PeekStatus::Ready(PeekResponse::Rows(vec![collection]))
1756    }
1757}
1758
1759/// For keeping track of the state of pending or ready peeks, and managing
1760/// control flow.
1761enum PeekStatus {
1762    /// The frontiers of objects are not yet advanced enough, peek is still
1763    /// pending.
1764    NotReady,
1765    /// The result size is above the configured threshold and the peek is
1766    /// eligible for using the peek result stash.
1767    UsePeekStash,
1768    /// The peek result is ready.
1769    Ready(PeekResponse),
1770}
1771
1772/// The frontiers we have reported to the controller for a collection.
1773#[derive(Debug)]
1774struct ReportedFrontiers {
1775    /// The reported write frontier.
1776    write_frontier: ReportedFrontier,
1777    /// The reported input frontier.
1778    input_frontier: ReportedFrontier,
1779    /// The reported output frontier.
1780    output_frontier: ReportedFrontier,
1781}
1782
1783impl ReportedFrontiers {
1784    /// Creates a new `ReportedFrontiers` instance.
1785    fn new() -> Self {
1786        Self {
1787            write_frontier: ReportedFrontier::new(),
1788            input_frontier: ReportedFrontier::new(),
1789            output_frontier: ReportedFrontier::new(),
1790        }
1791    }
1792}
1793
1794/// A frontier we have reported to the controller, or the least frontier we are allowed to report.
1795#[derive(Clone, Debug)]
1796pub enum ReportedFrontier {
1797    /// A frontier has been previously reported.
1798    Reported(Antichain<Timestamp>),
1799    /// No frontier has been reported yet.
1800    NotReported {
1801        /// A lower bound for frontiers that may be reported in the future.
1802        lower: Antichain<Timestamp>,
1803    },
1804}
1805
1806impl ReportedFrontier {
1807    /// Create a new `ReportedFrontier` enforcing the minimum lower bound.
1808    pub fn new() -> Self {
1809        let lower = Antichain::from_elem(timely::progress::Timestamp::minimum());
1810        Self::NotReported { lower }
1811    }
1812
1813    /// Whether the reported frontier is the empty frontier.
1814    pub fn is_empty(&self) -> bool {
1815        match self {
1816            Self::Reported(frontier) => frontier.is_empty(),
1817            Self::NotReported { .. } => false,
1818        }
1819    }
1820
1821    /// Whether this `ReportedFrontier` allows reporting the given frontier.
1822    ///
1823    /// A `ReportedFrontier` allows reporting of another frontier if:
1824    ///  * The other frontier is greater than the reported frontier.
1825    ///  * The other frontier is greater than or equal to the lower bound.
1826    fn allows_reporting(&self, other: &Antichain<Timestamp>) -> bool {
1827        match self {
1828            Self::Reported(frontier) => PartialOrder::less_than(frontier, other),
1829            Self::NotReported { lower } => PartialOrder::less_equal(lower, other),
1830        }
1831    }
1832}
1833
1834/// State maintained for a compute collection.
1835pub struct CollectionState {
1836    /// Tracks the frontiers that have been reported to the controller.
1837    reported_frontiers: ReportedFrontiers,
1838    /// The index of the dataflow computing this collection.
1839    ///
1840    /// Used for dropping the dataflow when the collection is dropped.
1841    /// The Dataflow index is wrapped in an `Rc`s and can be shared between collections, to reflect
1842    /// the possibility that a single dataflow can export multiple collections.
1843    dataflow_index: Rc<usize>,
1844    /// Whether this collection is a subscribe or copy-to.
1845    ///
1846    /// The compute protocol does not allow `Frontiers` responses for subscribe and copy-to
1847    /// collections, so we need to be able to recognize them. This is something we would like to
1848    /// change in the future (database-issues#4701).
1849    pub is_subscribe_or_copy: bool,
1850    /// The collection's initial as-of frontier.
1851    ///
1852    /// Used to determine hydration status.
1853    as_of: Antichain<Timestamp>,
1854
1855    /// A token that should be dropped when this collection is dropped to clean up associated
1856    /// sink state.
1857    ///
1858    /// Only `Some` if the collection is a sink.
1859    pub sink_token: Option<SinkToken>,
1860    /// Frontier of sink writes.
1861    ///
1862    /// Only `Some` if the collection is a sink and *not* a subscribe.
1863    pub sink_write_frontier: Option<Rc<RefCell<Antichain<Timestamp>>>>,
1864    /// Frontier probes for every input to the collection.
1865    pub input_probes: BTreeMap<GlobalId, probe::Handle<Timestamp>>,
1866    /// A probe reporting the frontier of times through which all collection outputs have been
1867    /// computed (but not necessarily written).
1868    ///
1869    /// `None` for collections with compute frontiers equal to their write frontiers.
1870    pub compute_probe: Option<probe::Handle<Timestamp>>,
1871    /// Logging state maintained for this collection.
1872    logging: Option<CollectionLogging>,
1873    /// Metrics tracked for this collection.
1874    metrics: CollectionMetrics,
1875    /// Send-side to transition a dataflow from read-only mode to read-write mode.
1876    ///
1877    /// All dataflows start in read-only mode. Only after receiving a
1878    /// `AllowWrites` command from the controller will they transition to
1879    /// read-write mode.
1880    ///
1881    /// A dataflow in read-only mode must not affect any external state.
1882    ///
1883    /// NOTE: In the future, we might want a more complicated flag, for example
1884    /// something that tells us after which timestamp we are allowed to write.
1885    /// In this first version we are keeping things as simple as possible!
1886    read_only_tx: watch::Sender<bool>,
1887    /// Receive-side to observe whether a dataflow is in read-only mode.
1888    pub read_only_rx: watch::Receiver<bool>,
1889}
1890
1891impl CollectionState {
1892    fn new(
1893        dataflow_index: Rc<usize>,
1894        is_subscribe_or_copy: bool,
1895        as_of: Antichain<Timestamp>,
1896        metrics: CollectionMetrics,
1897    ) -> Self {
1898        // We always initialize as read_only=true. Only when we're explicitly
1899        // allowed to we switch to read-write.
1900        let (read_only_tx, read_only_rx) = watch::channel(true);
1901
1902        Self {
1903            reported_frontiers: ReportedFrontiers::new(),
1904            dataflow_index,
1905            is_subscribe_or_copy,
1906            as_of,
1907            sink_token: None,
1908            sink_write_frontier: None,
1909            input_probes: Default::default(),
1910            compute_probe: None,
1911            logging: None,
1912            metrics,
1913            read_only_tx,
1914            read_only_rx,
1915        }
1916    }
1917
1918    /// Return the frontiers that have been reported to the controller.
1919    fn reported_frontiers(&self) -> &ReportedFrontiers {
1920        &self.reported_frontiers
1921    }
1922
1923    /// Reset all reported frontiers to the given value.
1924    pub fn reset_reported_frontiers(&mut self, frontier: ReportedFrontier) {
1925        self.reported_frontiers.write_frontier = frontier.clone();
1926        self.reported_frontiers.input_frontier = frontier.clone();
1927        self.reported_frontiers.output_frontier = frontier;
1928    }
1929
1930    /// Set the write frontier that has been reported to the controller.
1931    fn set_reported_write_frontier(&mut self, frontier: ReportedFrontier) {
1932        if let Some(logging) = &mut self.logging {
1933            let time = match &frontier {
1934                ReportedFrontier::Reported(frontier) => frontier.get(0).copied(),
1935                ReportedFrontier::NotReported { .. } => Some(Timestamp::MIN),
1936            };
1937            logging.set_frontier(time);
1938        }
1939
1940        self.reported_frontiers.write_frontier = frontier;
1941    }
1942
1943    /// Set the input frontier that has been reported to the controller.
1944    fn set_reported_input_frontier(&mut self, frontier: ReportedFrontier) {
1945        // Use this opportunity to update our input frontier logging.
1946        if let Some(logging) = &mut self.logging {
1947            for (id, probe) in &self.input_probes {
1948                let new_time = probe.with_frontier(|frontier| frontier.as_option().copied());
1949                logging.set_import_frontier(*id, new_time);
1950            }
1951        }
1952
1953        self.reported_frontiers.input_frontier = frontier;
1954    }
1955
1956    /// Set the output frontier that has been reported to the controller.
1957    fn set_reported_output_frontier(&mut self, frontier: ReportedFrontier) {
1958        let already_hydrated = self.hydrated();
1959
1960        self.reported_frontiers.output_frontier = frontier;
1961
1962        if !already_hydrated && self.hydrated() {
1963            if let Some(logging) = &mut self.logging {
1964                logging.set_hydrated();
1965            }
1966            self.metrics.record_collection_hydrated();
1967        }
1968    }
1969
1970    /// Return whether this collection is hydrated.
1971    fn hydrated(&self) -> bool {
1972        match &self.reported_frontiers.output_frontier {
1973            ReportedFrontier::Reported(frontier) => PartialOrder::less_than(&self.as_of, frontier),
1974            ReportedFrontier::NotReported { .. } => false,
1975        }
1976    }
1977
1978    /// Allow writes for this collection.
1979    fn allow_writes(&self) {
1980        info!(
1981            dataflow_index = *self.dataflow_index,
1982            export = ?self.logging.as_ref().map(|l| l.export_id()),
1983            "allowing writes for dataflow",
1984        );
1985        let _ = self.read_only_tx.send(false);
1986    }
1987}