1use std::fmt;
16use std::task::{Context, Poll};
17
18use itertools::Itertools;
19use tracing::trace;
20
21pub trait GenericClient<C, R>: fmt::Debug + Send {
24 fn send(&mut self, cmd: C) -> impl Future<Output = Result<(), anyhow::Error>> + Send;
28
29 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 fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll<Result<Option<R>, anyhow::Error>>;
56}
57
58#[derive(Debug)]
65pub struct Partitioned<P, C, R>
66where
67 (C, R): Partitionable<C, R>,
68{
69 pub parts: Vec<P>,
71 state: <(C, R) as Partitionable<C, R>>::PartitionedState,
73 next_part: usize,
75}
76
77impl<P, C, R> Partitioned<P, C, R>
78where
79 (C, R): Partitionable<C, R>,
80{
81 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 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 return Poll::Ready(Ok(None));
138 }
139 if !received {
140 return Poll::Pending;
142 }
143 }
144 }
145}
146
147pub trait Partitionable<C, R> {
150 type PartitionedState: PartitionedState<C, R>;
152
153 fn new(parts: usize) -> Self::PartitionedState;
155}
156
157pub trait PartitionedState<C, R>: fmt::Debug + Send {
160 fn split_command(&mut self, command: C) -> Vec<Option<C>>;
162
163 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 #[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 #[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}