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