Skip to main content

klukai_client/
sub.rs

1use std::{
2    error::Error,
3    future::Future,
4    io,
5    net::SocketAddr,
6    pin::Pin,
7    task::{Context, Poll},
8    time::Duration,
9};
10
11type ResponseFuture = Pin<
12    Box<
13        dyn Future<Output = Result<hyper::Response<Incoming>, hyper_util::client::legacy::Error>>
14            + Send
15            + Sync,
16    >,
17>;
18
19use bytes::{Buf, Bytes, BytesMut};
20use futures::{Stream, ready};
21use http_body_util::Full;
22use hyper::body::{Body, Incoming};
23use hyper_util::client::legacy::{Client, connect::HttpConnector};
24use klukai_types::api::{ChangeId, QueryEvent, TypedNotifyEvent, TypedQueryEvent};
25use pin_project_lite::pin_project;
26use serde::de::DeserializeOwned;
27use tokio::time::{Sleep, sleep};
28use tokio_util::{
29    codec::{Decoder, FramedRead, LinesCodecError},
30    io::StreamReader,
31};
32use tracing::error;
33use uuid::Uuid;
34
35pin_project! {
36    pub struct IoBodyStream {
37        #[pin]
38        body: Incoming
39    }
40}
41
42impl Stream for IoBodyStream {
43    type Item = io::Result<Bytes>;
44
45    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
46        let this = self.project();
47
48        match ready!(this.body.poll_frame(cx)) {
49            Some(Ok(frame)) => {
50                if let Ok(data) = frame.into_data() {
51                    Poll::Ready(Some(Ok(data)))
52                } else {
53                    // Skip non-data frames, continue polling
54                    Poll::Pending
55                }
56            }
57            Some(Err(e)) => {
58                let io_err = match e
59                    .source()
60                    .and_then(|source| source.downcast_ref::<io::Error>())
61                {
62                    Some(io_err) => io::Error::from(io_err.kind()),
63                    None => io::Error::other(e),
64                };
65                Poll::Ready(Some(Err(io_err)))
66            }
67            None => Poll::Ready(None),
68        }
69    }
70}
71
72type IoBodyStreamReader = StreamReader<IoBodyStream, Bytes>;
73type FramedBody = FramedRead<IoBodyStreamReader, LinesBytesCodec>;
74
75pub struct SubscriptionStream<T> {
76    id: Uuid,
77    hash: Option<String>,
78    client: Client<HttpConnector, Full<Bytes>>,
79    api_addr: SocketAddr,
80    observed_eoq: bool,
81    last_change_id: Option<ChangeId>,
82    stream: Option<FramedBody>,
83    backoff: Option<Pin<Box<Sleep>>>,
84    backoff_count: u32,
85    response: Option<ResponseFuture>,
86    _deser: std::marker::PhantomData<T>,
87}
88
89#[derive(Debug, thiserror::Error)]
90pub enum SubscriptionError {
91    #[error(transparent)]
92    Io(#[from] io::Error),
93    #[error(transparent)]
94    Http(#[from] hyper::http::Error),
95    #[error(transparent)]
96    Deserialize(#[from] serde_json::Error),
97    #[error("missed a change (expected: {expected}, got: {got}), inconsistent state")]
98    MissedChange { expected: ChangeId, got: ChangeId },
99    #[error("max line length exceeded")]
100    MaxLineLengthExceeded,
101    #[error("initial query never finished")]
102    UnfinishedQuery,
103    #[error("max retry attempts exceeded")]
104    MaxRetryAttempts,
105}
106
107impl<T> SubscriptionStream<T>
108where
109    T: DeserializeOwned + Unpin,
110{
111    pub fn new(
112        id: Uuid,
113        hash: Option<String>,
114        client: Client<HttpConnector, Full<Bytes>>,
115        api_addr: SocketAddr,
116        body: Incoming,
117        change_id: Option<ChangeId>,
118    ) -> Self {
119        Self {
120            id,
121            hash,
122            client,
123            api_addr,
124            observed_eoq: change_id.is_some(),
125            last_change_id: change_id,
126            stream: Some(FramedRead::new(
127                StreamReader::new(IoBodyStream { body }),
128                LinesBytesCodec::default(),
129            )),
130            backoff: None,
131            backoff_count: 0,
132            response: None,
133            _deser: Default::default(),
134        }
135    }
136
137    pub fn id(&self) -> Uuid {
138        self.id
139    }
140
141    pub fn hash(&self) -> Option<&str> {
142        self.hash.as_deref()
143    }
144
145    pub fn api_addr(&self) -> SocketAddr {
146        self.api_addr
147    }
148
149    fn poll_stream(
150        mut self: Pin<&mut Self>,
151        cx: &mut Context<'_>,
152    ) -> Poll<Option<Result<TypedQueryEvent<T>, SubscriptionError>>> {
153        let stream = loop {
154            match self.stream.as_mut() {
155                None => match ready!(self.as_mut().poll_request(cx)) {
156                    Ok(stream) => {
157                        self.stream = Some(stream);
158                    }
159                    Err(e) => return Poll::Ready(Some(Err(e))),
160                },
161                Some(stream) => {
162                    break stream;
163                }
164            }
165        };
166
167        let res = ready!(Pin::new(stream).poll_next(cx));
168        match res {
169            Some(Ok(b)) => match serde_json::from_slice(&b) {
170                Ok(evt) => {
171                    if let TypedQueryEvent::EndOfQuery { change_id, .. } = &evt {
172                        self.handle_eoq(*change_id);
173                    }
174
175                    if let TypedQueryEvent::Change(_, _, _, change_id) = &evt
176                        && let Err(e) = self.handle_change(*change_id)
177                    {
178                        return Poll::Ready(Some(Err(e)));
179                    }
180
181                    Poll::Ready(Some(Ok(evt)))
182                }
183                Err(deser_err) => {
184                    // It failed to deserialize, try untyped variant to extract the metadata
185                    if let Ok(evt) = serde_json::from_slice::<QueryEvent>(&b) {
186                        if let TypedQueryEvent::EndOfQuery { change_id, .. } = &evt {
187                            self.handle_eoq(*change_id);
188                        }
189
190                        if let TypedQueryEvent::Change(_, _, _, change_id) = &evt
191                            && let Err(e) = self.handle_change(*change_id)
192                        {
193                            return Poll::Ready(Some(Err(e)));
194                        }
195                    }
196
197                    // But return the original error anyway (unless this is out-of-order event)
198                    Poll::Ready(Some(Err(deser_err.into())))
199                }
200            },
201            Some(Err(e)) => match e {
202                LinesCodecError::MaxLineLengthExceeded => {
203                    Poll::Ready(Some(Err(SubscriptionError::MaxLineLengthExceeded)))
204                }
205                LinesCodecError::Io(io_err) => Poll::Ready(Some(Err(io_err.into()))),
206            },
207            None => Poll::Ready(None),
208        }
209    }
210
211    fn handle_eoq(&mut self, change_id: Option<ChangeId>) {
212        self.observed_eoq = true;
213        self.last_change_id = change_id;
214    }
215
216    fn handle_change(&mut self, change_id: ChangeId) -> Result<(), SubscriptionError> {
217        match self.last_change_id {
218            Some(id) if id + 1 != change_id => {
219                return Err(SubscriptionError::MissedChange {
220                    expected: id + 1,
221                    got: change_id,
222                });
223            }
224            _ => (),
225        }
226
227        self.last_change_id = Some(change_id);
228
229        Ok(())
230    }
231
232    fn poll_request(
233        mut self: Pin<&mut Self>,
234        cx: &mut Context<'_>,
235    ) -> Poll<Result<FramedBody, SubscriptionError>> {
236        loop {
237            if let Some(res_fut) = self.response.as_mut() {
238                // return early w/ Poll::Pending if response is not ready
239                let res = ready!(Pin::new(res_fut).poll(cx));
240
241                // reset response
242                self.response = None;
243
244                return match res {
245                    Ok(res) => Poll::Ready(Ok(FramedRead::new(
246                        StreamReader::new(IoBodyStream {
247                            body: res.into_body(),
248                        }),
249                        LinesBytesCodec::default(),
250                    ))),
251                    Err(e) => {
252                        let io_err = match e
253                            .source()
254                            .and_then(|source| source.downcast_ref::<io::Error>())
255                        {
256                            Some(io_err) => io::Error::from(io_err.kind()),
257                            None => io::Error::other(e),
258                        };
259                        Poll::Ready(Err(io_err.into()))
260                    }
261                };
262            } else if self.observed_eoq {
263                let req = hyper::Request::builder()
264                    .method(hyper::Method::GET)
265                    .uri(format!(
266                        "http://{}/v1/subscriptions/{}?from={}",
267                        self.api_addr,
268                        self.id,
269                        self.last_change_id.unwrap_or_default()
270                    ))
271                    .header(hyper::header::ACCEPT, "application/json")
272                    .body(Full::new(Bytes::new()))?;
273
274                let response = Box::pin(self.client.request(req));
275                self.response = Some(response);
276                // loop around!
277            } else {
278                return Poll::Ready(Err(SubscriptionError::UnfinishedQuery));
279            }
280        }
281    }
282}
283
284impl<T> Stream for SubscriptionStream<T>
285where
286    T: DeserializeOwned + Unpin,
287{
288    type Item = Result<TypedQueryEvent<T>, SubscriptionError>;
289
290    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
291        // first, check if we need to wait for a backoff...
292        if let Some(backoff) = self.backoff.as_mut() {
293            ready!(backoff.as_mut().poll(cx));
294            self.backoff = None;
295        }
296
297        let io_err = match ready!(self.as_mut().poll_stream(cx)) {
298            Some(Err(SubscriptionError::Io(io_err))) => io_err,
299            other => {
300                self.backoff_count = 0;
301                return Poll::Ready(other);
302            }
303        };
304
305        // reset the stream
306        self.stream = None;
307
308        if self.backoff_count >= 10 {
309            return Poll::Ready(Some(Err(SubscriptionError::MaxRetryAttempts)));
310        }
311
312        error!("encountered a stream IO error: {io_err}, retrying in a bit");
313
314        let mut backoff = Box::pin(sleep(Duration::from_secs(1)));
315
316        // register w/ waker
317        _ = backoff.as_mut().poll(cx);
318
319        // this can't return Ready, right?
320        self.backoff = Some(backoff);
321
322        self.backoff_count += 1;
323
324        Poll::Pending
325    }
326}
327
328pub struct UpdatesStream<T> {
329    id: Uuid,
330    stream: FramedBody,
331    _deser: std::marker::PhantomData<T>,
332}
333
334#[derive(Debug, thiserror::Error)]
335pub enum UpdatesError {
336    #[error(transparent)]
337    Io(#[from] io::Error),
338    #[error(transparent)]
339    Deserialize(#[from] serde_json::Error),
340    #[error("max line length exceeded")]
341    MaxLineLengthExceeded,
342}
343
344impl<T> UpdatesStream<T>
345where
346    T: DeserializeOwned + Unpin,
347{
348    pub fn new(id: Uuid, body: Incoming) -> Self {
349        Self {
350            id,
351            stream: FramedRead::new(
352                StreamReader::new(IoBodyStream { body }),
353                LinesBytesCodec::default(),
354            ),
355            _deser: Default::default(),
356        }
357    }
358
359    pub fn id(&self) -> Uuid {
360        self.id
361    }
362}
363
364impl<T> Stream for UpdatesStream<T>
365where
366    T: DeserializeOwned + Unpin,
367{
368    type Item = Result<TypedNotifyEvent<T>, UpdatesError>;
369
370    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
371        let res = ready!(Pin::new(&mut self.stream).poll_next(cx));
372        match res {
373            Some(Ok(b)) => match serde_json::from_slice(&b) {
374                Ok(evt) => Poll::Ready(Some(Ok(evt))),
375                Err(e) => Poll::Ready(Some(Err(e.into()))),
376            },
377            Some(Err(e)) => match e {
378                LinesCodecError::MaxLineLengthExceeded => {
379                    Poll::Ready(Some(Err(UpdatesError::MaxLineLengthExceeded)))
380                }
381                LinesCodecError::Io(io_err) => Poll::Ready(Some(Err(io_err.into()))),
382            },
383            None => Poll::Ready(None),
384        }
385    }
386}
387
388pub struct QueryStream<T> {
389    stream: FramedBody,
390    _deser: std::marker::PhantomData<T>,
391}
392
393#[derive(Debug, thiserror::Error)]
394pub enum QueryError {
395    #[error(transparent)]
396    Io(#[from] io::Error),
397    #[error(transparent)]
398    Deserialize(#[from] serde_json::Error),
399    #[error("max line length exceeded")]
400    MaxLineLengthExceeded,
401}
402
403impl<T> QueryStream<T>
404where
405    T: DeserializeOwned + Unpin,
406{
407    pub fn new(body: Incoming) -> Self {
408        Self {
409            stream: FramedRead::new(
410                StreamReader::new(IoBodyStream { body }),
411                LinesBytesCodec::default(),
412            ),
413            _deser: Default::default(),
414        }
415    }
416}
417
418impl<T> Stream for QueryStream<T>
419where
420    T: DeserializeOwned + Unpin,
421{
422    type Item = Result<TypedQueryEvent<T>, QueryError>;
423
424    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
425        match ready!(Pin::new(&mut self.stream).poll_next(cx)) {
426            Some(Ok(b)) => match serde_json::from_slice(&b) {
427                Ok(evt) => Poll::Ready(Some(Ok(evt))),
428                Err(e) => Poll::Ready(Some(Err(e.into()))),
429            },
430            Some(Err(e)) => match e {
431                LinesCodecError::MaxLineLengthExceeded => {
432                    Poll::Ready(Some(Err(QueryError::MaxLineLengthExceeded)))
433                }
434                LinesCodecError::Io(io_err) => Poll::Ready(Some(Err(io_err.into()))),
435            },
436            None => Poll::Ready(None),
437        }
438    }
439}
440
441pub struct LinesBytesCodec {
442    // Stored index of the next index to examine for a `\n` character.
443    // This is used to optimize searching.
444    // For example, if `decode` was called with `abc`, it would hold `3`,
445    // because that is the next index to examine.
446    // The next time `decode` is called with `abcde\n`, the method will
447    // only look at `de\n` before returning.
448    next_index: usize,
449
450    /// The maximum length for a given line. If `usize::MAX`, lines will be
451    /// read until a `\n` character is reached.
452    max_length: usize,
453
454    /// Are we currently discarding the remainder of a line which was over
455    /// the length limit?
456    is_discarding: bool,
457}
458
459impl Default for LinesBytesCodec {
460    /// Returns a `LinesBytesCodec` for splitting up data into lines.
461    ///
462    /// # Note
463    ///
464    /// The returned `LinesBytesCodec` will not have an upper bound on the length
465    /// of a buffered line. See the documentation for [`new_with_max_length`]
466    /// for information on why this could be a potential security risk.
467    ///
468    /// [`new_with_max_length`]: crate::codec::LinesBytesCodec::default_with_max_length()
469    fn default() -> Self {
470        LinesBytesCodec {
471            next_index: 0,
472            max_length: usize::MAX,
473            is_discarding: false,
474        }
475    }
476}
477
478impl Decoder for LinesBytesCodec {
479    type Item = BytesMut;
480    type Error = LinesCodecError;
481
482    fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<BytesMut>, LinesCodecError> {
483        loop {
484            // Determine how far into the buffer we'll search for a newline. If
485            // there's no max_length set, we'll read to the end of the buffer.
486            let read_to = std::cmp::min(self.max_length.saturating_add(1), buf.len());
487
488            let newline_offset = buf[self.next_index..read_to]
489                .iter()
490                .position(|b| *b == b'\n');
491
492            match (self.is_discarding, newline_offset) {
493                (true, Some(offset)) => {
494                    // If we found a newline, discard up to that offset and
495                    // then stop discarding. On the next iteration, we'll try
496                    // to read a line normally.
497                    buf.advance(offset + self.next_index + 1);
498                    self.is_discarding = false;
499                    self.next_index = 0;
500                }
501                (true, None) => {
502                    // Otherwise, we didn't find a newline, so we'll discard
503                    // everything we read. On the next iteration, we'll continue
504                    // discarding up to max_len bytes unless we find a newline.
505                    buf.advance(read_to);
506                    self.next_index = 0;
507                    if buf.is_empty() {
508                        return Ok(None);
509                    }
510                }
511                (false, Some(offset)) => {
512                    // Found a line!
513                    let newline_index = offset + self.next_index;
514                    self.next_index = 0;
515                    let mut line = buf.split_to(newline_index + 1);
516                    line.truncate(line.len() - 1);
517                    without_carriage_return(&mut line);
518                    return Ok(Some(line));
519                }
520                (false, None) if buf.len() > self.max_length => {
521                    // Reached the maximum length without finding a
522                    // newline, return an error and start discarding on the
523                    // next call.
524                    self.is_discarding = true;
525                    return Err(LinesCodecError::MaxLineLengthExceeded);
526                }
527                (false, None) => {
528                    // We didn't find a line or reach the length limit, so the next
529                    // call will resume searching at the current offset.
530                    self.next_index = read_to;
531                    return Ok(None);
532                }
533            }
534        }
535    }
536
537    fn decode_eof(&mut self, buf: &mut BytesMut) -> Result<Option<BytesMut>, LinesCodecError> {
538        Ok(match self.decode(buf)? {
539            Some(frame) => Some(frame),
540            None => {
541                // No terminating newline - return remaining data, if any
542                if buf.is_empty() || buf == &b"\r"[..] {
543                    None
544                } else {
545                    let mut line = buf.split_to(buf.len());
546                    line.truncate(line.len() - 1);
547                    without_carriage_return(&mut line);
548                    self.next_index = 0;
549                    Some(line)
550                }
551            }
552        })
553    }
554}
555
556fn without_carriage_return(s: &mut BytesMut) {
557    if let Some(&b'\r') = s.last() {
558        s.truncate(s.len() - 1);
559    }
560}