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::thread::Thread;
20use std::time::{Duration, Instant};
21
22use anyhow::Error;
23use mz_cluster::client::{ClusterClient, ClusterSpec, GuestClusterClient};
24use mz_cluster_client::client::TimelyConfig;
25use mz_compute_client::protocol::command::ComputeCommand;
26use mz_compute_client::protocol::history::ComputeCommandHistory;
27use mz_compute_client::protocol::response::ComputeResponse;
28use mz_compute_client::service::ComputeClient;
29use mz_ore::halt;
30use mz_ore::metrics::MetricsRegistry;
31use mz_ore::now::NowFn;
32use mz_ore::tracing::TracingHandle;
33use mz_persist_client::cache::PersistClientCache;
34use mz_rocksdb::config::SharedWriteBufferManager;
35use mz_storage::internal_control::{InternalCommandSender, InternalStorageCommand};
36use mz_storage::metrics::StorageMetrics;
37use mz_storage::storage_state::{StorageInstanceContext, StorageState, Worker as StorageWorker};
38use mz_storage_client::client::{StorageClient, StorageCommand, StorageResponse};
39use mz_storage_operators::persist::SharedBatches;
40use mz_storage_types::connections::ConnectionContext;
41use mz_txn_wal::operator::TxnsContext;
42use timely::progress::Antichain;
43use timely::worker::Worker as TimelyWorker;
44use tokio::sync::mpsc;
45use tokio::sync::mpsc::error::SendError;
46use tracing::{info, trace, warn};
47use uuid::Uuid;
48
49use crate::command_channel::{self, StorageLaneInput, UnifiedCommand};
50use crate::compute_state::{
51    ActiveComputeState, ComputeState, PeekPermits, PendingPeek, ReportedFrontier,
52};
53use crate::metrics::{ComputeMetrics, WorkerMetrics};
54
55/// Caller-provided configuration for compute.
56#[derive(Clone, Debug)]
57pub struct ComputeInstanceContext {
58    /// A directory that can be used for scratch work.
59    pub scratch_directory: Option<PathBuf>,
60    /// Whether to set core affinity for Timely workers.
61    pub worker_core_affinity: bool,
62    /// Context required to connect to an external sink from compute,
63    /// like the `CopyToS3OneshotSink` compute sink.
64    pub connection_context: ConnectionContext,
65}
66
67/// Which of a process's compute runtimes a given runtime is.
68///
69/// A clusterd process runs a single `Solo` runtime by default. When an interactive runtime is
70/// configured, the process instead runs a `Maintenance` and an `Interactive` runtime side by side.
71/// The named roles share per-process resources (persist cache, metrics registry, log spans). The
72/// role distinguishes them so that only the globals-owning runtime runs the non-idempotent
73/// process-global initializers, and so metric series and log spans do not collide.
74///
75/// `Solo` exists so the single-runtime default stays behaviorally identical to a deployment without
76/// a second runtime: no `role` metric label, and it owns the process globals just as the sole
77/// runtime always has.
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum ComputeRuntimeRole {
80    /// The sole runtime of a single-runtime process. Owns index maintenance and the process-global
81    /// initializers.
82    Solo,
83    /// The maintenance runtime of a two-runtime process. Owns index maintenance and the
84    /// process-global initializers.
85    Maintenance,
86    /// The interactive runtime of a two-runtime process. Shares the process globals owned by
87    /// maintenance and serves reads.
88    ///
89    /// Test-only until the interactive runtime exists to construct it. It is present because the
90    /// `role` label's entire purpose is that two named roles register into one process registry
91    /// without colliding, and nothing else can express that: `Solo` registers the same metric names
92    /// with no `role` label, so prometheus rejects it alongside a named role for differing label
93    /// dimensions rather than treating it as a second series. Verifying non-collision therefore
94    /// needs a second *named* role.
95    ///
96    /// TODO: drop the `cfg` when the interactive runtime lands and constructs this.
97    #[cfg(test)]
98    Interactive,
99}
100
101impl ComputeRuntimeRole {
102    /// The `role` metric/log label for this role, or `None` for `Solo`.
103    ///
104    /// `Solo` omits the label so a single-runtime deployment registers exactly as it did before a
105    /// second runtime existed, keeping exact-match dashboards and alerts unchanged.
106    pub fn label(self) -> Option<&'static str> {
107        match self {
108            ComputeRuntimeRole::Solo => None,
109            ComputeRuntimeRole::Maintenance => Some("maintenance"),
110            #[cfg(test)]
111            ComputeRuntimeRole::Interactive => Some("interactive"),
112        }
113    }
114
115    /// Whether this role runs the non-idempotent, process-global initializers.
116    ///
117    /// `Solo` and `Maintenance` run them. An interactive runtime shares the same process and
118    /// inherits the globals maintenance installs, so re-running them would either double-apply a
119    /// non-idempotent effect or race maintenance.
120    ///
121    /// NOTE: every role a release build can construct owns the globals, so this is constantly true
122    /// outside tests. The distinction becomes load-bearing when the interactive runtime lands.
123    pub fn owns_process_globals(self) -> bool {
124        matches!(
125            self,
126            ComputeRuntimeRole::Solo | ComputeRuntimeRole::Maintenance
127        )
128    }
129}
130
131/// Configures the server with compute-specific metrics.
132#[derive(Clone)]
133struct Config {
134    /// `persist` client cache.
135    pub persist_clients: Arc<PersistClientCache>,
136    /// Shared persist batch state.
137    pub persist_batches: SharedBatches,
138    /// Context necessary for rendering txn-wal operators.
139    pub txns_ctx: TxnsContext,
140    /// A process-global handle to tracing configuration.
141    pub tracing_handle: Arc<TracingHandle>,
142    /// Metrics exposed by compute replicas.
143    pub metrics: ComputeMetrics,
144    /// Other configuration for compute.
145    pub context: ComputeInstanceContext,
146    /// The process-global metrics registry.
147    pub metrics_registry: MetricsRegistry,
148    /// The number of timely workers per process.
149    pub workers_per_process: usize,
150    /// Bounds how many offloaded peek walks run at once, shared by every worker this server runs.
151    ///
152    /// NOTE: per compute runtime, not global. A process running a maintenance and an interactive
153    /// runtime calls `serve` twice and admits the bound once per call.
154    pub peek_permits: Arc<PeekPermits>,
155    /// Configuration for hosting storage objects on this cluster, if enabled.
156    pub storage_guest: Option<Arc<StorageGuestConfig>>,
157}
158
159/// A per-worker channel delivering storage client connections.
160type StorageClientRx = mpsc::UnboundedReceiver<(
161    Uuid,
162    mpsc::UnboundedReceiver<StorageCommand>,
163    mpsc::UnboundedSender<StorageResponse>,
164)>;
165
166/// Configuration for hosting storage objects on the compute cluster.
167pub struct StorageGuestConfig {
168    /// Per-worker channels delivering storage client connections, indexed by local worker index.
169    client_rxs: Mutex<Vec<Option<StorageClientRx>>>,
170    /// Metrics for storage objects.
171    metrics: StorageMetrics,
172    /// Function to get wall time now.
173    now: NowFn,
174    /// Configuration for source and sink connections.
175    connection_context: ConnectionContext,
176    /// Other configuration for storage instances.
177    instance_context: StorageInstanceContext,
178    /// Shared rocksdb write buffer manager.
179    shared_rocksdb_write_buffer_manager: SharedWriteBufferManager,
180}
181
182/// Initiates a timely dataflow computation, processing compute commands.
183pub async fn serve(
184    timely_config: TimelyConfig,
185    role: ComputeRuntimeRole,
186    metrics_registry: &MetricsRegistry,
187    persist_clients: Arc<PersistClientCache>,
188    txns_ctx: TxnsContext,
189    tracing_handle: Arc<TracingHandle>,
190    context: ComputeInstanceContext,
191) -> Result<impl Fn() -> Box<dyn ComputeClient> + use<>, Error> {
192    let workers_per_process = timely_config.workers;
193    let config = Config {
194        persist_batches: SharedBatches::new(),
195        persist_clients,
196        txns_ctx,
197        tracing_handle,
198        metrics: ComputeMetrics::register_with(metrics_registry, role),
199        context,
200        metrics_registry: metrics_registry.clone(),
201        workers_per_process,
202        peek_permits: Arc::new(PeekPermits::new(workers_per_process)),
203        storage_guest: None,
204    };
205
206    let (_worker_threads, client_builder) = serve_inner(config, timely_config).await?;
207    Ok(client_builder)
208}
209
210/// Initiates a timely dataflow computation that processes compute commands and additionally hosts
211/// storage objects, processing storage commands received over a separate client connection.
212///
213/// Returns client builders for both the compute and the storage side.
214pub async fn serve_unified(
215    timely_config: TimelyConfig,
216    role: ComputeRuntimeRole,
217    metrics_registry: &MetricsRegistry,
218    persist_clients: Arc<PersistClientCache>,
219    txns_ctx: TxnsContext,
220    tracing_handle: Arc<TracingHandle>,
221    context: ComputeInstanceContext,
222    now: NowFn,
223    storage_connection_context: ConnectionContext,
224    storage_instance_context: StorageInstanceContext,
225) -> Result<
226    (
227        impl Fn() -> Box<dyn ComputeClient> + use<>,
228        impl Fn() -> Box<dyn StorageClient> + use<>,
229    ),
230    Error,
231> {
232    let workers_per_process = timely_config.workers;
233
234    // Per-worker channels over which storage client connections are delivered.
235    let mut storage_client_txs = Vec::new();
236    let mut storage_client_rxs = Vec::new();
237    for _ in 0..workers_per_process {
238        let (tx, rx) = mpsc::unbounded_channel();
239        storage_client_txs.push(tx);
240        storage_client_rxs.push(Some(rx));
241    }
242
243    let storage_guest = StorageGuestConfig {
244        client_rxs: Mutex::new(storage_client_rxs),
245        metrics: StorageMetrics::register_with(metrics_registry),
246        now,
247        connection_context: storage_connection_context,
248        instance_context: storage_instance_context,
249        shared_rocksdb_write_buffer_manager: Default::default(),
250    };
251
252    let config = Config {
253        persist_batches: SharedBatches::new(),
254        persist_clients,
255        txns_ctx,
256        tracing_handle,
257        metrics: ComputeMetrics::register_with(metrics_registry, role),
258        context,
259        metrics_registry: metrics_registry.clone(),
260        workers_per_process,
261        peek_permits: Arc::new(PeekPermits::new(workers_per_process)),
262        storage_guest: Some(Arc::new(storage_guest)),
263    };
264
265    let (worker_threads, compute_client_builder) = serve_inner(config, timely_config).await?;
266
267    let storage_client_txs = Arc::new(storage_client_txs);
268    let storage_client_builder = move || {
269        let client =
270            GuestClusterClient::new(Arc::clone(&storage_client_txs), worker_threads.clone());
271        let client: Box<dyn StorageClient> = Box::new(client);
272        client
273    };
274
275    Ok((compute_client_builder, storage_client_builder))
276}
277
278/// Builds the Timely cluster for the given config and returns its worker threads along with a
279/// builder for compute clients to it.
280async fn serve_inner(
281    config: Config,
282    timely_config: TimelyConfig,
283) -> Result<(Vec<Thread>, impl Fn() -> Box<dyn ComputeClient> + use<>), Error> {
284    mz_timely_util::column_pager::metrics::register(
285        &config.metrics_registry,
286        mz_timely_util::column_pager::tiered_policy(),
287    );
288    mz_timely_util::pool_config::metrics::register(&config.metrics_registry);
289    mz_cluster::client::register_exert_policy_metrics(&config.metrics_registry);
290
291    let tokio_executor = tokio::runtime::Handle::current();
292
293    let timely_container = config.build_cluster(timely_config, tokio_executor).await?;
294    let worker_threads = timely_container.worker_threads();
295    let timely_container = Arc::new(Mutex::new(timely_container));
296
297    let client_builder = move || {
298        let client = ClusterClient::new(Arc::clone(&timely_container));
299        let client: Box<dyn ComputeClient> = Box::new(client);
300        client
301    };
302
303    Ok((worker_threads, client_builder))
304}
305
306/// Error type returned on connection nonce changes.
307///
308/// A nonce change informs workers that subsequent commands come a from a new client connection
309/// and therefore require reconciliation.
310struct NonceChange(Uuid);
311
312/// Endpoint used by workers to receive compute commands.
313///
314/// Observes nonce changes in the command stream and converts them into receive errors.
315struct CommandReceiver {
316    /// The channel supplying commands.
317    inner: command_channel::Receiver,
318    /// The ID of the Timely worker.
319    worker_id: usize,
320    /// The nonce identifying the current cluster protocol incarnation.
321    nonce: Option<Uuid>,
322    /// A stash to enable peeking the next command, used in `try_recv`.
323    stashed_command: Option<ComputeCommand>,
324}
325
326impl CommandReceiver {
327    fn new(inner: command_channel::Receiver, worker_id: usize) -> Self {
328        Self {
329            inner,
330            worker_id,
331            nonce: None,
332            stashed_command: None,
333        }
334    }
335
336    /// Receive the next pending command, if any.
337    ///
338    /// If the next compute command has a different nonce, this method instead returns an `Err`
339    /// containing the new nonce.
340    fn try_recv(&mut self) -> Result<Option<WorkerCommand>, NonceChange> {
341        if let Some(command) = self.stashed_command.take() {
342            return Ok(Some(WorkerCommand::Compute(command)));
343        }
344        let Some(message) = self.inner.try_recv() else {
345            return Ok(None);
346        };
347
348        let (command, nonce) = match message {
349            UnifiedCommand::Compute(command, nonce) => (command, nonce),
350            UnifiedCommand::Storage(command) => {
351                trace!(
352                    worker = self.worker_id,
353                    ?command,
354                    "received storage command"
355                );
356                return Ok(Some(WorkerCommand::Storage(command)));
357            }
358        };
359
360        trace!(worker = self.worker_id, %nonce, ?command, "received command");
361
362        if Some(nonce) == self.nonce {
363            Ok(Some(WorkerCommand::Compute(command)))
364        } else {
365            self.nonce = Some(nonce);
366            self.stashed_command = Some(command);
367            Err(NonceChange(nonce))
368        }
369    }
370}
371
372/// A command dispatched to the worker from the command channel.
373enum WorkerCommand {
374    /// A compute command.
375    Compute(ComputeCommand),
376    /// A storage-internal command, to be dispatched to the storage guest.
377    Storage(InternalStorageCommand),
378}
379
380/// Endpoint used by workers to send sending compute responses.
381///
382/// Tags responses with the current nonce, allowing receivers to filter out responses intended for
383/// previous client connections.
384pub(crate) struct ResponseSender {
385    /// The channel consuming responses.
386    inner: mpsc::UnboundedSender<(ComputeResponse, Uuid)>,
387    /// The ID of the Timely worker.
388    worker_id: usize,
389    /// The nonce identifying the current cluster protocol incarnation.
390    nonce: Option<Uuid>,
391}
392
393impl ResponseSender {
394    /// `pub(crate)` rather than private so the peek tests can build the sender a worker holds.
395    pub(crate) fn new(
396        inner: mpsc::UnboundedSender<(ComputeResponse, Uuid)>,
397        worker_id: usize,
398    ) -> Self {
399        Self {
400            inner,
401            worker_id,
402            nonce: None,
403        }
404    }
405
406    /// Set the cluster protocol nonce.
407    pub(crate) fn set_nonce(&mut self, nonce: Uuid) {
408        self.nonce = Some(nonce);
409    }
410
411    /// Send a compute response.
412    pub fn send(&self, response: ComputeResponse) -> Result<(), SendError<ComputeResponse>> {
413        let nonce = self.nonce.expect("nonce must be initialized");
414
415        trace!(worker = self.worker_id, %nonce, ?response, "sending response");
416        self.inner
417            .send((response, nonce))
418            .map_err(|SendError((resp, _))| SendError(resp))
419    }
420}
421
422/// State maintained for each worker thread.
423///
424/// Much of this state can be viewed as local variables for the worker thread,
425/// holding state that persists across function calls.
426struct Worker<'w> {
427    /// The underlying Timely worker.
428    timely_worker: &'w mut TimelyWorker,
429    /// The channel over which commands are received.
430    command_rx: CommandReceiver,
431    /// The channel over which responses are sent.
432    response_tx: ResponseSender,
433    compute_state: Option<ComputeState>,
434    /// Compute metrics.
435    metrics: WorkerMetrics,
436    /// A process-global cache of (blob_uri, consensus_uri) -> PersistClient.
437    /// This is intentionally shared between workers
438    persist_clients: Arc<PersistClientCache>,
439    persist_batches: SharedBatches,
440    /// Context necessary for rendering txn-wal operators.
441    txns_ctx: TxnsContext,
442    /// A process-global handle to tracing configuration.
443    tracing_handle: Arc<TracingHandle>,
444    context: ComputeInstanceContext,
445    /// The process-global metrics registry.
446    metrics_registry: MetricsRegistry,
447    /// The number of timely workers per process.
448    workers_per_process: usize,
449    /// Bounds how many offloaded peek walks run at once, shared by the workers of one `serve`
450    /// call rather than by the process.
451    peek_permits: Arc<PeekPermits>,
452    /// The hosted storage guest, if any.
453    storage: Option<StorageGuest>,
454}
455
456/// Per-worker state for hosting storage objects on the compute cluster.
457struct StorageGuest {
458    /// Channel delivering new storage client connections.
459    client_rx: StorageClientRx,
460    /// The current storage client connection, if any.
461    conn: Option<StorageConn>,
462    /// The hosted storage worker state.
463    storage_state: StorageState,
464    /// The last time storage maintenance ran.
465    last_maintenance: Instant,
466    /// The last time storage statistics were reported.
467    last_stats_time: Instant,
468}
469
470impl StorageGuest {
471    /// The longest the worker may park before the guest's next periodic duty (frontier reporting
472    /// or statistics collection) comes due, or `None` when no duty is pending.
473    ///
474    /// Mirrors the parking of storage's own server loop: the maintenance and statistics intervals
475    /// bound the park. A maintenance deadline in the past does not bound it, because maintenance
476    /// runs on the next wakeup anyway. The initial zero maintenance interval would otherwise turn
477    /// every park into a spin.
478    fn park_cap(&self) -> Option<Duration> {
479        // Periodic duties run only on a reconciled connection. Without one there is no deadline
480        // to meet, and connection and command arrivals unpark the worker.
481        let conn_serving = self
482            .conn
483            .as_ref()
484            .is_some_and(|conn| conn.reconcile_buf.is_none());
485        if !conn_serving {
486            return None;
487        }
488
489        let maintenance_interval = self.storage_state.server_maintenance_interval;
490        let stats_interval = self
491            .storage_state
492            .storage_configuration
493            .parameters
494            .statistics_collection_interval;
495
496        let next_maintenance =
497            (self.last_maintenance + maintenance_interval).checked_duration_since(Instant::now());
498        let next_stats = stats_interval.saturating_sub(self.last_stats_time.elapsed());
499        match next_maintenance {
500            Some(maintenance) => Some(maintenance.min(next_stats)),
501            None => Some(next_stats),
502        }
503    }
504}
505
506/// A storage client connection.
507struct StorageConn {
508    /// The channel over which storage commands are received.
509    command_rx: mpsc::UnboundedReceiver<StorageCommand>,
510    /// The channel over which storage responses are sent.
511    response_tx: mpsc::UnboundedSender<StorageResponse>,
512    /// Commands buffered for reconciliation, until `InitializationComplete` is received.
513    /// `None` once the connection is reconciled and serving.
514    reconcile_buf: Option<Vec<StorageCommand>>,
515}
516
517impl ClusterSpec for Config {
518    type Command = ComputeCommand;
519    type Response = ComputeResponse;
520
521    const NAME: &str = "compute";
522
523    fn run_worker(
524        &self,
525        timely_worker: &mut TimelyWorker,
526        client_rx: mpsc::UnboundedReceiver<(
527            Uuid,
528            mpsc::UnboundedReceiver<ComputeCommand>,
529            mpsc::UnboundedSender<ComputeResponse>,
530        )>,
531    ) {
532        if self.context.worker_core_affinity {
533            set_core_affinity(timely_worker.index());
534        }
535
536        let worker_id = timely_worker.index();
537        let metrics = self.metrics.for_worker(worker_id);
538
539        let local_index = worker_id % self.workers_per_process;
540
541        // Prepare the storage guest's inputs to the command channel, so
542        // storage-internal commands are sequenced through the same lane as compute commands.
543        let mut storage_lane_input = None;
544        let guest_setup = self.storage_guest.as_ref().map(|cfg| {
545            let storage_client_rx = cfg.client_rxs.lock().expect("poisoned")[local_index]
546                .take()
547                .expect("each worker takes its storage client_rx exactly once");
548            let (internal_tx, internal_rx) = std::sync::mpsc::channel();
549            let activator_slot = Rc::new(RefCell::new(None));
550            storage_lane_input = Some(StorageLaneInput {
551                rx: internal_rx,
552                activator_slot: Rc::clone(&activator_slot),
553            });
554            let internal_cmd_tx = InternalCommandSender::from_parts(internal_tx, activator_slot);
555            (Arc::clone(cfg), storage_client_rx, internal_cmd_tx)
556        });
557
558        // Create the command channel that broadcasts commands from worker 0 to other workers. We
559        // reuse this channel between client connections, to avoid bugs where different workers end
560        // up creating incompatible sides of the channel dataflow after reconnects.
561        // See database-issues#8964.
562        let (cmd_tx, cmd_rx) = command_channel::render(timely_worker, storage_lane_input);
563        let (resp_tx, resp_rx) = mpsc::unbounded_channel();
564
565        spawn_channel_adapter(client_rx, cmd_tx, resp_rx, worker_id);
566
567        // Create the storage guest state.
568        let storage = guest_setup.map(|(cfg, storage_client_rx, internal_cmd_tx)| {
569            let storage_state = StorageState::new_guest(
570                timely_worker.index(),
571                timely_worker.peers(),
572                internal_cmd_tx,
573                // The host dispatches internal commands from the unified command channel, so
574                // the guest reads no receiver of its own.
575                None,
576                cfg.metrics.clone(),
577                cfg.now.clone(),
578                cfg.connection_context.clone(),
579                cfg.instance_context.clone(),
580                Arc::clone(&self.persist_clients),
581                self.txns_ctx.clone(),
582                Arc::clone(&self.tracing_handle),
583                cfg.shared_rocksdb_write_buffer_manager.clone(),
584            );
585
586            StorageGuest {
587                client_rx: storage_client_rx,
588                conn: None,
589                storage_state,
590                last_maintenance: Instant::now(),
591                last_stats_time: Instant::now(),
592            }
593        });
594
595        Worker {
596            timely_worker,
597            command_rx: CommandReceiver::new(cmd_rx, worker_id),
598            response_tx: ResponseSender::new(resp_tx, worker_id),
599            metrics,
600            context: self.context.clone(),
601            persist_clients: Arc::clone(&self.persist_clients),
602            persist_batches: self.persist_batches.clone(),
603            txns_ctx: self.txns_ctx.clone(),
604            compute_state: None,
605            tracing_handle: Arc::clone(&self.tracing_handle),
606            metrics_registry: self.metrics_registry.clone(),
607            workers_per_process: self.workers_per_process,
608            peek_permits: Arc::clone(&self.peek_permits),
609            storage,
610        }
611        .run()
612    }
613}
614
615/// Set the current thread's core affinity, based on the given `worker_id`.
616#[cfg(not(target_os = "macos"))]
617fn set_core_affinity(worker_id: usize) {
618    use tracing::error;
619
620    let Some(mut core_ids) = core_affinity::get_core_ids() else {
621        error!(worker_id, "unable to get core IDs for setting affinity");
622        return;
623    };
624
625    // The `get_core_ids` docs don't say anything about a guaranteed order of the returned Vec,
626    // so sort it just to be safe.
627    core_ids.sort_unstable_by_key(|i| i.id);
628
629    // On multi-process replicas `worker_id` might be greater than the number of available cores.
630    // However, we assume that we always have at least as many cores as there are local workers.
631    // Violating this assumption is safe but might lead to degraded performance due to skew in core
632    // utilization.
633    let idx = worker_id % core_ids.len();
634    let core_id = core_ids[idx];
635
636    if core_affinity::set_for_current(core_id) {
637        info!(
638            worker_id,
639            core_id = core_id.id,
640            "set core affinity for worker"
641        );
642    } else {
643        error!(
644            worker_id,
645            core_id = core_id.id,
646            "failed to set core affinity for worker"
647        )
648    }
649}
650
651/// Set the current thread's core affinity, based on the given `worker_id`.
652#[cfg(target_os = "macos")]
653fn set_core_affinity(_worker_id: usize) {
654    // Setting core affinity is known to not work on Apple Silicon:
655    // https://github.com/Elzair/core_affinity_rs/issues/22
656    info!("setting core affinity is not supported on macOS");
657}
658
659impl<'w> Worker<'w> {
660    /// Runs a compute worker.
661    pub fn run(&mut self) {
662        // The command receiver is initialized without an nonce, so receiving the first command
663        // always triggers a nonce change.
664        let NonceChange(nonce) = self.recv_command().expect_err("change to first nonce");
665        self.set_nonce(nonce);
666
667        loop {
668            let Err(NonceChange(nonce)) = self.run_client();
669            self.set_nonce(nonce);
670        }
671    }
672
673    fn set_nonce(&mut self, nonce: Uuid) {
674        self.response_tx.set_nonce(nonce);
675    }
676
677    /// Handles commands for a client connection, returns when the nonce changes.
678    fn run_client(&mut self) -> Result<Infallible, NonceChange> {
679        self.reconcile()?;
680
681        // The last time we did periodic maintenance.
682        let mut last_maintenance = Instant::now();
683
684        // Commence normal operation.
685        loop {
686            // Get the maintenance interval, default to zero if we don't have a compute state.
687            let maintenance_interval = self
688                .compute_state
689                .as_ref()
690                .map_or(Duration::ZERO, |state| state.server_maintenance_interval);
691
692            let now = Instant::now();
693            // Determine if we need to perform maintenance, which is true if `maintenance_interval`
694            // time has passed since the last maintenance.
695            let sleep_duration;
696            if now >= last_maintenance + maintenance_interval {
697                last_maintenance = now;
698                sleep_duration = None;
699
700                // Report frontier information back the coordinator.
701                if let Some(mut compute_state) = self.activate_compute() {
702                    compute_state.compute_state.traces.maintenance();
703                    compute_state.report_frontiers();
704                    compute_state.report_metrics();
705                    compute_state.check_expiration();
706                }
707
708                self.metrics.record_shared_row_metrics();
709            } else {
710                // We didn't perform maintenance, sleep until the next maintenance interval.
711                let next_maintenance = last_maintenance + maintenance_interval;
712                sleep_duration = Some(next_maintenance.saturating_duration_since(now))
713            };
714
715            // Do not sleep while a peek waits for its turn. Only the sweep below gives it one,
716            // and nothing else leaves an activation behind to end the park.
717            let sleep_duration = match &self.compute_state {
718                Some(state) if state.peeks_awaiting_turn() => Some(Duration::ZERO),
719                _ => sleep_duration,
720            };
721
722            // With a storage guest, cap the park duration so the guest's periodic duties run on
723            // time.
724            let sleep_duration = match self.storage.as_ref().and_then(StorageGuest::park_cap) {
725                Some(cap) => Some(sleep_duration.map_or(cap, |d| d.min(cap))),
726                None => sleep_duration,
727            };
728
729            // Step the timely worker, recording the time taken.
730            let timer = self.metrics.timely_step_duration_seconds.start_timer();
731            if self.storage_guest_busy() {
732                self.timely_worker.step();
733            } else {
734                self.timely_worker.step_or_park(sleep_duration);
735            }
736            timer.observe_duration();
737
738            self.handle_pending_commands()?;
739
740            self.process_storage_guest();
741
742            if let Some(mut compute_state) = self.activate_compute() {
743                compute_state.process_peeks();
744                compute_state.process_subscribes();
745                compute_state.process_copy_tos();
746            }
747        }
748    }
749
750    fn handle_pending_commands(&mut self) -> Result<(), NonceChange> {
751        while let Some(cmd) = self.command_rx.try_recv()? {
752            match cmd {
753                WorkerCommand::Compute(cmd) => self.handle_command(cmd),
754                WorkerCommand::Storage(cmd) => self.handle_storage_internal_command(cmd),
755            }
756        }
757        Ok(())
758    }
759
760    /// Whether the storage guest has pending work that forbids parking.
761    ///
762    /// It is critical that we allow Timely to park iff there are no pending commands or async
763    /// worker responses, since those are delivered by other threads that only unpark us once, at
764    /// send time.
765    fn storage_guest_busy(&self) -> bool {
766        self.storage.as_ref().is_some_and(|guest| {
767            !guest.client_rx.is_empty()
768                || (!self.storage_commands_held()
769                    && guest
770                        .conn
771                        .as_ref()
772                        .is_some_and(|conn| !conn.command_rx.is_empty()))
773                || !guest.storage_state.async_worker.is_empty()
774        })
775    }
776
777    /// Whether the storage guest must leave external storage commands unread.
778    ///
779    /// Commands are held until compute's `CreateInstance` has initialized logging. Held commands
780    /// do not keep the worker busy. Handling `CreateInstance` releases them.
781    fn storage_commands_held(&self) -> bool {
782        // Storage commands render storage dataflows, and the loggers only observe operators
783        // created after their initialization, so a storage dataflow rendered earlier never
784        // appears in introspection. Nothing orders the storage and compute controller
785        // connections, so the storage controller can connect first. Every storage dataflow
786        // stems from an external storage command, so holding those back suffices.
787        self.compute_state.is_none()
788    }
789
790    /// Dispatch a storage-internal command from the command channel to
791    /// the storage guest. This is where all storage dataflow rendering happens.
792    fn handle_storage_internal_command(&mut self, cmd: InternalStorageCommand) {
793        let mut guest = self
794            .storage
795            .take()
796            .expect("the command channel carries storage commands only when a guest is hosted");
797
798        let mut worker = StorageWorker {
799            timely_worker: &mut *self.timely_worker,
800            client_rx: guest.client_rx,
801            storage_state: guest.storage_state,
802        };
803        worker.handle_internal_storage_command(cmd);
804
805        let StorageWorker {
806            timely_worker: _,
807            client_rx,
808            storage_state,
809        } = worker;
810        guest.client_rx = client_rx;
811        guest.storage_state = storage_state;
812        self.storage = Some(guest);
813    }
814
815    /// Process the storage guest's per-iteration duties: accept client
816    /// connections, handle external storage commands (buffering for reconciliation until
817    /// `InitializationComplete`), forward async worker responses, and report frontiers, dropped
818    /// collections, status updates, and statistics.
819    fn process_storage_guest(&mut self) {
820        let Some(mut guest) = self.storage.take() else {
821            return;
822        };
823
824        // Accept new client connections, replacing any current one. Every new connection starts
825        // with a reconciliation.
826        loop {
827            use tokio::sync::mpsc::error::TryRecvError;
828            match guest.client_rx.try_recv() {
829                Ok((_nonce, command_rx, response_tx)) => {
830                    guest.conn = Some(StorageConn {
831                        command_rx,
832                        response_tx,
833                        reconcile_buf: Some(Vec::new()),
834                    });
835                }
836                Err(TryRecvError::Empty | TryRecvError::Disconnected) => break,
837            }
838        }
839
840        let commands_held = self.storage_commands_held();
841        let mut worker = StorageWorker {
842            timely_worker: &mut *self.timely_worker,
843            client_rx: guest.client_rx,
844            storage_state: guest.storage_state,
845        };
846
847        // Handle responses from the async worker. Only worker 0 does async processing, so only
848        // worker 0 ever receives any. This must run with or without a client connection:
849        // storage-internal commands keep flowing through the command channel while the
850        // controller is away and can issue async work, and an undrained response queue keeps
851        // `storage_guest_busy` true, turning every park into a spin.
852        while let Ok(response) = worker.storage_state.async_worker.try_recv() {
853            worker.handle_async_worker_response(response);
854        }
855
856        let conn = if commands_held {
857            None
858        } else {
859            guest.conn.take()
860        };
861        let Some(mut conn) = conn else {
862            let StorageWorker {
863                timely_worker: _,
864                client_rx,
865                storage_state,
866            } = worker;
867            guest.client_rx = client_rx;
868            guest.storage_state = storage_state;
869            self.storage = Some(guest);
870            return;
871        };
872
873        // Drain external storage commands.
874        let mut disconnected = false;
875        loop {
876            use tokio::sync::mpsc::error::TryRecvError;
877            match conn.command_rx.try_recv() {
878                Ok(StorageCommand::InitializationComplete) if conn.reconcile_buf.is_some() => {
879                    let commands = conn.reconcile_buf.take().expect("checked above");
880                    worker.reconcile_commands(commands);
881                }
882                Ok(cmd) => match &mut conn.reconcile_buf {
883                    Some(buf) => buf.push(cmd),
884                    None => worker.storage_state.handle_storage_command(cmd),
885                },
886                Err(TryRecvError::Empty) => break,
887                Err(TryRecvError::Disconnected) => {
888                    disconnected = true;
889                    break;
890                }
891            }
892        }
893
894        // Response-producing duties run only on a reconciled connection.
895        if conn.reconcile_buf.is_none() {
896            let maintenance_interval = worker.storage_state.server_maintenance_interval;
897            let now = Instant::now();
898            if now >= guest.last_maintenance + maintenance_interval {
899                guest.last_maintenance = now;
900                worker.report_frontier_progress(&conn.response_tx);
901            }
902
903            for id in std::mem::take(&mut worker.storage_state.dropped_ids) {
904                worker.send_storage_response(&conn.response_tx, StorageResponse::DroppedId(id));
905            }
906
907            worker.process_oneshot_ingestions(&conn.response_tx);
908            worker.report_status_updates(&conn.response_tx);
909
910            let stats_interval = worker
911                .storage_state
912                .storage_configuration
913                .parameters
914                .statistics_collection_interval;
915            if guest.last_stats_time.elapsed() >= stats_interval {
916                worker.report_storage_statistics(&conn.response_tx);
917                guest.last_stats_time = Instant::now();
918            }
919        }
920
921        let StorageWorker {
922            timely_worker: _,
923            client_rx,
924            storage_state,
925        } = worker;
926        guest.client_rx = client_rx;
927        guest.storage_state = storage_state;
928        guest.conn = (!disconnected).then_some(conn);
929        self.storage = Some(guest);
930    }
931
932    fn handle_command(&mut self, cmd: ComputeCommand) {
933        if matches!(&cmd, ComputeCommand::CreateInstance(_)) {
934            self.compute_state = Some(ComputeState::new(
935                Arc::clone(&self.persist_clients),
936                self.persist_batches.clone(),
937                self.txns_ctx.clone(),
938                self.metrics.clone(),
939                Arc::clone(&self.tracing_handle),
940                self.context.clone(),
941                self.metrics_registry.clone(),
942                self.workers_per_process,
943                Arc::clone(&self.peek_permits),
944            ));
945        }
946        self.activate_compute().unwrap().handle_compute_command(cmd);
947    }
948
949    fn activate_compute(&mut self) -> Option<ActiveComputeState<'_>> {
950        if let Some(compute_state) = &mut self.compute_state {
951            Some(ActiveComputeState {
952                timely_worker: &mut *self.timely_worker,
953                compute_state,
954                response_tx: &mut self.response_tx,
955            })
956        } else {
957            None
958        }
959    }
960
961    /// Receive the next compute command.
962    ///
963    /// This method blocks if no command is currently available, but takes care to step the Timely
964    /// worker while doing so.
965    fn recv_command(&mut self) -> Result<ComputeCommand, NonceChange> {
966        loop {
967            if let Some(cmd) = self.command_rx.try_recv()? {
968                match cmd {
969                    WorkerCommand::Compute(cmd) => return Ok(cmd),
970                    // Storage-internal commands are dispatched even while
971                    // waiting for compute commands (e.g. during compute reconciliation), so
972                    // storage dataflow construction keeps its lane position on all workers.
973                    WorkerCommand::Storage(cmd) => {
974                        self.handle_storage_internal_command(cmd);
975                        continue;
976                    }
977                }
978            }
979
980            // Keep serving the storage guest while blocked on compute
981            // commands, and avoid unbounded parks that would stall its periodic duties.
982            self.process_storage_guest();
983            let park_cap = self.storage.as_ref().and_then(StorageGuest::park_cap);
984
985            let start = Instant::now();
986            if self.storage_guest_busy() {
987                self.timely_worker.step();
988            } else if let Some(cap) = park_cap {
989                self.timely_worker.step_or_park(Some(cap));
990            } else {
991                self.timely_worker.step_or_park(None);
992            }
993            self.metrics
994                .timely_step_duration_seconds
995                .observe(start.elapsed().as_secs_f64());
996        }
997    }
998
999    /// Extract commands until `InitializationComplete`, and make the worker reflect those commands.
1000    ///
1001    /// This method is meant to be a function of the commands received thus far (as recorded in the
1002    /// compute state command history) and the new commands from `command_rx`. It should not be a
1003    /// function of other characteristics, like whether the worker has managed to respond to a peek
1004    /// or not. Some effort goes in to narrowing our view to only the existing commands we can be sure
1005    /// are live at all other workers.
1006    ///
1007    /// The methodology here is to drain `command_rx` until an `InitializationComplete`, at which point
1008    /// the prior commands are "reconciled" in. Reconciliation takes each goal dataflow and looks for an
1009    /// existing "compatible" dataflow (per `compatible()`) it can repurpose, with some additional tests
1010    /// to be sure that we can cut over from one to the other (no additional compaction, no tails/sinks).
1011    /// With any connections established, old orphaned dataflows are allow to compact away, and any new
1012    /// dataflows are created from scratch. "Kept" dataflows are allowed to compact up to any new `as_of`.
1013    ///
1014    /// Some additional tidying happens, cleaning up pending peeks, reported frontiers, and creating a new
1015    /// subscribe response buffer. We will need to be vigilant with future modifications to `ComputeState` to
1016    /// line up changes there with clean resets here.
1017    fn reconcile(&mut self) -> Result<(), NonceChange> {
1018        // To initialize the connection, we want to drain all commands until we receive a
1019        // `ComputeCommand::InitializationComplete` command to form a target command state.
1020        let mut new_commands = Vec::new();
1021        loop {
1022            match self.recv_command()? {
1023                ComputeCommand::InitializationComplete => break,
1024                command => new_commands.push(command),
1025            }
1026        }
1027
1028        // Commands we will need to apply before entering normal service.
1029        // These commands may include dropping existing dataflows, compacting existing dataflows,
1030        // and creating new dataflows, in addition to standard peek and compaction commands.
1031        // The result should be the same as if dropping all dataflows and running `new_commands`.
1032        let mut todo_commands = Vec::new();
1033        // We only have a compute history if we are in an initialized state
1034        // (i.e. after a `CreateInstance`).
1035        // If this is not the case, just copy `new_commands` into `todo_commands`.
1036        if let Some(compute_state) = &mut self.compute_state {
1037            // Reduce the installed commands.
1038            // Importantly, act as if all peeks may have been retired (as we cannot know otherwise).
1039            compute_state.command_history.discard_peeks();
1040            compute_state.command_history.reduce();
1041
1042            // At this point, we need to sort out which of the *certainly installed* dataflows are
1043            // suitable replacements for the requested dataflows. A dataflow is "certainly installed"
1044            // as of a frontier if its compaction allows it to go no further. We ignore peeks for this
1045            // reasoning, as we cannot be certain that peeks still exist at any other worker.
1046
1047            // Having reduced our installed command history retaining no peeks (above), we should be able
1048            // to use track down installed dataflows we can use as surrogates for requested dataflows (which
1049            // have retained all of their peeks, creating a more demanding `as_of` requirement).
1050            // NB: installed dataflows may still be allowed to further compact, and we should double check
1051            // this before being too confident. It should be rare without peeks, but could happen with e.g.
1052            // multiple outputs of a dataflow.
1053
1054            // The values with which a prior `CreateInstance` was called, if it was.
1055            let mut old_instance_config = None;
1056            // Index dataflows by `export_ids().collect()`, as this is a precondition for their compatibility.
1057            let mut old_dataflows = BTreeMap::default();
1058            // Maintain allowed compaction, in case installed identifiers may have been allowed to compact.
1059            let mut old_frontiers = BTreeMap::default();
1060            for command in compute_state.command_history.iter() {
1061                match command {
1062                    ComputeCommand::CreateInstance(config) => {
1063                        old_instance_config = Some(config);
1064                    }
1065                    ComputeCommand::CreateDataflow(dataflow) => {
1066                        let export_ids = dataflow.export_ids().collect::<BTreeSet<_>>();
1067                        old_dataflows.insert(export_ids, dataflow);
1068                    }
1069                    ComputeCommand::AllowCompaction { id, frontier } => {
1070                        old_frontiers.insert(id, frontier);
1071                    }
1072                    _ => {
1073                        // Nothing to do in these cases.
1074                    }
1075                }
1076            }
1077
1078            // Compaction commands that can be applied to existing dataflows.
1079            let mut old_compaction = BTreeMap::default();
1080            // Exported identifiers from dataflows we retain.
1081            let mut retain_ids = BTreeSet::default();
1082
1083            // Traverse new commands, sorting out what remediation we can do.
1084            for command in new_commands.iter() {
1085                match command {
1086                    ComputeCommand::CreateDataflow(dataflow) => {
1087                        // Attempt to find an existing match for the dataflow.
1088                        let as_of = dataflow.as_of.as_ref().unwrap();
1089                        let export_ids = dataflow.export_ids().collect::<BTreeSet<_>>();
1090
1091                        if let Some(old_dataflow) = old_dataflows.get(&export_ids) {
1092                            let compatible = old_dataflow.compatible_with(dataflow);
1093                            let uncompacted = !export_ids
1094                                .iter()
1095                                .flat_map(|id| old_frontiers.get(id))
1096                                .any(|frontier| {
1097                                    !timely::PartialOrder::less_equal(
1098                                        *frontier,
1099                                        dataflow.as_of.as_ref().unwrap(),
1100                                    )
1101                                });
1102
1103                            // We cannot reconcile subscribe and copy-to sinks at the moment,
1104                            // because the response buffer is shared, and to a first approximation
1105                            // must be completely reformed.
1106                            let subscribe_free = dataflow.subscribe_ids().next().is_none();
1107                            let copy_to_free = dataflow.copy_to_ids().next().is_none();
1108
1109                            // If we have replaced any dependency of this dataflow, we need to
1110                            // replace this dataflow, to make it use the replacement.
1111                            let dependencies_retained = dataflow
1112                                .imported_index_ids()
1113                                .all(|id| retain_ids.contains(&id));
1114
1115                            if compatible
1116                                && uncompacted
1117                                && subscribe_free
1118                                && copy_to_free
1119                                && dependencies_retained
1120                            {
1121                                // Match found; remove the match from the deletion queue,
1122                                // and compact its outputs to the dataflow's `as_of`.
1123                                old_dataflows.remove(&export_ids);
1124                                for id in export_ids.iter() {
1125                                    old_compaction.insert(*id, as_of.clone());
1126                                }
1127                                retain_ids.extend(export_ids);
1128                            } else {
1129                                warn!(
1130                                    ?export_ids,
1131                                    ?compatible,
1132                                    ?uncompacted,
1133                                    ?subscribe_free,
1134                                    ?copy_to_free,
1135                                    ?dependencies_retained,
1136                                    old_as_of = ?old_dataflow.as_of,
1137                                    new_as_of = ?as_of,
1138                                    "dataflow reconciliation failed",
1139                                );
1140
1141                                // Dump the full dataflow plans if they are incompatible, to
1142                                // simplify debugging hard-to-reproduce reconciliation failures.
1143                                if !compatible {
1144                                    warn!(
1145                                        old = ?old_dataflow,
1146                                        new = ?dataflow,
1147                                        "incompatible dataflows in reconciliation",
1148                                    );
1149                                }
1150
1151                                todo_commands
1152                                    .push(ComputeCommand::CreateDataflow(dataflow.clone()));
1153                            }
1154
1155                            compute_state.metrics.record_dataflow_reconciliation(
1156                                compatible,
1157                                uncompacted,
1158                                subscribe_free,
1159                                copy_to_free,
1160                                dependencies_retained,
1161                            );
1162                        } else {
1163                            todo_commands.push(ComputeCommand::CreateDataflow(dataflow.clone()));
1164                        }
1165                    }
1166                    ComputeCommand::CreateInstance(config) => {
1167                        // Cluster creation should not be performed again!
1168                        if old_instance_config.map_or(false, |old| !old.compatible_with(config)) {
1169                            halt!(
1170                                "new instance configuration not compatible with existing instance configuration:\n{:?}\nvs\n{:?}",
1171                                config,
1172                                old_instance_config,
1173                            );
1174                        }
1175                    }
1176                    // All other commands we apply as requested.
1177                    command => {
1178                        todo_commands.push(command.clone());
1179                    }
1180                }
1181            }
1182
1183            // Issue compaction commands first to reclaim resources.
1184            for (_, dataflow) in old_dataflows.iter() {
1185                for id in dataflow.export_ids() {
1186                    // We want to drop anything that has not yet been dropped,
1187                    // and nothing that has already been dropped.
1188                    if old_frontiers.get(&id) != Some(&&Antichain::new()) {
1189                        old_compaction.insert(id, Antichain::new());
1190                    }
1191                }
1192            }
1193            for (&id, frontier) in &old_compaction {
1194                let frontier = frontier.clone();
1195                todo_commands.insert(0, ComputeCommand::AllowCompaction { id, frontier });
1196            }
1197
1198            // Clean up worker-local state.
1199            //
1200            // Various aspects of `ComputeState` need to be either uninstalled, or return to a blank slate.
1201            // All dropped dataflows should clean up after themselves, as we plan to install new dataflows
1202            // re-using the same identifiers.
1203            // All re-used dataflows should roll back any believed communicated information (e.g. frontiers)
1204            // so that they recommunicate that information as if from scratch.
1205
1206            // Remove all peeks, whether they have started or are still awaiting a turn.
1207            let queued = std::mem::take(&mut compute_state.queued_peeks);
1208            let pending = std::mem::take(&mut compute_state.pending_peeks);
1209            for peek in queued.into_iter().map(PendingPeek::Index).chain(pending) {
1210                // Log dropping the peek request.
1211                if let Some(logger) = compute_state.compute_logger.as_mut() {
1212                    logger.log(&peek.as_log_event(false));
1213                }
1214            }
1215
1216            for (&id, collection) in compute_state.collections.iter_mut() {
1217                // Adjust reported frontiers:
1218                //  * For dataflows we continue to use, reset to ensure we report something not
1219                //    before the new `as_of` next.
1220                //  * For dataflows we drop, set to the empty frontier, to ensure we don't report
1221                //    anything for them.
1222                let retained = retain_ids.contains(&id);
1223                let compaction = old_compaction.remove(&id);
1224                let new_reported_frontier = match (retained, compaction) {
1225                    (true, Some(new_as_of)) => ReportedFrontier::NotReported { lower: new_as_of },
1226                    (true, None) => {
1227                        unreachable!("retained dataflows are compacted to the new as_of")
1228                    }
1229                    (false, Some(new_frontier)) => {
1230                        assert!(new_frontier.is_empty());
1231                        ReportedFrontier::Reported(new_frontier)
1232                    }
1233                    (false, None) => {
1234                        // Logging dataflows are implicitly retained and don't have a new as_of.
1235                        // Reset them to the minimal frontier.
1236                        ReportedFrontier::new()
1237                    }
1238                };
1239
1240                collection.reset_reported_frontiers(new_reported_frontier);
1241
1242                // Sink tokens should be retained for retained dataflows, and dropped for dropped
1243                // dataflows.
1244                //
1245                // Dropping the tokens of active subscribe and copy-tos makes them place
1246                // `DroppedAt` responses into the respective response buffer. We drop those buffers
1247                // in the next step, which ensures that we don't send out `DroppedAt` responses for
1248                // subscribe/copy-tos dropped during reconciliation.
1249                if !retained {
1250                    collection.sink_token = None;
1251                }
1252            }
1253
1254            // We must drop the response buffers as they are global across all subscribe/copy-tos.
1255            // If they were broken out by `GlobalId` then we could drop only the response buffers
1256            // of dataflows we drop.
1257            compute_state.subscribe_response_buffer = Rc::new(RefCell::new(Vec::new()));
1258            compute_state.copy_to_response_buffer = Rc::new(RefCell::new(Vec::new()));
1259
1260            // The controller expects the logging collections to be readable from the minimum time
1261            // initially. We cannot recreate the logging arrangements without restarting the
1262            // instance, but we can pad the compacted times with empty data. Doing so is sound
1263            // because logging collections from different replica incarnations are considered
1264            // distinct TVCs, so the controller doesn't expect any historical consistency from
1265            // these collections when it reconnects to a replica.
1266            //
1267            // TODO(database-issues#8152): Consider resolving this with controller-side reconciliation instead.
1268            if let Some(config) = old_instance_config {
1269                for id in config.logging.index_logs.values() {
1270                    let trace = compute_state
1271                        .traces
1272                        .remove(id)
1273                        .expect("logging trace exists");
1274                    let padded = trace.into_padded();
1275                    compute_state.traces.set(*id, padded);
1276                }
1277            }
1278        } else {
1279            todo_commands.clone_from(&new_commands);
1280        }
1281
1282        // Execute the commands to bring us to `new_commands`.
1283        for command in todo_commands.into_iter() {
1284            self.handle_command(command);
1285        }
1286
1287        // Overwrite `self.command_history` to reflect `new_commands`.
1288        // It is possible that there still isn't a compute state yet.
1289        if let Some(compute_state) = &mut self.compute_state {
1290            let mut command_history = ComputeCommandHistory::new(self.metrics.for_history());
1291            for command in new_commands.iter() {
1292                command_history.push(command.clone());
1293            }
1294            compute_state.command_history = command_history;
1295        }
1296        Ok(())
1297    }
1298}
1299
1300/// Spawn a task to bridge between [`ClusterClient`] and [`Worker`] channels.
1301///
1302/// The [`Worker`] expects a pair of persistent channels, with punctuation marking reconnects,
1303/// while the [`ClusterClient`] provides a new pair of channels on each reconnect.
1304fn spawn_channel_adapter(
1305    mut client_rx: mpsc::UnboundedReceiver<(
1306        Uuid,
1307        mpsc::UnboundedReceiver<ComputeCommand>,
1308        mpsc::UnboundedSender<ComputeResponse>,
1309    )>,
1310    command_tx: command_channel::Sender,
1311    mut response_rx: mpsc::UnboundedReceiver<(ComputeResponse, Uuid)>,
1312    worker_id: usize,
1313) {
1314    mz_ore::task::spawn(
1315        || format!("compute-channel-adapter-{worker_id}"),
1316        async move {
1317            // To make workers aware of the individual client connections, we tag forwarded
1318            // commands with the client nonce. Additionally, we use the nonce to filter out
1319            // responses with a different nonce, which are intended for different client
1320            // connections.
1321            //
1322            // It's possible that we receive responses with nonces from the past but also from the
1323            // future: Worker 0 might have received a new nonce before us and broadcasted it to our
1324            // Timely cluster. When we receive a response with a future nonce, we need to wait with
1325            // forwarding it until we have received the same nonce from a client connection.
1326            //
1327            // Nonces are not ordered so we don't know whether a response nonce is from the past or
1328            // the future. We thus assume that every response with an unknown nonce might be from
1329            // the future and stash them all. Every time we reconnect, we immediately send all
1330            // stashed responses with a matching nonce. Every time we receive a new response with a
1331            // nonce that matches our current one, we can discard the entire response stash as we
1332            // know that all stashed responses must be from the past.
1333            let mut stashed_responses = BTreeMap::<Uuid, Vec<ComputeResponse>>::new();
1334
1335            while let Some((nonce, mut command_rx, response_tx)) = client_rx.recv().await {
1336                // Send stashed responses for this client.
1337                if let Some(resps) = stashed_responses.remove(&nonce) {
1338                    for resp in resps {
1339                        let _ = response_tx.send(resp);
1340                    }
1341                }
1342
1343                // Wait for a new response while forwarding received commands.
1344                let mut serve_rx_channels = async || loop {
1345                    tokio::select! {
1346                        msg = command_rx.recv() => match msg {
1347                            Some(cmd) => command_tx.send((cmd, nonce)),
1348                            None => return Err(()),
1349                        },
1350                        msg = response_rx.recv() => {
1351                            return Ok(msg.expect("worker connected"));
1352                        }
1353                    }
1354                };
1355
1356                // Serve this connection until we see any of the channels disconnect.
1357                loop {
1358                    let Ok((resp, resp_nonce)) = serve_rx_channels().await else {
1359                        break;
1360                    };
1361
1362                    if resp_nonce == nonce {
1363                        // Response for the current connection; forward it.
1364                        stashed_responses.clear();
1365                        if response_tx.send(resp).is_err() {
1366                            break;
1367                        }
1368                    } else {
1369                        // Response for a past or future connection; stash it.
1370                        let stash = stashed_responses.entry(resp_nonce).or_default();
1371                        stash.push(resp);
1372                    }
1373                }
1374            }
1375        },
1376    );
1377}