Skip to main content

oxia/
streams.rs

1//! Streaming result types: ordered range scans / lists merged across shards,
2//! and the notification / sequence-update subscription handles.
3
4use crate::client::SubscriptionGuard;
5use crate::errors::OxiaError;
6use crate::key;
7use crate::types::{GetResult, Notification};
8use futures::Stream;
9use std::fmt;
10use std::pin::Pin;
11use std::task::{Context, Poll};
12use tokio::sync::mpsc;
13
14/// A boxed stream of results coming from a single shard.
15pub(crate) type ShardStream<T> = Pin<Box<dyn Stream<Item = Result<T, OxiaError>> + Send>>;
16
17/// Items that can be merged across shards in key order.
18pub(crate) trait MergeItem {
19    fn merge_key(&self) -> &str;
20}
21
22impl MergeItem for String {
23    fn merge_key(&self) -> &str {
24        self
25    }
26}
27
28impl MergeItem for GetResult {
29    fn merge_key(&self) -> &str {
30        &self.key
31    }
32}
33
34/// An ordered k-way merge over per-shard streams.
35///
36/// Each shard returns its results in Oxia's key order; holding at most one
37/// pending item per shard and always yielding the smallest keeps the global
38/// order identical to the server's while buffering O(shards) items.
39pub(crate) struct Merged<T> {
40    streams: Vec<ShardStream<T>>,
41    peeked: Vec<Option<T>>,
42    done: Vec<bool>,
43    finished: bool,
44}
45
46pub(crate) fn merged<T>(streams: Vec<ShardStream<T>>) -> Merged<T> {
47    let count = streams.len();
48    Merged {
49        streams,
50        peeked: (0..count).map(|_| None).collect(),
51        done: vec![false; count],
52        finished: false,
53    }
54}
55
56impl<T: MergeItem + Unpin> Stream for Merged<T> {
57    type Item = Result<T, OxiaError>;
58
59    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
60        let this = self.get_mut();
61        if this.finished {
62            return Poll::Ready(None);
63        }
64        // Fill every empty slot; a stream that is Pending keeps the whole merge
65        // Pending (its waker is registered), because yielding without knowing
66        // its next key could break the global order.
67        let mut pending = false;
68        for i in 0..this.streams.len() {
69            while this.peeked[i].is_none() && !this.done[i] {
70                match this.streams[i].as_mut().poll_next(cx) {
71                    Poll::Ready(Some(Ok(item))) => this.peeked[i] = Some(item),
72                    Poll::Ready(Some(Err(err))) => {
73                        this.finished = true;
74                        return Poll::Ready(Some(Err(err)));
75                    }
76                    Poll::Ready(None) => this.done[i] = true,
77                    Poll::Pending => {
78                        pending = true;
79                        break;
80                    }
81                }
82            }
83        }
84        if pending {
85            return Poll::Pending;
86        }
87        let mut min_idx: Option<usize> = None;
88        for i in 0..this.peeked.len() {
89            if let (Some(candidate), Some(m)) = (&this.peeked[i], min_idx) {
90                let current = this.peeked[m].as_ref().expect("min slot is filled");
91                if key::compare(candidate.merge_key(), current.merge_key()).is_lt() {
92                    min_idx = Some(i);
93                }
94            } else if this.peeked[i].is_some() && min_idx.is_none() {
95                min_idx = Some(i);
96            }
97        }
98        match min_idx {
99            Some(i) => Poll::Ready(Some(Ok(this.peeked[i].take().expect("slot is filled")))),
100            None => {
101                this.finished = true;
102                Poll::Ready(None)
103            }
104        }
105    }
106}
107
108/// An ordered stream of keys produced by
109/// [`ListBuilder::stream`](crate::ListBuilder::stream).
110///
111/// Yields keys in the server's global (slash-aware) key order, merged across
112/// shards while buffering at most one pending key per shard. The stream has
113/// no overall deadline; it is naturally backpressured — keys are pulled from
114/// the server only as fast as they are consumed. The first error is yielded
115/// as an `Err` item and terminates the stream. Dropping the stream cancels
116/// the underlying RPCs.
117pub struct ListStream {
118    inner: Merged<String>,
119}
120
121impl ListStream {
122    pub(crate) fn new(inner: Merged<String>) -> Self {
123        ListStream { inner }
124    }
125}
126
127impl Stream for ListStream {
128    type Item = Result<String, OxiaError>;
129
130    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
131        Pin::new(&mut self.inner).poll_next(cx)
132    }
133}
134
135impl fmt::Debug for ListStream {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.debug_struct("ListStream").finish_non_exhaustive()
138    }
139}
140
141/// An ordered stream of records produced by
142/// [`RangeScanBuilder::stream`](crate::RangeScanBuilder::stream).
143///
144/// Yields records in the server's global (slash-aware) key order, merged
145/// across shards while buffering at most one pending record per shard — a
146/// scan of any size runs in O(shards) memory. The stream has no overall
147/// deadline; it is naturally backpressured — records are pulled from the
148/// server only as fast as they are consumed. The first error is yielded as an
149/// `Err` item and terminates the stream. Dropping the stream cancels the
150/// underlying RPCs.
151pub struct RangeScanStream {
152    inner: Merged<GetResult>,
153}
154
155impl RangeScanStream {
156    pub(crate) fn new(inner: Merged<GetResult>) -> Self {
157        RangeScanStream { inner }
158    }
159}
160
161impl Stream for RangeScanStream {
162    type Item = Result<GetResult, OxiaError>;
163
164    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
165        Pin::new(&mut self.inner).poll_next(cx)
166    }
167}
168
169impl fmt::Debug for RangeScanStream {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        f.debug_struct("RangeScanStream").finish_non_exhaustive()
172    }
173}
174
175/// A subscription to database change notifications, created with
176/// [`OxiaClient::notifications`](crate::OxiaClient::notifications).
177///
178/// Consume it with [`recv`](Notifications::recv) or as a [`Stream`].
179/// Notifications from the same shard arrive in the order the changes were
180/// applied; interleaving across shards is unspecified. The subscription
181/// survives connection loss: each per-shard listener reconnects and resumes
182/// from its last delivered offset. Dropping the handle releases the
183/// subscription.
184pub struct Notifications {
185    rx: mpsc::Receiver<Notification>,
186    // Stops the per-shard listeners when this handle is dropped.
187    _guard: SubscriptionGuard,
188}
189
190impl Notifications {
191    pub(crate) fn new(rx: mpsc::Receiver<Notification>, guard: SubscriptionGuard) -> Self {
192        Notifications { rx, _guard: guard }
193    }
194
195    /// Receives the next notification.
196    ///
197    /// Returns `None` once the client is closed. Cancel-safe: dropping the
198    /// returned future loses no notification.
199    pub async fn recv(&mut self) -> Option<Notification> {
200        self.rx.recv().await
201    }
202}
203
204impl fmt::Debug for Notifications {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        f.debug_struct("Notifications").finish_non_exhaustive()
207    }
208}
209
210impl Stream for Notifications {
211    type Item = Notification;
212
213    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
214        self.rx.poll_recv(cx)
215    }
216}
217
218/// A subscription to sequence advances, created with
219/// [`OxiaClient::sequence_updates`](crate::OxiaClient::sequence_updates).
220///
221/// Yields the highest assigned sequence key each time the sequence advances;
222/// rapid advances may be coalesced into one delivery carrying the highest
223/// key. The subscription survives connection loss and re-subscribes
224/// automatically. Consume it with [`recv`](SequenceUpdates::recv) or as a
225/// [`Stream`]; dropping the handle releases the subscription.
226pub struct SequenceUpdates {
227    rx: mpsc::Receiver<String>,
228    // Stops the listener when this handle is dropped.
229    _guard: SubscriptionGuard,
230}
231
232impl SequenceUpdates {
233    pub(crate) fn new(rx: mpsc::Receiver<String>, guard: SubscriptionGuard) -> Self {
234        SequenceUpdates { rx, _guard: guard }
235    }
236
237    /// Receives the next sequence key.
238    ///
239    /// Returns `None` once the client is closed. Cancel-safe: dropping the
240    /// returned future loses no update.
241    pub async fn recv(&mut self) -> Option<String> {
242        self.rx.recv().await
243    }
244}
245
246impl fmt::Debug for SequenceUpdates {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        f.debug_struct("SequenceUpdates").finish_non_exhaustive()
249    }
250}
251
252impl Stream for SequenceUpdates {
253    type Item = String;
254
255    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
256        self.rx.poll_recv(cx)
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use futures::StreamExt;
264
265    fn shard(items: Vec<Result<String, OxiaError>>) -> ShardStream<String> {
266        Box::pin(futures::stream::iter(items))
267    }
268
269    fn keys(items: &[&str]) -> ShardStream<String> {
270        shard(items.iter().map(|s| Ok(s.to_string())).collect())
271    }
272
273    #[tokio::test]
274    async fn merges_in_key_order() {
275        let merged = merged(vec![
276            keys(&["a", "d", "e"]),
277            keys(&["b", "c", "f"]),
278            keys(&[]),
279        ]);
280        let out: Vec<String> = merged.map(|r| r.unwrap()).collect().await;
281        assert_eq!(out, vec!["a", "b", "c", "d", "e", "f"]);
282    }
283
284    #[tokio::test]
285    async fn merges_with_slash_order() {
286        // "a" sorts before "/" in Oxia's slash-aware order.
287        let merged = merged(vec![keys(&["/x"]), keys(&["a"])]);
288        let out: Vec<String> = merged.map(|r| r.unwrap()).collect().await;
289        assert_eq!(out, vec!["a", "/x"]);
290    }
291
292    #[tokio::test]
293    async fn error_terminates_stream() {
294        let merged = merged(vec![
295            shard(vec![Ok("a".to_string()), Err(OxiaError::Timeout)]),
296            keys(&["b", "c"]),
297        ]);
298        let out: Vec<Result<String, OxiaError>> = merged.collect().await;
299        assert!(matches!(out[0], Ok(ref k) if k == "a"));
300        assert!(out.iter().any(|r| r.is_err()));
301        // Nothing after the error.
302        let err_pos = out.iter().position(|r| r.is_err()).unwrap();
303        assert_eq!(err_pos, out.len() - 1);
304    }
305
306    #[tokio::test]
307    async fn empty_merge_ends() {
308        let merged = merged::<String>(vec![]);
309        let out: Vec<_> = merged.collect().await;
310        assert!(out.is_empty());
311    }
312}