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