Skip to main content

mz_cluster/
client.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 cluster server.
11
12use std::fmt;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Arc, Mutex, OnceLock};
15use std::task::{Context, Poll};
16use std::thread::Thread;
17
18use anyhow::{Error, anyhow};
19use async_trait::async_trait;
20use differential_dataflow::trace::ExertionLogic;
21use mz_cluster_client::client::{TimelyConfig, TryIntoProtocolNonce};
22use mz_ore::metric;
23use mz_ore::metrics::{ComputedUIntGauge, MetricsRegistry};
24use mz_service::client::{GenericClient, Partitionable, Partitioned};
25use mz_service::local::LocalClient;
26use timely::WorkerConfig;
27use timely::communication::allocator::zero_copy::bytes_slab::BytesRefill;
28use timely::communication::initialize::WorkerGuards;
29use timely::execute::execute_from;
30use timely::worker::Worker as TimelyWorker;
31use tokio::runtime::Handle;
32use tokio::sync::mpsc;
33use tracing::{info, info_span};
34use uuid::Uuid;
35
36use crate::communication::initialize_networking;
37
38type PartitionedClient<C, R> = Partitioned<LocalClient<C, R>, C, R>;
39
40/// A client managing access to the local portion of a Timely cluster
41pub struct ClusterClient<C>
42where
43    C: ClusterSpec,
44    (C::Command, C::Response): Partitionable<C::Command, C::Response>,
45{
46    /// The actual client to talk to the cluster
47    inner: Option<PartitionedClient<C::Command, C::Response>>,
48    /// The running timely instance
49    timely_container: Arc<Mutex<TimelyContainer<C>>>,
50}
51
52/// Metadata about timely workers in this process.
53pub struct TimelyContainer<C: ClusterSpec> {
54    /// Channels over which to send endpoints for wiring up a new Client
55    client_txs: Vec<
56        mpsc::UnboundedSender<(
57            Uuid,
58            mpsc::UnboundedReceiver<C::Command>,
59            mpsc::UnboundedSender<C::Response>,
60        )>,
61    >,
62    /// Thread guards that keep worker threads alive
63    worker_guards: WorkerGuards<()>,
64}
65
66impl<C: ClusterSpec> TimelyContainer<C> {
67    /// The threads of the Timely workers in this process.
68    pub fn worker_threads(&self) -> Vec<Thread> {
69        self.worker_guards
70            .guards()
71            .iter()
72            .map(|h| h.thread().clone())
73            .collect()
74    }
75}
76
77/// A client to a secondary ("guest") command stream served by workers of
78/// an existing Timely cluster. Like [`ClusterClient`], but the per-worker client channels are
79/// provided externally instead of coming from a [`TimelyContainer`] built for this command type.
80pub struct GuestClusterClient<Cmd, Resp>
81where
82    (Cmd, Resp): Partitionable<Cmd, Resp>,
83{
84    /// Per-worker channels over which to send endpoints for wiring up a new client.
85    client_txs: Arc<
86        Vec<
87            mpsc::UnboundedSender<(
88                Uuid,
89                mpsc::UnboundedReceiver<Cmd>,
90                mpsc::UnboundedSender<Resp>,
91            )>,
92        >,
93    >,
94    /// The worker threads, for unparking on send.
95    worker_threads: Vec<Thread>,
96    /// The actual client to talk to the cluster.
97    inner: Option<PartitionedClient<Cmd, Resp>>,
98}
99
100impl<Cmd, Resp> GuestClusterClient<Cmd, Resp>
101where
102    Cmd: fmt::Debug + Send + TryIntoProtocolNonce,
103    Resp: fmt::Debug + Send,
104    (Cmd, Resp): Partitionable<Cmd, Resp>,
105{
106    /// Create a new `GuestClusterClient`.
107    pub fn new(
108        client_txs: Arc<
109            Vec<
110                mpsc::UnboundedSender<(
111                    Uuid,
112                    mpsc::UnboundedReceiver<Cmd>,
113                    mpsc::UnboundedSender<Resp>,
114                )>,
115            >,
116        >,
117        worker_threads: Vec<Thread>,
118    ) -> Self {
119        Self {
120            client_txs,
121            worker_threads,
122            inner: None,
123        }
124    }
125
126    fn connect(&mut self, nonce: Uuid) {
127        let mut command_txs = Vec::new();
128        let mut response_rxs = Vec::new();
129        for client_tx in self.client_txs.iter() {
130            let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
131            let (resp_tx, resp_rx) = mpsc::unbounded_channel();
132
133            client_tx
134                .send((nonce, cmd_rx, resp_tx))
135                .expect("worker not dropped");
136
137            command_txs.push(cmd_tx);
138            response_rxs.push(resp_rx);
139        }
140
141        self.inner = Some(LocalClient::new_partitioned(
142            response_rxs,
143            command_txs,
144            self.worker_threads.clone(),
145        ));
146    }
147}
148
149impl<Cmd, Resp> fmt::Debug for GuestClusterClient<Cmd, Resp>
150where
151    (Cmd, Resp): Partitionable<Cmd, Resp>,
152{
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        f.debug_struct("GuestClusterClient").finish_non_exhaustive()
155    }
156}
157
158impl<Cmd, Resp> GenericClient<Cmd, Resp> for GuestClusterClient<Cmd, Resp>
159where
160    Cmd: fmt::Debug + Send + TryIntoProtocolNonce,
161    Resp: fmt::Debug + Send,
162    (Cmd, Resp): Partitionable<Cmd, Resp>,
163{
164    async fn send(&mut self, cmd: Cmd) -> Result<(), Error> {
165        match cmd.try_into_protocol_nonce() {
166            Ok(nonce) => {
167                self.connect(nonce);
168                Ok(())
169            }
170            Err(cmd) => self.inner.as_mut().expect("initialized").send(cmd).await,
171        }
172    }
173
174    fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll<Result<Option<Resp>, Error>> {
175        match self.inner.as_mut() {
176            Some(client) => client.poll_recv(cx),
177            None => Poll::Pending,
178        }
179    }
180}
181
182impl<C> ClusterClient<C>
183where
184    C: ClusterSpec,
185    (C::Command, C::Response): Partitionable<C::Command, C::Response>,
186{
187    /// Create a new `ClusterClient`.
188    pub fn new(timely_container: Arc<Mutex<TimelyContainer<C>>>) -> Self {
189        Self {
190            timely_container,
191            inner: None,
192        }
193    }
194
195    /// Connect to the Timely cluster with the given client nonce.
196    fn connect(&mut self, nonce: Uuid) -> Result<(), Error> {
197        let timely = self.timely_container.lock().expect("poisoned");
198
199        let mut command_txs = Vec::new();
200        let mut response_rxs = Vec::new();
201        for client_tx in &timely.client_txs {
202            let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
203            let (resp_tx, resp_rx) = mpsc::unbounded_channel();
204
205            client_tx
206                .send((nonce, cmd_rx, resp_tx))
207                .expect("worker not dropped");
208
209            command_txs.push(cmd_tx);
210            response_rxs.push(resp_rx);
211        }
212
213        self.inner = Some(LocalClient::new_partitioned(
214            response_rxs,
215            command_txs,
216            timely.worker_threads(),
217        ));
218        Ok(())
219    }
220}
221
222impl<C> fmt::Debug for ClusterClient<C>
223where
224    C: ClusterSpec,
225    (C::Command, C::Response): Partitionable<C::Command, C::Response>,
226{
227    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228        f.debug_struct("ClusterClient")
229            .field("inner", &self.inner)
230            .finish_non_exhaustive()
231    }
232}
233
234impl<C> GenericClient<C::Command, C::Response> for ClusterClient<C>
235where
236    C: ClusterSpec,
237    (C::Command, C::Response): Partitionable<C::Command, C::Response>,
238{
239    async fn send(&mut self, cmd: C::Command) -> Result<(), Error> {
240        // Changing this debug statement requires changing the replica-isolation test
241        tracing::debug!("ClusterClient send={:?}", &cmd);
242
243        match cmd.try_into_protocol_nonce() {
244            Ok(nonce) => self.connect(nonce),
245            Err(cmd) => self.inner.as_mut().expect("initialized").send(cmd).await,
246        }
247    }
248
249    fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll<Result<Option<C::Response>, Error>> {
250        match self.inner.as_mut() {
251            Some(client) => client.poll_recv(cx),
252            None => Poll::Pending,
253        }
254    }
255}
256
257/// Specification for a Timely cluster to which a [`ClusterClient`] connects.
258///
259/// This trait is used to make the [`ClusterClient`] generic over the compute and storage cluster
260/// implementations.
261#[async_trait]
262pub trait ClusterSpec: Clone + Send + Sync + 'static {
263    /// The cluster command type.
264    type Command: fmt::Debug + Send + TryIntoProtocolNonce;
265    /// The cluster response type.
266    type Response: fmt::Debug + Send;
267
268    /// The name of this cluster ("compute" or "storage").
269    const NAME: &str;
270
271    /// Run the given Timely worker.
272    fn run_worker(
273        &self,
274        timely_worker: &mut TimelyWorker,
275        client_rx: mpsc::UnboundedReceiver<(
276            Uuid,
277            mpsc::UnboundedReceiver<Self::Command>,
278            mpsc::UnboundedSender<Self::Response>,
279        )>,
280    );
281
282    /// Build a Timely cluster using the given config.
283    async fn build_cluster(
284        &self,
285        config: TimelyConfig,
286        tokio_executor: Handle,
287    ) -> Result<TimelyContainer<Self>, Error> {
288        info!("Building timely container with config {config:?}");
289        let (client_txs, client_rxs): (Vec<_>, Vec<_>) = (0..config.workers)
290            .map(|_| mpsc::unbounded_channel())
291            .unzip();
292        let client_rxs: Mutex<Vec<_>> = Mutex::new(client_rxs.into_iter().map(Some).collect());
293
294        let refill = if config.enable_zero_copy_lgalloc {
295            BytesRefill {
296                logic: Arc::new(|size| Box::new(alloc::lgalloc_refill(size))),
297                limit: config.zero_copy_limit,
298            }
299        } else {
300            BytesRefill {
301                logic: Arc::new(|size| Box::new(vec![0; size])),
302                limit: config.zero_copy_limit,
303            }
304        };
305
306        let (builders, other) = initialize_networking(
307            config.workers,
308            config.process,
309            config.addresses.clone(),
310            refill,
311            config.enable_zero_copy,
312        )
313        .await?;
314
315        let mut worker_config = WorkerConfig::default();
316
317        // We set a custom exertion logic for proportionality > 0. A proportionality value of 0
318        // means that no arrangement merge effort is exerted and merging occurs only in response to
319        // updates.
320        if config.arrangement_exert_proportionality > 0 {
321            let merge_effort = Some(1000);
322
323            // ExertionLogic defines a function to determine if a spine is sufficiently tidied.
324            // Its arguments are an iterator over the index of a layer, the count of batches in the
325            // layer and the length of batches at the layer. The iterator enumerates layers from the
326            // largest to the smallest layer.
327
328            let arc: ExertionLogic = Arc::new(move |layers| {
329                EXERT_POLICY_CALLS.fetch_add(1, Ordering::Relaxed);
330                let mut prop = config.arrangement_exert_proportionality;
331
332                // Layers are ordered from largest to smallest.
333                // Skip to the largest occupied layer.
334                let layers = layers
335                    .iter()
336                    .copied()
337                    .skip_while(|(_idx, count, _len)| *count == 0);
338
339                let mut first = true;
340                for (_idx, count, len) in layers {
341                    if count > 1 {
342                        // Found an in-progress merge that we should continue.
343                        EXERT_POLICY_MERGE_GRANTS.fetch_add(1, Ordering::Relaxed);
344                        return merge_effort;
345                    }
346
347                    if !first && prop > 0 && len > 0 {
348                        // Found a non-empty batch within `arrangement_exert_proportionality` of
349                        // the largest one.
350                        EXERT_POLICY_CONSOLIDATION_GRANTS.fetch_add(1, Ordering::Relaxed);
351                        return merge_effort;
352                    }
353
354                    first = false;
355                    prop /= 2;
356                }
357
358                None
359            });
360            worker_config.set::<ExertionLogic>("differential/default_exert_logic".to_string(), arc);
361        }
362
363        let spec = self.clone();
364        let worker_guards = execute_from(builders, other, worker_config, move |timely_worker| {
365            let worker_idx = timely_worker.index();
366
367            // Per worker tracing span, lets us identify Timely clusters and workers in the logs.
368            let span = info_span!("timely", name = Self::NAME, worker_id = worker_idx);
369            let _span_guard = span.enter();
370
371            // Every Timely instance in this process names its threads `timely:work-N`, restarting
372            // the index at 0, so storage and compute worker threads collide under the same OS
373            // thread name. Rename to disambiguate them for profilers and `top -H`.
374            mz_ore::process::set_current_thread_name(&format!("{}:{worker_idx}", Self::NAME));
375
376            let _tokio_guard = tokio_executor.enter();
377            let client_rx = client_rxs.lock().unwrap()[worker_idx % config.workers]
378                .take()
379                .unwrap();
380            spec.run_worker(timely_worker, client_rx);
381        })
382        .map_err(|e| anyhow!(e))?;
383
384        Ok(TimelyContainer {
385            client_txs,
386            worker_guards,
387        })
388    }
389}
390
391// Evaluations of the exertion policy installed above, process-wide across
392// every timely runtime and trace implementation. A spine evaluates the policy
393// both when exerted and after each insert, so evaluations and grants count
394// decisions, not applications of effort. With
395// `arrangement_exert_proportionality` at zero no policy is installed and the
396// counts stay zero.
397static EXERT_POLICY_CALLS: AtomicU64 = AtomicU64::new(0);
398static EXERT_POLICY_MERGE_GRANTS: AtomicU64 = AtomicU64::new(0);
399static EXERT_POLICY_CONSOLIDATION_GRANTS: AtomicU64 = AtomicU64::new(0);
400
401/// Register gauges for the arrangement exertion policy's decisions.
402///
403/// The counters are process-wide, so only the first call registers them, and
404/// they cover every timely runtime in the process, not only the caller's.
405pub fn register_exert_policy_metrics(registry: &MetricsRegistry) {
406    static REGISTERED: OnceLock<()> = OnceLock::new();
407    REGISTERED.get_or_init(|| {
408        let _: ComputedUIntGauge = registry.register_computed_gauge(
409            metric!(name: "mz_arrangement_exert_policy_calls_total", help: "Arrangement exertion policy evaluations."),
410            || EXERT_POLICY_CALLS.load(Ordering::Relaxed),
411        );
412        for (reason, grants) in [
413            ("active_merge", &EXERT_POLICY_MERGE_GRANTS),
414            ("consolidation", &EXERT_POLICY_CONSOLIDATION_GRANTS),
415        ] {
416            let _: ComputedUIntGauge = registry.register_computed_gauge(
417                metric!(name: "mz_arrangement_exert_policy_grants_total", help: "Arrangement exertion policy evaluations that returned effort, by reason.", const_labels: {"reason" => reason}),
418                move || grants.load(Ordering::Relaxed),
419            );
420        }
421    });
422}
423
424mod alloc {
425    /// A Timely communication refill function that uses lgalloc.
426    ///
427    /// Returns a handle to lgalloc'ed memory if lgalloc can handle
428    /// the request, otherwise we fall back to a heap allocation. In either case, the handle must
429    /// be dropped to free the memory.
430    pub(crate) fn lgalloc_refill(size: usize) -> LgallocHandle {
431        match lgalloc::allocate::<u8>(size) {
432            Ok((pointer, capacity, handle)) => {
433                let handle = Some(handle);
434                LgallocHandle {
435                    handle,
436                    pointer,
437                    capacity,
438                }
439            }
440            Err(_) => {
441                // Allocate memory
442                let mut alloc = vec![0_u8; size];
443                // Ensure that the length matches the capacity.
444                alloc.shrink_to_fit();
445                // Get a pointer to the allocated memory.
446                let pointer = std::ptr::NonNull::new(alloc.as_mut_ptr()).unwrap();
447                // Forget the vector to avoid dropping it. We'll free the memory in `drop`.
448                std::mem::forget(alloc);
449                LgallocHandle {
450                    handle: None,
451                    pointer,
452                    capacity: size,
453                }
454            }
455        }
456    }
457
458    /// A handle to memory allocated by lgalloc. This can either be memory allocated by lgalloc or
459    /// memory allocated by Vec. If the handle is set, it's lgalloc memory. If the handle is None,
460    /// it's a regular heap allocation.
461    pub(crate) struct LgallocHandle {
462        /// Lgalloc handle, set if the memory was allocated by lgalloc.
463        handle: Option<lgalloc::Handle>,
464        /// Pointer to the allocated memory. Always well-aligned, but can be dangling.
465        pointer: std::ptr::NonNull<u8>,
466        /// Capacity of the allocated memory in bytes.
467        capacity: usize,
468    }
469
470    // SAFETY: `LgallocHandle` exclusively owns its allocation (either an
471    // lgalloc handle or a leaked `Vec`), and the `NonNull<u8>` is just a pointer
472    // into that owned memory. Transferring the handle transfers ownership, so it
473    // is safe to send across threads. Required because timely's `BytesRefill`
474    // produces `Box<dyn DerefMut<Target=[u8]> + Send>`.
475    unsafe impl Send for LgallocHandle {}
476
477    impl std::ops::Deref for LgallocHandle {
478        type Target = [u8];
479        #[inline(always)]
480        fn deref(&self) -> &Self::Target {
481            unsafe { std::slice::from_raw_parts(self.pointer.as_ptr(), self.capacity) }
482        }
483    }
484
485    impl std::ops::DerefMut for LgallocHandle {
486        #[inline(always)]
487        fn deref_mut(&mut self) -> &mut Self::Target {
488            unsafe { std::slice::from_raw_parts_mut(self.pointer.as_ptr(), self.capacity) }
489        }
490    }
491
492    impl Drop for LgallocHandle {
493        fn drop(&mut self) {
494            // If we have a handle, it's lgalloc memory. Otherwise, it's a heap allocation.
495            if let Some(handle) = self.handle.take() {
496                lgalloc::deallocate(handle);
497            } else {
498                unsafe { Vec::from_raw_parts(self.pointer.as_ptr(), 0, self.capacity) };
499            }
500            // Update pointer and capacity such that we don't double-free.
501            self.pointer = std::ptr::NonNull::dangling();
502            self.capacity = 0;
503        }
504    }
505}
506
507#[cfg(test)]
508mod exert_policy_metrics_tests {
509    use super::*;
510
511    /// Registration is guarded process-wide, so under a test runner that
512    /// shares the process another test may have registered first, into its
513    /// own registry. Either way a second call must not panic, and this
514    /// registry holds both families or neither.
515    #[mz_ore::test]
516    fn registering_twice_does_not_panic() {
517        let registry = MetricsRegistry::new();
518        register_exert_policy_metrics(&registry);
519        register_exert_policy_metrics(&registry);
520        let names: Vec<_> = registry
521            .gather()
522            .into_iter()
523            .map(|family| family.name().to_string())
524            .filter(|name| name.starts_with("mz_arrangement_exert_policy"))
525            .collect();
526        assert!(matches!(names.len(), 0 | 2), "{names:?}");
527    }
528}