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