Skip to main content

mz_service/
transport.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//! The Cluster Transport Protocol (CTP).
11//!
12//! CTP is the protocol used to transmit commands from controllers to replicas and responses from
13//! replicas to controllers. It runs on top of a reliable bidirectional connection stream, as
14//! provided by TCP or UDS, and adds message framing as well as heartbeating.
15//!
16//! CTP supports any message type that implements the serde [`Serialize`] and [`Deserialize`]
17//! traits. Messages are encoded using the [`bincode`] format and then sent over the wire with a
18//! length prefix.
19//!
20//! A CTP server only serves a single client at a time. If a new client connects while a connection
21//! is already established, the previous connection is canceled.
22
23mod metrics;
24#[cfg(test)]
25mod tests;
26
27use std::convert::Infallible;
28use std::fmt::Debug;
29use std::task::{Context, Poll, ready};
30use std::time::Duration;
31
32use anyhow::{anyhow, bail};
33use bincode::Options;
34use futures::future;
35use mz_ore::cast::CastInto;
36use mz_ore::netio::{Listener, SocketAddr, Stream, TimedReader, TimedWriter};
37use mz_ore::task::{AbortOnDropHandle, JoinHandle};
38use semver::Version;
39use serde::de::DeserializeOwned;
40use serde::{Deserialize, Serialize};
41use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
42use tokio::sync::{mpsc, oneshot, watch};
43use tracing::{Instrument, info, info_span, trace, warn};
44
45use crate::client::{GenericClient, Partitionable, Partitioned};
46
47pub use metrics::{ClusterServerMetrics, Metrics, NoopMetrics, PerClusterServerMetrics};
48
49/// Trait for messages that can be sent over CTP.
50pub trait Message: Debug + Send + Sync + Serialize + DeserializeOwned + 'static {}
51impl<T: Debug + Send + Sync + Serialize + DeserializeOwned + 'static> Message for T {}
52
53/// A client for a CTP connection.
54#[derive(Debug)]
55pub struct Client<Out, In> {
56    conn: Connection<Out, In>,
57}
58
59impl<Out: Message, In: Message> Client<Out, In> {
60    /// Connect to the server at the given address.
61    ///
62    /// This call resolves once a connection with the server host was either established, was
63    /// rejected, or timed out.
64    pub async fn connect(
65        address: &str,
66        version: Version,
67        connect_timeout: Duration,
68        idle_timeout: Duration,
69        metrics: impl Metrics<Out, In>,
70    ) -> anyhow::Result<Self> {
71        let dest_host = host_from_address(address);
72        let stream = mz_ore::future::timeout(connect_timeout, Stream::connect(address)).await?;
73        info!(%address, "ctp: connected to server");
74
75        // The connection tasks inherit this span, so their log lines name the peer. A controller
76        // does not otherwise put its replica in scope, and under Kubernetes the address names the
77        // cluster, replica, generation, and process.
78        let span = info_span!("ctp", %address);
79        let conn = Connection::start(stream, version, dest_host, idle_timeout, metrics)
80            .instrument(span)
81            .await?;
82        Ok(Self { conn })
83    }
84}
85
86/// Helper function to extract the host part from an address string.
87///
88/// This function assumes addresses to be of the form `<host>:<port>` or `<protocol>:<host>:<port>`
89/// and yields `None` otherwise.
90fn host_from_address(address: &str) -> Option<String> {
91    let mut p = address.split(':');
92    let (host, port) = match (p.next(), p.next(), p.next(), p.next()) {
93        (Some(host), Some(port), None, None) => (host, port),
94        (Some(_protocol), Some(host), Some(port), None) => (host, port),
95        _ => return None,
96    };
97
98    let _: u16 = port.parse().ok()?;
99    Some(host.into())
100}
101
102impl<Out, In> Client<Out, In>
103where
104    Out: Message,
105    In: Message,
106    (Out, In): Partitionable<Out, In>,
107{
108    /// Create a `Partitioned` client that connects through CTP.
109    pub async fn connect_partitioned(
110        addresses: Vec<String>,
111        version: Version,
112        connect_timeout: Duration,
113        idle_timeout: Duration,
114        metrics: impl Metrics<Out, In>,
115    ) -> anyhow::Result<Partitioned<Self, Out, In>> {
116        let connects = addresses.iter().map(|addr| {
117            Self::connect(
118                addr,
119                version.clone(),
120                connect_timeout,
121                idle_timeout,
122                metrics.clone(),
123            )
124        });
125        let clients = future::try_join_all(connects).await?;
126        Ok(Partitioned::new(clients))
127    }
128}
129
130impl<Out: Message, In: Message> GenericClient<Out, In> for Client<Out, In> {
131    async fn send(&mut self, cmd: Out) -> anyhow::Result<()> {
132        self.conn.send(cmd).await
133    }
134
135    fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll<anyhow::Result<Option<In>>> {
136        self.conn.poll_recv(cx).map_ok(Some)
137    }
138}
139
140/// Spawn a CTP server that serves connections at the given address.
141pub async fn serve<In, Out, H>(
142    address: SocketAddr,
143    version: Version,
144    server_fqdn: Option<String>,
145    idle_timeout: Duration,
146    handler_fn: impl Fn() -> H,
147    metrics: impl Metrics<Out, In>,
148) -> anyhow::Result<()>
149where
150    In: Message,
151    Out: Message,
152    H: GenericClient<In, Out> + 'static,
153{
154    // Keep a handle to the task serving the current connection, as well as a cancelation token, so
155    // we can cancel it when a new client connects.
156    //
157    // Note that we cannot simply abort the previous connection task because its future isn't known
158    // to be cancel safe. Instead we pass the connection tasks a cancelation token and wait for
159    // them to shut themselves down gracefully once the token gets dropped.
160    let mut connection_task: Option<(JoinHandle<()>, oneshot::Sender<()>)> = None;
161
162    let listener = Listener::bind(&address).await?;
163    info!(%address, "ctp: listening for client connections");
164
165    loop {
166        let (stream, peer) = listener.accept().await?;
167        info!(%peer, "ctp: accepted client connection");
168
169        // Cancel any existing connection before starting to serve the new one.
170        if let Some((task, token)) = connection_task.take() {
171            drop(token);
172            task.await;
173        }
174
175        let handler = handler_fn();
176        let version = version.clone();
177        let server_fqdn = server_fqdn.clone();
178        let metrics = metrics.clone();
179        let (cancel_tx, cancel_rx) = oneshot::channel();
180
181        let span = tracing::Span::current();
182        let handle = mz_ore::task::spawn(
183            || "ctp::connection",
184            async move {
185                let Err(error) = serve_connection(
186                    stream,
187                    handler,
188                    version,
189                    server_fqdn,
190                    idle_timeout,
191                    cancel_rx,
192                    metrics,
193                )
194                .await;
195                info!("ctp: connection failed: {error}");
196            }
197            .instrument(span),
198        );
199
200        connection_task = Some((handle, cancel_tx));
201    }
202}
203
204/// Serve a single CTP connection.
205async fn serve_connection<In, Out, H>(
206    stream: Stream,
207    mut handler: H,
208    version: Version,
209    server_fqdn: Option<String>,
210    timeout: Duration,
211    cancel_rx: oneshot::Receiver<()>,
212    metrics: impl Metrics<Out, In>,
213) -> anyhow::Result<Infallible>
214where
215    In: Message,
216    Out: Message,
217    H: GenericClient<In, Out>,
218{
219    let mut conn = Connection::start(stream, version, server_fqdn, timeout, metrics).await?;
220
221    let mut cancel_rx = cancel_rx;
222    loop {
223        tokio::select! {
224            // `Connection::recv` is documented to be cancel safe.
225            inbound = conn.recv() => {
226                let msg = inbound?;
227                handler.send(msg).await?;
228            },
229            // Poll the handler directly, because `GenericClient::recv` allocates a new future on
230            // every loop iteration.
231            outbound = future::poll_fn(|cx| handler.poll_recv(cx)) => match outbound? {
232                Some(msg) => conn.send(msg).await?,
233                None => bail!("client disconnected"),
234            },
235            _ = &mut cancel_rx => bail!("connection canceled"),
236        }
237    }
238}
239
240/// An active CTP connection.
241///
242/// This type encapsulates the core connection logic. It is used by both the client and the server
243/// implementation, with swapped `Out`/`In` types.
244///
245/// Each connection spawns two tasks:
246///
247///  * The send task is responsible for encoding and sending enqueued messages.
248///  * The recv task is responsible for receiving and decoding messages from the peer.
249///
250/// The separation into tasks provides some performance isolation between the sending and the
251/// receiving half of the connection.
252#[derive(Debug)]
253struct Connection<Out, In> {
254    /// Message sender connected to the send task.
255    msg_tx: mpsc::UnboundedSender<Out>,
256    /// Message receiver connected to the receive task.
257    msg_rx: mpsc::UnboundedReceiver<In>,
258    /// Receiver for errors encountered by connection tasks.
259    error_rx: ErrorRx,
260
261    /// Handles to connection tasks.
262    _tasks: [AbortOnDropHandle<()>; 2],
263}
264
265impl<Out: Message, In: Message> Connection<Out, In> {
266    /// The interval with which keepalives are emitted on idle connections.
267    const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
268    /// The minimum acceptable idle timeout.
269    ///
270    /// We want this to be significantly greater than `KEEPALIVE_INTERVAL`, to avoid connections
271    /// getting canceled unnecessarily.
272    const MIN_TIMEOUT: Duration = Duration::from_secs(2);
273
274    /// Start a new connection wrapping the given stream.
275    async fn start(
276        stream: Stream,
277        version: Version,
278        server_fqdn: Option<String>,
279        mut timeout: Duration,
280        metrics: impl Metrics<Out, In>,
281    ) -> anyhow::Result<Self> {
282        if timeout < Self::MIN_TIMEOUT {
283            warn!(
284                ?timeout,
285                "ctp: configured timeout is less than minimum timeout",
286            );
287            timeout = Self::MIN_TIMEOUT;
288        }
289
290        let (reader, writer) = stream.split();
291
292        // Apply the timeout to all connection reads and writes.
293        let reader = TimedReader::new(reader, timeout);
294        let writer = TimedWriter::new(writer, timeout);
295        // Track byte count metrics for all connection reads and writes.
296        let mut reader = metrics::Reader::new(reader, metrics.clone());
297        let mut writer = metrics::Writer::new(writer, metrics.clone());
298
299        handshake(&mut reader, &mut writer, version, server_fqdn).await?;
300
301        let (out_tx, out_rx) = mpsc::unbounded_channel();
302        let (in_tx, in_rx) = mpsc::unbounded_channel();
303        let (error_tx, error_rx) = error_channel();
304
305        let span = tracing::Span::current();
306        let send_task = mz_ore::task::spawn(
307            || "ctp::send",
308            Self::run_send_task(writer, out_rx, error_tx.clone(), metrics.clone())
309                .instrument(span.clone()),
310        );
311        let recv_task = mz_ore::task::spawn(
312            || "ctp::recv",
313            Self::run_recv_task(reader, in_tx, error_tx, metrics).instrument(span),
314        );
315
316        Ok(Self {
317            msg_tx: out_tx,
318            msg_rx: in_rx,
319            error_rx,
320            _tasks: [send_task.abort_on_drop(), recv_task.abort_on_drop()],
321        })
322    }
323
324    /// Enqueue a message for sending.
325    async fn send(&mut self, msg: Out) -> anyhow::Result<()> {
326        match self.msg_tx.send(msg) {
327            Ok(()) => Ok(()),
328            Err(_) => bail!(self.error_rx.collect().await),
329        }
330    }
331
332    /// Return a received message.
333    ///
334    /// # Cancel safety
335    ///
336    /// This method is cancel safe.
337    async fn recv(&mut self) -> anyhow::Result<In> {
338        future::poll_fn(|cx| self.poll_recv(cx)).await
339    }
340
341    /// Poll for a received message.
342    fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll<anyhow::Result<In>> {
343        match ready!(self.msg_rx.poll_recv(cx)) {
344            Some(msg) => Poll::Ready(Ok(msg)),
345            // Only the recv task holds the sender. It reports its error before it drops the
346            // sender, so the error is available without waiting.
347            None => Poll::Ready(Err(anyhow!(self.error_rx.reported()))),
348        }
349    }
350
351    /// Run a connection's send task.
352    async fn run_send_task<W: AsyncWrite + Unpin>(
353        mut writer: W,
354        mut msg_rx: mpsc::UnboundedReceiver<Out>,
355        error_tx: ErrorTx,
356        mut metrics: impl Metrics<Out, In>,
357    ) {
358        loop {
359            let msg = tokio::select! {
360                // `mpsc::UnboundedReceiver::recv` is cancel safe.
361                msg = msg_rx.recv() => match msg {
362                    Some(msg) => {
363                        trace!(?msg, "ctp: sending message");
364                        Some(msg)
365                    }
366                    None => break,
367                },
368                // `tokio::time::sleep` is cancel safe.
369                _ = tokio::time::sleep(Self::KEEPALIVE_INTERVAL) => {
370                    trace!("ctp: sending keepalive");
371                    None
372                },
373            };
374
375            if let Err(error) = write_message(&mut writer, msg.as_ref()).await {
376                warn!("ctp: send error: {error}");
377                error_tx.report(format!("send error: {error}"));
378                break;
379            };
380
381            if let Some(msg) = &msg {
382                metrics.message_sent(msg);
383            }
384        }
385    }
386
387    /// Run a connection's recv task.
388    async fn run_recv_task<R: AsyncRead + Unpin>(
389        mut reader: R,
390        msg_tx: mpsc::UnboundedSender<In>,
391        error_tx: ErrorTx,
392        mut metrics: impl Metrics<Out, In>,
393    ) {
394        loop {
395            match read_message(&mut reader).await {
396                Ok(msg) => {
397                    trace!(?msg, "ctp: received message");
398                    metrics.message_received(&msg);
399
400                    if msg_tx.send(msg).is_err() {
401                        break;
402                    }
403                }
404                Err(error) => {
405                    warn!("ctp: recv error: {error}");
406                    error_tx.report(format!("recv error: {error}"));
407                    break;
408                }
409            };
410        }
411    }
412}
413
414/// Error reported when a connection was closed without either task reporting an error.
415const CONNECTION_CLOSED: &str = "connection closed";
416
417/// Create a channel for reporting errors encountered by a connection's tasks.
418fn error_channel() -> (ErrorTx, ErrorRx) {
419    let (tx, rx) = watch::channel(None);
420    (ErrorTx(tx), ErrorRx(rx))
421}
422
423/// The sending half of a connection's error channel.
424#[derive(Clone, Debug)]
425struct ErrorTx(watch::Sender<Option<String>>);
426
427impl ErrorTx {
428    /// Report an error, unless an error was reported before.
429    fn report(&self, error: String) {
430        // The first error wins. A broken connection usually makes both tasks fail in sequence,
431        // with the first failure causing the second, so the first error is the one that explains
432        // what happened. For example, when the send task hits its idle deadline it drops its write
433        // half, whereupon the peer closes the connection and the recv task observes an EOF.
434        self.0.send_if_modified(|slot| match slot {
435            Some(_) => false,
436            None => {
437                *slot = Some(error);
438                true
439            }
440        });
441    }
442}
443
444/// The receiving half of a connection's error channel.
445#[derive(Debug)]
446struct ErrorRx(watch::Receiver<Option<String>>);
447
448impl ErrorRx {
449    /// Return the first error reported on this channel.
450    ///
451    /// If all [`ErrorTx`]s are dropped without an error being reported, returns
452    /// [`CONNECTION_CLOSED`]. Repeated calls return the same error.
453    async fn collect(&mut self) -> String {
454        // Wait for the first error to be reported, or for all connection tasks to shut down.
455        let _ = self.0.changed().await;
456        // Mark the current value as unseen, so the next `collect` call can return immediately.
457        self.0.mark_changed();
458
459        self.reported()
460    }
461
462    /// Return the first error reported on this channel so far, or [`CONNECTION_CLOSED`] if none
463    /// was.
464    fn reported(&self) -> String {
465        let error = self.0.borrow().clone();
466        error.unwrap_or_else(|| CONNECTION_CLOSED.into())
467    }
468}
469
470/// Perform the CTP handshake.
471///
472/// To perform the handshake, each endpoint sends the protocol magic number, followed by a
473/// `Hello` message. The `Hello` message contains information about the originating endpoint that
474/// is used by the receiver to validate compatibility with its peer. Only if both endpoints
475/// determine that they are compatible does the handshake succeed.
476async fn handshake<R, W>(
477    mut reader: R,
478    mut writer: W,
479    version: Version,
480    server_fqdn: Option<String>,
481) -> anyhow::Result<()>
482where
483    R: AsyncRead + Unpin,
484    W: AsyncWrite + Unpin,
485{
486    /// A randomly chosen magic number identifying CTP connections.
487    const MAGIC: u64 = 0x477574656e546167;
488
489    writer.write_u64(MAGIC).await?;
490
491    let hello = Hello {
492        version: version.clone(),
493        server_fqdn: server_fqdn.clone(),
494    };
495    write_message(&mut writer, Some(&hello)).await?;
496
497    let peer_magic = reader.read_u64().await?;
498    if peer_magic != MAGIC {
499        bail!("invalid protocol magic: {peer_magic:#x}");
500    }
501
502    let Hello {
503        version: peer_version,
504        server_fqdn: peer_server_fqdn,
505    } = read_message(&mut reader).await?;
506
507    if peer_version != version {
508        bail!("version mismatch: {peer_version} != {version}");
509    }
510    if let (Some(other), Some(mine)) = (&peer_server_fqdn, &server_fqdn) {
511        if other != mine {
512            bail!("server FQDN mismatch: {other} != {mine}");
513        }
514    }
515
516    Ok(())
517}
518
519/// A message for exchanging compatibility information during the CTP handshake.
520#[derive(Debug, Serialize, Deserialize)]
521struct Hello {
522    /// The version of the originating endpoint.
523    version: Version,
524    /// The FQDN of the server endpoint.
525    server_fqdn: Option<String>,
526}
527
528/// Write a message into the given writer.
529///
530/// The message can be `None`, in which case an empty message is written. This is used to implement
531/// keepalives. At the receiver, empty messages are ignored, but they do reset the read timeout.
532async fn write_message<W, M>(mut writer: W, msg: Option<&M>) -> anyhow::Result<()>
533where
534    W: AsyncWrite + Unpin,
535    M: Message,
536{
537    let bytes = match msg {
538        Some(msg) => &*wire_encode(msg)?,
539        None => &[],
540    };
541
542    let len = bytes.len().cast_into();
543    writer.write_u64(len).await?;
544    writer.write_all(bytes).await?;
545
546    Ok(())
547}
548
549/// Read a message from the given reader.
550async fn read_message<R, M>(mut reader: R) -> anyhow::Result<M>
551where
552    R: AsyncRead + Unpin,
553    M: Message,
554{
555    // Skip over any empty messages (i.e. keepalives).
556    let mut len = 0;
557    while len == 0 {
558        len = reader.read_u64().await?;
559    }
560
561    let mut bytes = vec![0; len.cast_into()];
562    reader.read_exact(&mut bytes).await?;
563
564    wire_decode(&bytes)
565}
566
567/// Encode a message for wire transport.
568fn wire_encode<M: Message>(msg: &M) -> anyhow::Result<Vec<u8>> {
569    let bytes = bincode::DefaultOptions::new().serialize(msg)?;
570    Ok(bytes)
571}
572
573/// Decode a wire frame back into a message.
574fn wire_decode<M: Message>(bytes: &[u8]) -> anyhow::Result<M> {
575    let msg = bincode::DefaultOptions::new().deserialize(bytes)?;
576    Ok(msg)
577}