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