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