Skip to main content

mz_compute/
server.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// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! An interactive dataflow server.
11
12use std::cell::RefCell;
13use std::collections::{BTreeMap, BTreeSet};
14use std::convert::Infallible;
15use std::fmt::Debug;
16use std::path::PathBuf;
17use std::rc::Rc;
18use std::sync::{Arc, Mutex};
19use std::time::{Duration, Instant};
20
21use anyhow::Error;
22use mz_cluster::client::{ClusterClient, ClusterSpec};
23use mz_cluster_client::client::TimelyConfig;
24use mz_compute_client::protocol::command::ComputeCommand;
25use mz_compute_client::protocol::history::ComputeCommandHistory;
26use mz_compute_client::protocol::response::ComputeResponse;
27use mz_compute_client::service::ComputeClient;
28use mz_ore::halt;
29use mz_ore::metrics::MetricsRegistry;
30use mz_ore::tracing::TracingHandle;
31use mz_persist_client::cache::PersistClientCache;
32use mz_storage_types::connections::ConnectionContext;
33use mz_timely_util::capture::EventLink;
34use mz_txn_wal::operator::TxnsContext;
35use timely::logging::TimelyEvent;
36use timely::progress::Antichain;
37use timely::worker::Worker as TimelyWorker;
38use tokio::sync::mpsc;
39use tokio::sync::mpsc::error::SendError;
40use tracing::{info, trace, warn};
41use uuid::Uuid;
42
43use crate::command_channel;
44use crate::compute_state::{
45    ActiveComputeState, ComputeState, PeekPermits, PendingPeek, ReportedFrontier,
46};
47use crate::metrics::{ComputeMetrics, WorkerMetrics};
48
49/// Caller-provided configuration for compute.
50#[derive(Clone, Debug)]
51pub struct ComputeInstanceContext {
52    /// A directory that can be used for scratch work.
53    pub scratch_directory: Option<PathBuf>,
54    /// Whether to set core affinity for Timely workers.
55    pub worker_core_affinity: bool,
56    /// Context required to connect to an external sink from compute,
57    /// like the `CopyToS3OneshotSink` compute sink.
58    pub connection_context: ConnectionContext,
59}
60
61/// Which of a process's compute runtimes a given runtime is.
62///
63/// A clusterd process runs a single `Solo` runtime by default. When an interactive runtime is
64/// configured, the process instead runs a `Maintenance` and an `Interactive` runtime side by side.
65/// The named roles share per-process resources (persist cache, metrics registry, log spans). The
66/// role distinguishes them so that only the globals-owning runtime runs the non-idempotent
67/// process-global initializers, and so metric series and log spans do not collide.
68///
69/// `Solo` exists so the single-runtime default stays behaviorally identical to a deployment without
70/// a second runtime: no `role` metric label, and it owns the process globals just as the sole
71/// runtime always has.
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum ComputeRuntimeRole {
74    /// The sole runtime of a single-runtime process. Owns index maintenance and the process-global
75    /// initializers.
76    Solo,
77    /// The maintenance runtime of a two-runtime process. Owns index maintenance and the
78    /// process-global initializers.
79    Maintenance,
80    /// The interactive runtime of a two-runtime process. Shares the process globals owned by
81    /// maintenance and serves reads.
82    ///
83    /// Test-only until the interactive runtime exists to construct it. It is present because the
84    /// `role` label's entire purpose is that two named roles register into one process registry
85    /// without colliding, and nothing else can express that: `Solo` registers the same metric names
86    /// with no `role` label, so prometheus rejects it alongside a named role for differing label
87    /// dimensions rather than treating it as a second series. Verifying non-collision therefore
88    /// needs a second *named* role.
89    ///
90    /// TODO: drop the `cfg` when the interactive runtime lands and constructs this.
91    #[cfg(test)]
92    Interactive,
93}
94
95impl ComputeRuntimeRole {
96    /// The `role` metric/log label for this role, or `None` for `Solo`.
97    ///
98    /// `Solo` omits the label so a single-runtime deployment registers exactly as it did before a
99    /// second runtime existed, keeping exact-match dashboards and alerts unchanged.
100    pub fn label(self) -> Option<&'static str> {
101        match self {
102            ComputeRuntimeRole::Solo => None,
103            ComputeRuntimeRole::Maintenance => Some("maintenance"),
104            #[cfg(test)]
105            ComputeRuntimeRole::Interactive => Some("interactive"),
106        }
107    }
108
109    /// Whether this role runs the non-idempotent, process-global initializers.
110    ///
111    /// `Solo` and `Maintenance` run them. An interactive runtime shares the same process and
112    /// inherits the globals maintenance installs, so re-running them would either double-apply a
113    /// non-idempotent effect or race maintenance.
114    ///
115    /// NOTE: every role a release build can construct owns the globals, so this is constantly true
116    /// outside tests. The distinction becomes load-bearing when the interactive runtime lands.
117    pub fn owns_process_globals(self) -> bool {
118        matches!(
119            self,
120            ComputeRuntimeRole::Solo | ComputeRuntimeRole::Maintenance
121        )
122    }
123}
124
125/// Type alias for the storage timely log reader.
126pub(crate) type StorageTimelyLogReader =
127    Arc<EventLink<mz_repr::Timestamp, Vec<(Duration, TimelyEvent)>>>;
128
129/// Configures the server with compute-specific metrics.
130#[derive(Clone)]
131struct Config {
132    /// `persist` client cache.
133    pub persist_clients: Arc<PersistClientCache>,
134    /// Context necessary for rendering txn-wal operators.
135    pub txns_ctx: TxnsContext,
136    /// A process-global handle to tracing configuration.
137    pub tracing_handle: Arc<TracingHandle>,
138    /// Metrics exposed by compute replicas.
139    pub metrics: ComputeMetrics,
140    /// Other configuration for compute.
141    pub context: ComputeInstanceContext,
142    /// The process-global metrics registry.
143    pub metrics_registry: MetricsRegistry,
144    /// The number of timely workers per process.
145    pub workers_per_process: usize,
146    /// Bounds how many offloaded peek walks run at once, shared by every worker this server runs.
147    pub peek_permits: Arc<PeekPermits>,
148    /// A reader for each storage worker in this process.
149    pub storage_log_readers: Arc<Mutex<Vec<Option<StorageTimelyLogReader>>>>,
150}
151
152/// Initiates a timely dataflow computation, processing compute commands.
153pub async fn serve(
154    timely_config: TimelyConfig,
155    role: ComputeRuntimeRole,
156    metrics_registry: &MetricsRegistry,
157    persist_clients: Arc<PersistClientCache>,
158    txns_ctx: TxnsContext,
159    tracing_handle: Arc<TracingHandle>,
160    context: ComputeInstanceContext,
161    storage_log_readers: Vec<StorageTimelyLogReader>,
162) -> Result<impl Fn() -> Box<dyn ComputeClient> + use<>, Error> {
163    let workers_per_process = timely_config.workers;
164    // Normalize the log-reader vec to exactly one slot per local worker. Empty
165    // input means logging is disabled; pad with `None` so index-based access is
166    // always in bounds.
167    let storage_log_readers = if storage_log_readers.is_empty() {
168        (0..workers_per_process).map(|_| None).collect()
169    } else {
170        assert_eq!(storage_log_readers.len(), workers_per_process);
171        storage_log_readers.into_iter().map(Some).collect()
172    };
173    mz_timely_util::column_pager::metrics::register(
174        metrics_registry,
175        mz_timely_util::column_pager::tiered_policy(),
176    );
177    mz_timely_util::pool_config::metrics::register(metrics_registry);
178
179    let config = Config {
180        persist_clients,
181        txns_ctx,
182        tracing_handle,
183        metrics: ComputeMetrics::register_with(metrics_registry, role),
184        context,
185        metrics_registry: metrics_registry.clone(),
186        workers_per_process,
187        // NOTE: per compute runtime, not global. A process running a maintenance and an
188        // interactive runtime calls `serve` twice and admits the bound once per call.
189        peek_permits: Arc::new(PeekPermits::new(workers_per_process)),
190        storage_log_readers: Arc::new(Mutex::new(storage_log_readers)),
191    };
192    let tokio_executor = tokio::runtime::Handle::current();
193
194    let timely_container = config.build_cluster(timely_config, tokio_executor).await?;
195    let timely_container = Arc::new(Mutex::new(timely_container));
196
197    let client_builder = move || {
198        let client = ClusterClient::new(Arc::clone(&timely_container));
199        let client: Box<dyn ComputeClient> = Box::new(client);
200        client
201    };
202
203    Ok(client_builder)
204}
205
206/// Error type returned on connection nonce changes.
207///
208/// A nonce change informs workers that subsequent commands come a from a new client connection
209/// and therefore require reconciliation.
210struct NonceChange(Uuid);
211
212/// Endpoint used by workers to receive compute commands.
213///
214/// Observes nonce changes in the command stream and converts them into receive errors.
215struct CommandReceiver {
216    /// The channel supplying commands.
217    inner: command_channel::Receiver,
218    /// The ID of the Timely worker.
219    worker_id: usize,
220    /// The nonce identifying the current cluster protocol incarnation.
221    nonce: Option<Uuid>,
222    /// A stash to enable peeking the next command, used in `try_recv`.
223    stashed_command: Option<ComputeCommand>,
224}
225
226impl CommandReceiver {
227    fn new(inner: command_channel::Receiver, worker_id: usize) -> Self {
228        Self {
229            inner,
230            worker_id,
231            nonce: None,
232            stashed_command: None,
233        }
234    }
235
236    /// Receive the next pending command, if any.
237    ///
238    /// If the next command has a different nonce, this method instead returns an `Err`
239    /// containing the new nonce.
240    fn try_recv(&mut self) -> Result<Option<ComputeCommand>, NonceChange> {
241        if let Some(command) = self.stashed_command.take() {
242            return Ok(Some(command));
243        }
244        let Some((command, nonce)) = self.inner.try_recv() else {
245            return Ok(None);
246        };
247
248        trace!(worker = self.worker_id, %nonce, ?command, "received command");
249
250        if Some(nonce) == self.nonce {
251            Ok(Some(command))
252        } else {
253            self.nonce = Some(nonce);
254            self.stashed_command = Some(command);
255            Err(NonceChange(nonce))
256        }
257    }
258}
259
260/// Endpoint used by workers to send sending compute responses.
261///
262/// Tags responses with the current nonce, allowing receivers to filter out responses intended for
263/// previous client connections.
264pub(crate) struct ResponseSender {
265    /// The channel consuming responses.
266    inner: mpsc::UnboundedSender<(ComputeResponse, Uuid)>,
267    /// The ID of the Timely worker.
268    worker_id: usize,
269    /// The nonce identifying the current cluster protocol incarnation.
270    nonce: Option<Uuid>,
271}
272
273impl ResponseSender {
274    /// `pub(crate)` rather than private so the peek tests can build the sender a worker holds.
275    pub(crate) fn new(
276        inner: mpsc::UnboundedSender<(ComputeResponse, Uuid)>,
277        worker_id: usize,
278    ) -> Self {
279        Self {
280            inner,
281            worker_id,
282            nonce: None,
283        }
284    }
285
286    /// Set the cluster protocol nonce.
287    pub(crate) fn set_nonce(&mut self, nonce: Uuid) {
288        self.nonce = Some(nonce);
289    }
290
291    /// Send a compute response.
292    pub fn send(&self, response: ComputeResponse) -> Result<(), SendError<ComputeResponse>> {
293        let nonce = self.nonce.expect("nonce must be initialized");
294
295        trace!(worker = self.worker_id, %nonce, ?response, "sending response");
296        self.inner
297            .send((response, nonce))
298            .map_err(|SendError((resp, _))| SendError(resp))
299    }
300}
301
302/// State maintained for each worker thread.
303///
304/// Much of this state can be viewed as local variables for the worker thread,
305/// holding state that persists across function calls.
306struct Worker<'w> {
307    /// The underlying Timely worker.
308    timely_worker: &'w mut TimelyWorker,
309    /// The channel over which commands are received.
310    command_rx: CommandReceiver,
311    /// The channel over which responses are sent.
312    response_tx: ResponseSender,
313    compute_state: Option<ComputeState>,
314    /// Compute metrics.
315    metrics: WorkerMetrics,
316    /// A process-global cache of (blob_uri, consensus_uri) -> PersistClient.
317    /// This is intentionally shared between workers
318    persist_clients: Arc<PersistClientCache>,
319    /// Context necessary for rendering txn-wal operators.
320    txns_ctx: TxnsContext,
321    /// A process-global handle to tracing configuration.
322    tracing_handle: Arc<TracingHandle>,
323    context: ComputeInstanceContext,
324    /// The process-global metrics registry.
325    metrics_registry: MetricsRegistry,
326    /// The number of timely workers per process.
327    workers_per_process: usize,
328    /// Bounds how many offloaded peek walks run at once, shared by the workers of one `serve`
329    /// call rather than by the process.
330    peek_permits: Arc<PeekPermits>,
331    /// Reader for storage timely logging events.
332    storage_log_reader: Option<StorageTimelyLogReader>,
333}
334
335impl ClusterSpec for Config {
336    type Command = ComputeCommand;
337    type Response = ComputeResponse;
338
339    const NAME: &str = "compute";
340
341    fn run_worker(
342        &self,
343        timely_worker: &mut TimelyWorker,
344        client_rx: mpsc::UnboundedReceiver<(
345            Uuid,
346            mpsc::UnboundedReceiver<ComputeCommand>,
347            mpsc::UnboundedSender<ComputeResponse>,
348        )>,
349    ) {
350        if self.context.worker_core_affinity {
351            set_core_affinity(timely_worker.index());
352        }
353
354        let worker_id = timely_worker.index();
355        let metrics = self.metrics.for_worker(worker_id);
356
357        // Take this worker's storage log reader, indexed by local worker index
358        // so compute worker x matches storage worker x.
359        let local_index = worker_id % self.workers_per_process;
360        let storage_log_reader = self.storage_log_readers.lock().unwrap()[local_index].take();
361
362        // Create the command channel that broadcasts commands from worker 0 to other workers. We
363        // reuse this channel between client connections, to avoid bugs where different workers end
364        // up creating incompatible sides of the channel dataflow after reconnects.
365        // See database-issues#8964.
366        let (cmd_tx, cmd_rx) = command_channel::render(timely_worker);
367        let (resp_tx, resp_rx) = mpsc::unbounded_channel();
368
369        spawn_channel_adapter(client_rx, cmd_tx, resp_rx, worker_id);
370
371        Worker {
372            timely_worker,
373            command_rx: CommandReceiver::new(cmd_rx, worker_id),
374            response_tx: ResponseSender::new(resp_tx, worker_id),
375            metrics,
376            context: self.context.clone(),
377            persist_clients: Arc::clone(&self.persist_clients),
378            txns_ctx: self.txns_ctx.clone(),
379            compute_state: None,
380            tracing_handle: Arc::clone(&self.tracing_handle),
381            metrics_registry: self.metrics_registry.clone(),
382            workers_per_process: self.workers_per_process,
383            peek_permits: Arc::clone(&self.peek_permits),
384            storage_log_reader,
385        }
386        .run()
387    }
388}
389
390/// Set the current thread's core affinity, based on the given `worker_id`.
391#[cfg(not(target_os = "macos"))]
392fn set_core_affinity(worker_id: usize) {
393    use tracing::error;
394
395    let Some(mut core_ids) = core_affinity::get_core_ids() else {
396        error!(worker_id, "unable to get core IDs for setting affinity");
397        return;
398    };
399
400    // The `get_core_ids` docs don't say anything about a guaranteed order of the returned Vec,
401    // so sort it just to be safe.
402    core_ids.sort_unstable_by_key(|i| i.id);
403
404    // On multi-process replicas `worker_id` might be greater than the number of available cores.
405    // However, we assume that we always have at least as many cores as there are local workers.
406    // Violating this assumption is safe but might lead to degraded performance due to skew in core
407    // utilization.
408    let idx = worker_id % core_ids.len();
409    let core_id = core_ids[idx];
410
411    if core_affinity::set_for_current(core_id) {
412        info!(
413            worker_id,
414            core_id = core_id.id,
415            "set core affinity for worker"
416        );
417    } else {
418        error!(
419            worker_id,
420            core_id = core_id.id,
421            "failed to set core affinity for worker"
422        )
423    }
424}
425
426/// Set the current thread's core affinity, based on the given `worker_id`.
427#[cfg(target_os = "macos")]
428fn set_core_affinity(_worker_id: usize) {
429    // Setting core affinity is known to not work on Apple Silicon:
430    // https://github.com/Elzair/core_affinity_rs/issues/22
431    info!("setting core affinity is not supported on macOS");
432}
433
434impl<'w> Worker<'w> {
435    /// Runs a compute worker.
436    pub fn run(&mut self) {
437        // The command receiver is initialized without an nonce, so receiving the first command
438        // always triggers a nonce change.
439        let NonceChange(nonce) = self.recv_command().expect_err("change to first nonce");
440        self.set_nonce(nonce);
441
442        loop {
443            let Err(NonceChange(nonce)) = self.run_client();
444            self.set_nonce(nonce);
445        }
446    }
447
448    fn set_nonce(&mut self, nonce: Uuid) {
449        self.response_tx.set_nonce(nonce);
450    }
451
452    /// Handles commands for a client connection, returns when the nonce changes.
453    fn run_client(&mut self) -> Result<Infallible, NonceChange> {
454        self.reconcile()?;
455
456        // The last time we did periodic maintenance.
457        let mut last_maintenance = Instant::now();
458
459        // Commence normal operation.
460        loop {
461            // Get the maintenance interval, default to zero if we don't have a compute state.
462            let maintenance_interval = self
463                .compute_state
464                .as_ref()
465                .map_or(Duration::ZERO, |state| state.server_maintenance_interval);
466
467            let now = Instant::now();
468            // Determine if we need to perform maintenance, which is true if `maintenance_interval`
469            // time has passed since the last maintenance.
470            let sleep_duration;
471            if now >= last_maintenance + maintenance_interval {
472                last_maintenance = now;
473                sleep_duration = None;
474
475                // Report frontier information back the coordinator.
476                if let Some(mut compute_state) = self.activate_compute() {
477                    compute_state.compute_state.traces.maintenance();
478                    compute_state.report_frontiers();
479                    compute_state.report_metrics();
480                    compute_state.check_expiration();
481                }
482
483                self.metrics.record_shared_row_metrics();
484            } else {
485                // We didn't perform maintenance, sleep until the next maintenance interval.
486                let next_maintenance = last_maintenance + maintenance_interval;
487                sleep_duration = Some(next_maintenance.saturating_duration_since(now))
488            };
489
490            // Do not sleep while a peek waits for its turn. Only the sweep below gives it one,
491            // and nothing else leaves an activation behind to end the park.
492            let sleep_duration = match &self.compute_state {
493                Some(state) if state.peeks_awaiting_turn() => Some(Duration::ZERO),
494                _ => sleep_duration,
495            };
496
497            // Step the timely worker, recording the time taken.
498            let timer = self.metrics.timely_step_duration_seconds.start_timer();
499            self.timely_worker.step_or_park(sleep_duration);
500            timer.observe_duration();
501
502            self.handle_pending_commands()?;
503
504            if let Some(mut compute_state) = self.activate_compute() {
505                compute_state.process_peeks();
506                compute_state.process_subscribes();
507                compute_state.process_copy_tos();
508            }
509        }
510    }
511
512    fn handle_pending_commands(&mut self) -> Result<(), NonceChange> {
513        while let Some(cmd) = self.command_rx.try_recv()? {
514            self.handle_command(cmd);
515        }
516        Ok(())
517    }
518
519    fn handle_command(&mut self, cmd: ComputeCommand) {
520        if matches!(&cmd, ComputeCommand::CreateInstance(_)) {
521            self.compute_state = Some(ComputeState::new(
522                Arc::clone(&self.persist_clients),
523                self.txns_ctx.clone(),
524                self.metrics.clone(),
525                Arc::clone(&self.tracing_handle),
526                self.context.clone(),
527                self.metrics_registry.clone(),
528                self.workers_per_process,
529                Arc::clone(&self.peek_permits),
530                self.storage_log_reader.take(),
531            ));
532        }
533        self.activate_compute().unwrap().handle_compute_command(cmd);
534    }
535
536    fn activate_compute(&mut self) -> Option<ActiveComputeState<'_>> {
537        if let Some(compute_state) = &mut self.compute_state {
538            Some(ActiveComputeState {
539                timely_worker: &mut *self.timely_worker,
540                compute_state,
541                response_tx: &mut self.response_tx,
542            })
543        } else {
544            None
545        }
546    }
547
548    /// Receive the next compute command.
549    ///
550    /// This method blocks if no command is currently available, but takes care to step the Timely
551    /// worker while doing so.
552    fn recv_command(&mut self) -> Result<ComputeCommand, NonceChange> {
553        loop {
554            if let Some(cmd) = self.command_rx.try_recv()? {
555                return Ok(cmd);
556            }
557
558            let start = Instant::now();
559            self.timely_worker.step_or_park(None);
560            self.metrics
561                .timely_step_duration_seconds
562                .observe(start.elapsed().as_secs_f64());
563        }
564    }
565
566    /// Extract commands until `InitializationComplete`, and make the worker reflect those commands.
567    ///
568    /// This method is meant to be a function of the commands received thus far (as recorded in the
569    /// compute state command history) and the new commands from `command_rx`. It should not be a
570    /// function of other characteristics, like whether the worker has managed to respond to a peek
571    /// or not. Some effort goes in to narrowing our view to only the existing commands we can be sure
572    /// are live at all other workers.
573    ///
574    /// The methodology here is to drain `command_rx` until an `InitializationComplete`, at which point
575    /// the prior commands are "reconciled" in. Reconciliation takes each goal dataflow and looks for an
576    /// existing "compatible" dataflow (per `compatible()`) it can repurpose, with some additional tests
577    /// to be sure that we can cut over from one to the other (no additional compaction, no tails/sinks).
578    /// With any connections established, old orphaned dataflows are allow to compact away, and any new
579    /// dataflows are created from scratch. "Kept" dataflows are allowed to compact up to any new `as_of`.
580    ///
581    /// Some additional tidying happens, cleaning up pending peeks, reported frontiers, and creating a new
582    /// subscribe response buffer. We will need to be vigilant with future modifications to `ComputeState` to
583    /// line up changes there with clean resets here.
584    fn reconcile(&mut self) -> Result<(), NonceChange> {
585        // To initialize the connection, we want to drain all commands until we receive a
586        // `ComputeCommand::InitializationComplete` command to form a target command state.
587        let mut new_commands = Vec::new();
588        loop {
589            match self.recv_command()? {
590                ComputeCommand::InitializationComplete => break,
591                command => new_commands.push(command),
592            }
593        }
594
595        // Commands we will need to apply before entering normal service.
596        // These commands may include dropping existing dataflows, compacting existing dataflows,
597        // and creating new dataflows, in addition to standard peek and compaction commands.
598        // The result should be the same as if dropping all dataflows and running `new_commands`.
599        let mut todo_commands = Vec::new();
600        // We only have a compute history if we are in an initialized state
601        // (i.e. after a `CreateInstance`).
602        // If this is not the case, just copy `new_commands` into `todo_commands`.
603        if let Some(compute_state) = &mut self.compute_state {
604            // Reduce the installed commands.
605            // Importantly, act as if all peeks may have been retired (as we cannot know otherwise).
606            compute_state.command_history.discard_peeks();
607            compute_state.command_history.reduce();
608
609            // At this point, we need to sort out which of the *certainly installed* dataflows are
610            // suitable replacements for the requested dataflows. A dataflow is "certainly installed"
611            // as of a frontier if its compaction allows it to go no further. We ignore peeks for this
612            // reasoning, as we cannot be certain that peeks still exist at any other worker.
613
614            // Having reduced our installed command history retaining no peeks (above), we should be able
615            // to use track down installed dataflows we can use as surrogates for requested dataflows (which
616            // have retained all of their peeks, creating a more demanding `as_of` requirement).
617            // NB: installed dataflows may still be allowed to further compact, and we should double check
618            // this before being too confident. It should be rare without peeks, but could happen with e.g.
619            // multiple outputs of a dataflow.
620
621            // The values with which a prior `CreateInstance` was called, if it was.
622            let mut old_instance_config = None;
623            // Index dataflows by `export_ids().collect()`, as this is a precondition for their compatibility.
624            let mut old_dataflows = BTreeMap::default();
625            // Maintain allowed compaction, in case installed identifiers may have been allowed to compact.
626            let mut old_frontiers = BTreeMap::default();
627            for command in compute_state.command_history.iter() {
628                match command {
629                    ComputeCommand::CreateInstance(config) => {
630                        old_instance_config = Some(config);
631                    }
632                    ComputeCommand::CreateDataflow(dataflow) => {
633                        let export_ids = dataflow.export_ids().collect::<BTreeSet<_>>();
634                        old_dataflows.insert(export_ids, dataflow);
635                    }
636                    ComputeCommand::AllowCompaction { id, frontier } => {
637                        old_frontiers.insert(id, frontier);
638                    }
639                    _ => {
640                        // Nothing to do in these cases.
641                    }
642                }
643            }
644
645            // Compaction commands that can be applied to existing dataflows.
646            let mut old_compaction = BTreeMap::default();
647            // Exported identifiers from dataflows we retain.
648            let mut retain_ids = BTreeSet::default();
649
650            // Traverse new commands, sorting out what remediation we can do.
651            for command in new_commands.iter() {
652                match command {
653                    ComputeCommand::CreateDataflow(dataflow) => {
654                        // Attempt to find an existing match for the dataflow.
655                        let as_of = dataflow.as_of.as_ref().unwrap();
656                        let export_ids = dataflow.export_ids().collect::<BTreeSet<_>>();
657
658                        if let Some(old_dataflow) = old_dataflows.get(&export_ids) {
659                            let compatible = old_dataflow.compatible_with(dataflow);
660                            let uncompacted = !export_ids
661                                .iter()
662                                .flat_map(|id| old_frontiers.get(id))
663                                .any(|frontier| {
664                                    !timely::PartialOrder::less_equal(
665                                        *frontier,
666                                        dataflow.as_of.as_ref().unwrap(),
667                                    )
668                                });
669
670                            // We cannot reconcile subscribe and copy-to sinks at the moment,
671                            // because the response buffer is shared, and to a first approximation
672                            // must be completely reformed.
673                            let subscribe_free = dataflow.subscribe_ids().next().is_none();
674                            let copy_to_free = dataflow.copy_to_ids().next().is_none();
675
676                            // If we have replaced any dependency of this dataflow, we need to
677                            // replace this dataflow, to make it use the replacement.
678                            let dependencies_retained = dataflow
679                                .imported_index_ids()
680                                .all(|id| retain_ids.contains(&id));
681
682                            if compatible
683                                && uncompacted
684                                && subscribe_free
685                                && copy_to_free
686                                && dependencies_retained
687                            {
688                                // Match found; remove the match from the deletion queue,
689                                // and compact its outputs to the dataflow's `as_of`.
690                                old_dataflows.remove(&export_ids);
691                                for id in export_ids.iter() {
692                                    old_compaction.insert(*id, as_of.clone());
693                                }
694                                retain_ids.extend(export_ids);
695                            } else {
696                                warn!(
697                                    ?export_ids,
698                                    ?compatible,
699                                    ?uncompacted,
700                                    ?subscribe_free,
701                                    ?copy_to_free,
702                                    ?dependencies_retained,
703                                    old_as_of = ?old_dataflow.as_of,
704                                    new_as_of = ?as_of,
705                                    "dataflow reconciliation failed",
706                                );
707
708                                // Dump the full dataflow plans if they are incompatible, to
709                                // simplify debugging hard-to-reproduce reconciliation failures.
710                                if !compatible {
711                                    warn!(
712                                        old = ?old_dataflow,
713                                        new = ?dataflow,
714                                        "incompatible dataflows in reconciliation",
715                                    );
716                                }
717
718                                todo_commands
719                                    .push(ComputeCommand::CreateDataflow(dataflow.clone()));
720                            }
721
722                            compute_state.metrics.record_dataflow_reconciliation(
723                                compatible,
724                                uncompacted,
725                                subscribe_free,
726                                copy_to_free,
727                                dependencies_retained,
728                            );
729                        } else {
730                            todo_commands.push(ComputeCommand::CreateDataflow(dataflow.clone()));
731                        }
732                    }
733                    ComputeCommand::CreateInstance(config) => {
734                        // Cluster creation should not be performed again!
735                        if old_instance_config.map_or(false, |old| !old.compatible_with(config)) {
736                            halt!(
737                                "new instance configuration not compatible with existing instance configuration:\n{:?}\nvs\n{:?}",
738                                config,
739                                old_instance_config,
740                            );
741                        }
742                    }
743                    // All other commands we apply as requested.
744                    command => {
745                        todo_commands.push(command.clone());
746                    }
747                }
748            }
749
750            // Issue compaction commands first to reclaim resources.
751            for (_, dataflow) in old_dataflows.iter() {
752                for id in dataflow.export_ids() {
753                    // We want to drop anything that has not yet been dropped,
754                    // and nothing that has already been dropped.
755                    if old_frontiers.get(&id) != Some(&&Antichain::new()) {
756                        old_compaction.insert(id, Antichain::new());
757                    }
758                }
759            }
760            for (&id, frontier) in &old_compaction {
761                let frontier = frontier.clone();
762                todo_commands.insert(0, ComputeCommand::AllowCompaction { id, frontier });
763            }
764
765            // Clean up worker-local state.
766            //
767            // Various aspects of `ComputeState` need to be either uninstalled, or return to a blank slate.
768            // All dropped dataflows should clean up after themselves, as we plan to install new dataflows
769            // re-using the same identifiers.
770            // All re-used dataflows should roll back any believed communicated information (e.g. frontiers)
771            // so that they recommunicate that information as if from scratch.
772
773            // Remove all peeks, whether they have started or are still awaiting a turn.
774            let queued = std::mem::take(&mut compute_state.queued_peeks);
775            let pending = std::mem::take(&mut compute_state.pending_peeks);
776            for peek in queued.into_iter().map(PendingPeek::Index).chain(pending) {
777                // Log dropping the peek request.
778                if let Some(logger) = compute_state.compute_logger.as_mut() {
779                    logger.log(&peek.as_log_event(false));
780                }
781            }
782
783            for (&id, collection) in compute_state.collections.iter_mut() {
784                // Adjust reported frontiers:
785                //  * For dataflows we continue to use, reset to ensure we report something not
786                //    before the new `as_of` next.
787                //  * For dataflows we drop, set to the empty frontier, to ensure we don't report
788                //    anything for them.
789                let retained = retain_ids.contains(&id);
790                let compaction = old_compaction.remove(&id);
791                let new_reported_frontier = match (retained, compaction) {
792                    (true, Some(new_as_of)) => ReportedFrontier::NotReported { lower: new_as_of },
793                    (true, None) => {
794                        unreachable!("retained dataflows are compacted to the new as_of")
795                    }
796                    (false, Some(new_frontier)) => {
797                        assert!(new_frontier.is_empty());
798                        ReportedFrontier::Reported(new_frontier)
799                    }
800                    (false, None) => {
801                        // Logging dataflows are implicitly retained and don't have a new as_of.
802                        // Reset them to the minimal frontier.
803                        ReportedFrontier::new()
804                    }
805                };
806
807                collection.reset_reported_frontiers(new_reported_frontier);
808
809                // Sink tokens should be retained for retained dataflows, and dropped for dropped
810                // dataflows.
811                //
812                // Dropping the tokens of active subscribe and copy-tos makes them place
813                // `DroppedAt` responses into the respective response buffer. We drop those buffers
814                // in the next step, which ensures that we don't send out `DroppedAt` responses for
815                // subscribe/copy-tos dropped during reconciliation.
816                if !retained {
817                    collection.sink_token = None;
818                }
819            }
820
821            // We must drop the response buffers as they are global across all subscribe/copy-tos.
822            // If they were broken out by `GlobalId` then we could drop only the response buffers
823            // of dataflows we drop.
824            compute_state.subscribe_response_buffer = Rc::new(RefCell::new(Vec::new()));
825            compute_state.copy_to_response_buffer = Rc::new(RefCell::new(Vec::new()));
826
827            // The controller expects the logging collections to be readable from the minimum time
828            // initially. We cannot recreate the logging arrangements without restarting the
829            // instance, but we can pad the compacted times with empty data. Doing so is sound
830            // because logging collections from different replica incarnations are considered
831            // distinct TVCs, so the controller doesn't expect any historical consistency from
832            // these collections when it reconnects to a replica.
833            //
834            // TODO(database-issues#8152): Consider resolving this with controller-side reconciliation instead.
835            if let Some(config) = old_instance_config {
836                for id in config.logging.index_logs.values() {
837                    let trace = compute_state
838                        .traces
839                        .remove(id)
840                        .expect("logging trace exists");
841                    let padded = trace.into_padded();
842                    compute_state.traces.set(*id, padded);
843                }
844            }
845        } else {
846            todo_commands.clone_from(&new_commands);
847        }
848
849        // Execute the commands to bring us to `new_commands`.
850        for command in todo_commands.into_iter() {
851            self.handle_command(command);
852        }
853
854        // Overwrite `self.command_history` to reflect `new_commands`.
855        // It is possible that there still isn't a compute state yet.
856        if let Some(compute_state) = &mut self.compute_state {
857            let mut command_history = ComputeCommandHistory::new(self.metrics.for_history());
858            for command in new_commands.iter() {
859                command_history.push(command.clone());
860            }
861            compute_state.command_history = command_history;
862        }
863        Ok(())
864    }
865}
866
867/// Spawn a task to bridge between [`ClusterClient`] and [`Worker`] channels.
868///
869/// The [`Worker`] expects a pair of persistent channels, with punctuation marking reconnects,
870/// while the [`ClusterClient`] provides a new pair of channels on each reconnect.
871fn spawn_channel_adapter(
872    mut client_rx: mpsc::UnboundedReceiver<(
873        Uuid,
874        mpsc::UnboundedReceiver<ComputeCommand>,
875        mpsc::UnboundedSender<ComputeResponse>,
876    )>,
877    command_tx: command_channel::Sender,
878    mut response_rx: mpsc::UnboundedReceiver<(ComputeResponse, Uuid)>,
879    worker_id: usize,
880) {
881    mz_ore::task::spawn(
882        || format!("compute-channel-adapter-{worker_id}"),
883        async move {
884            // To make workers aware of the individual client connections, we tag forwarded
885            // commands with the client nonce. Additionally, we use the nonce to filter out
886            // responses with a different nonce, which are intended for different client
887            // connections.
888            //
889            // It's possible that we receive responses with nonces from the past but also from the
890            // future: Worker 0 might have received a new nonce before us and broadcasted it to our
891            // Timely cluster. When we receive a response with a future nonce, we need to wait with
892            // forwarding it until we have received the same nonce from a client connection.
893            //
894            // Nonces are not ordered so we don't know whether a response nonce is from the past or
895            // the future. We thus assume that every response with an unknown nonce might be from
896            // the future and stash them all. Every time we reconnect, we immediately send all
897            // stashed responses with a matching nonce. Every time we receive a new response with a
898            // nonce that matches our current one, we can discard the entire response stash as we
899            // know that all stashed responses must be from the past.
900            let mut stashed_responses = BTreeMap::<Uuid, Vec<ComputeResponse>>::new();
901
902            while let Some((nonce, mut command_rx, response_tx)) = client_rx.recv().await {
903                // Send stashed responses for this client.
904                if let Some(resps) = stashed_responses.remove(&nonce) {
905                    for resp in resps {
906                        let _ = response_tx.send(resp);
907                    }
908                }
909
910                // Wait for a new response while forwarding received commands.
911                let mut serve_rx_channels = async || loop {
912                    tokio::select! {
913                        msg = command_rx.recv() => match msg {
914                            Some(cmd) => command_tx.send((cmd, nonce)),
915                            None => return Err(()),
916                        },
917                        msg = response_rx.recv() => {
918                            return Ok(msg.expect("worker connected"));
919                        }
920                    }
921                };
922
923                // Serve this connection until we see any of the channels disconnect.
924                loop {
925                    let Ok((resp, resp_nonce)) = serve_rx_channels().await else {
926                        break;
927                    };
928
929                    if resp_nonce == nonce {
930                        // Response for the current connection; forward it.
931                        stashed_responses.clear();
932                        if response_tx.send(resp).is_err() {
933                            break;
934                        }
935                    } else {
936                        // Response for a past or future connection; stash it.
937                        let stash = stashed_responses.entry(resp_nonce).or_default();
938                        stash.push(resp);
939                    }
940                }
941            }
942        },
943    );
944}