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}