Skip to main content

mz_service/
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//! Traits for client–server communication independent of transport layer.
11//!
12//! These traits are designed for servers where where commands must be sharded
13//! among several worker threads or processes.
14
15use std::fmt;
16use std::task::{Context, Poll};
17
18use itertools::Itertools;
19use tracing::trace;
20
21/// A generic client to a server that receives commands and asynchronously
22/// produces responses.
23pub trait GenericClient<C, R>: fmt::Debug + Send {
24    /// Sends a command to the dataflow server.
25    ///
26    /// The command can error for various reasons.
27    fn send(&mut self, cmd: C) -> impl Future<Output = Result<(), anyhow::Error>> + Send;
28
29    /// Receives the next response from the dataflow server.
30    ///
31    /// This method blocks until the next response is available.
32    ///
33    /// A return value of `Ok(Some(_))` transmits a response.
34    ///
35    /// A return value of `Ok(None)` indicates graceful termination of the
36    /// connection. The owner of the client should not call `recv` again.
37    ///
38    /// A return value of `Err(_)` indicates an unrecoverable error. After
39    /// observing an error, the owner of the client must drop the client.
40    ///
41    /// Implementations of this method **must** be [cancellation safe]. That
42    /// means that work must not be lost if the future returned by this method
43    /// is dropped.
44    ///
45    /// [cancellation safe]: https://docs.rs/tokio/latest/tokio/macro.select.html#cancellation-safety
46    fn recv(&mut self) -> impl Future<Output = Result<Option<R>, anyhow::Error>> + Send {
47        std::future::poll_fn(|cx| self.poll_recv(cx))
48    }
49
50    /// Polls for the next response from the dataflow server.
51    ///
52    /// Returns `Poll::Pending` and schedules `cx` for a wakeup if no response
53    /// is available. Ready values have the same meaning as for
54    /// [`GenericClient::recv`].
55    fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll<Result<Option<R>, anyhow::Error>>;
56}
57
58/// A client whose implementation is partitioned across a number of other
59/// clients.
60///
61/// Such a client needs to broadcast (partitioned) commands to all of its
62/// clients, and await responses from each of the client partitions before it
63/// can respond.
64#[derive(Debug)]
65pub struct Partitioned<P, C, R>
66where
67    (C, R): Partitionable<C, R>,
68{
69    /// The individual partitions representing per-worker clients.
70    pub parts: Vec<P>,
71    /// The partitioned state.
72    state: <(C, R) as Partitionable<C, R>>::PartitionedState,
73    /// The part that `poll_recv` polls first.
74    next_part: usize,
75}
76
77impl<P, C, R> Partitioned<P, C, R>
78where
79    (C, R): Partitionable<C, R>,
80{
81    /// Create a client partitioned across multiple client shards.
82    pub fn new(parts: Vec<P>) -> Self {
83        Self {
84            state: <(C, R) as Partitionable<C, R>>::new(parts.len()),
85            parts,
86            next_part: 0,
87        }
88    }
89}
90
91impl<P, C, R> GenericClient<C, R> for Partitioned<P, C, R>
92where
93    P: GenericClient<C, R>,
94    (C, R): Partitionable<C, R>,
95    C: fmt::Debug + Send,
96    R: fmt::Debug + Send,
97{
98    async fn send(&mut self, cmd: C) -> Result<(), anyhow::Error> {
99        trace!(command = ?cmd, "splitting command");
100        let cmd_parts = self.state.split_command(cmd);
101        for (index, (shard, cmd_part)) in self.parts.iter_mut().zip_eq(cmd_parts).enumerate() {
102            if let Some(cmd) = cmd_part {
103                trace!(shard = ?index, command = ?cmd, "sending command");
104                shard.send(cmd).await?;
105            }
106        }
107        Ok(())
108    }
109
110    fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll<Result<Option<R>, anyhow::Error>> {
111        let parts = self.parts.len();
112        loop {
113            let mut received = false;
114            let mut all_done = true;
115            // Start each pass after the part polled last, so that a part with a steady supply of
116            // responses cannot starve the others.
117            for _ in 0..parts {
118                let index = self.next_part;
119                self.next_part = (index + 1) % parts;
120                match self.parts[index].poll_recv(cx) {
121                    Poll::Pending => all_done = false,
122                    Poll::Ready(Ok(None)) => {}
123                    Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
124                    Poll::Ready(Ok(Some(response))) => {
125                        received = true;
126                        all_done = false;
127                        trace!(shard = ?index, response = ?response, "received response");
128                        if let Some(response) = self.state.absorb_response(index, response) {
129                            trace!(response = ?response, "returning response");
130                            return Poll::Ready(response.map(Some));
131                        }
132                    }
133                }
134            }
135            if all_done {
136                // Indicate completion of the communication.
137                return Poll::Ready(Ok(None));
138            }
139            if !received {
140                // Every part that is not done returned `Pending` and registered `cx`.
141                return Poll::Pending;
142            }
143        }
144    }
145}
146
147/// A trait for command–response pairs that can be partitioned across multiple
148/// workers via [`Partitioned`].
149pub trait Partitionable<C, R> {
150    /// The type which functions as the state machine for the partitioning.
151    type PartitionedState: PartitionedState<C, R>;
152
153    /// Construct a [`PartitionedState`] for the command–response pair.
154    fn new(parts: usize) -> Self::PartitionedState;
155}
156
157/// A state machine for a partitioned client that partitions commands across and
158/// amalgamates responses from multiple partitions.
159pub trait PartitionedState<C, R>: fmt::Debug + Send {
160    /// Splits a command into multiple partitions.
161    fn split_command(&mut self, command: C) -> Vec<Option<C>>;
162
163    /// Absorbs a response from a single partition.
164    ///
165    /// If responses from all partitions have been absorbed, returns an
166    /// amalgamated response.
167    fn absorb_response(&mut self, shard_id: usize, response: R)
168    -> Option<Result<R, anyhow::Error>>;
169}
170
171#[cfg(test)]
172mod tests {
173    use std::collections::VecDeque;
174
175    use anyhow::anyhow;
176    use futures::task::noop_waker;
177
178    use super::*;
179
180    /// A part that yields scripted responses, then `Ok(None)` if `done`, else `Pending`.
181    #[derive(Debug)]
182    struct ScriptedPart {
183        responses: VecDeque<Result<i32, String>>,
184        done: bool,
185    }
186
187    impl ScriptedPart {
188        fn new(responses: impl IntoIterator<Item = Result<i32, String>>, done: bool) -> Self {
189            Self {
190                responses: responses.into_iter().collect(),
191                done,
192            }
193        }
194    }
195
196    impl GenericClient<(), i32> for ScriptedPart {
197        async fn send(&mut self, _cmd: ()) -> Result<(), anyhow::Error> {
198            Ok(())
199        }
200
201        fn poll_recv(&mut self, _cx: &mut Context<'_>) -> Poll<Result<Option<i32>, anyhow::Error>> {
202            match self.responses.pop_front() {
203                Some(Ok(response)) => Poll::Ready(Ok(Some(response))),
204                Some(Err(error)) => Poll::Ready(Err(anyhow!(error))),
205                None if self.done => Poll::Ready(Ok(None)),
206                None => Poll::Pending,
207            }
208        }
209    }
210
211    /// Passes through responses, except that it swallows negative ones.
212    #[derive(Debug)]
213    pub struct PassthroughState;
214
215    impl PartitionedState<(), i32> for PassthroughState {
216        fn split_command(&mut self, _command: ()) -> Vec<Option<()>> {
217            unimplemented!()
218        }
219
220        fn absorb_response(
221            &mut self,
222            _shard_id: usize,
223            response: i32,
224        ) -> Option<Result<i32, anyhow::Error>> {
225            (response >= 0).then_some(Ok(response))
226        }
227    }
228
229    impl Partitionable<(), i32> for ((), i32) {
230        type PartitionedState = PassthroughState;
231
232        fn new(_parts: usize) -> PassthroughState {
233            PassthroughState
234        }
235    }
236
237    fn poll(client: &mut Partitioned<ScriptedPart, (), i32>) -> Poll<Result<Option<i32>, String>> {
238        let waker = noop_waker();
239        let mut cx = Context::from_waker(&waker);
240        client.poll_recv(&mut cx).map_err(|error| error.to_string())
241    }
242
243    #[mz_ore::test]
244    fn partitioned_poll_recv_round_robins_parts() {
245        let mut client = Partitioned::new(vec![
246            ScriptedPart::new([Ok(0), Ok(2), Ok(4)], false),
247            ScriptedPart::new([Ok(1), Ok(3)], false),
248        ]);
249        let received: Vec<_> = std::iter::from_fn(|| match poll(&mut client) {
250            Poll::Ready(response) => Some(response),
251            Poll::Pending => None,
252        })
253        .collect();
254        assert_eq!(
255            received,
256            vec![
257                Ok(Some(0)),
258                Ok(Some(1)),
259                Ok(Some(2)),
260                Ok(Some(3)),
261                Ok(Some(4))
262            ]
263        );
264    }
265
266    #[mz_ore::test]
267    fn partitioned_poll_recv_keeps_polling_after_absorbed_responses() {
268        let mut client = Partitioned::new(vec![
269            ScriptedPart::new([Ok(-1), Ok(-2)], false),
270            ScriptedPart::new([Ok(-3), Ok(7)], false),
271        ]);
272        assert_eq!(poll(&mut client), Poll::Ready(Ok(Some(7))));
273        assert_eq!(poll(&mut client), Poll::Pending);
274    }
275
276    #[mz_ore::test]
277    fn partitioned_poll_recv_completes_when_all_parts_complete() {
278        let mut client = Partitioned::new(vec![
279            ScriptedPart::new([], true),
280            ScriptedPart::new([Ok(5)], false),
281        ]);
282        assert_eq!(poll(&mut client), Poll::Ready(Ok(Some(5))));
283        assert_eq!(poll(&mut client), Poll::Pending, "one part still open");
284        client.parts[1].done = true;
285        assert_eq!(poll(&mut client), Poll::Ready(Ok(None)));
286        let mut empty = Partitioned::<ScriptedPart, (), i32>::new(vec![]);
287        assert_eq!(poll(&mut empty), Poll::Ready(Ok(None)));
288    }
289
290    #[mz_ore::test]
291    fn partitioned_poll_recv_returns_part_errors() {
292        let mut client = Partitioned::new(vec![
293            ScriptedPart::new([], false),
294            ScriptedPart::new([Err("boom".into())], false),
295        ]);
296        assert_eq!(poll(&mut client), Poll::Ready(Err("boom".into())));
297    }
298}