Skip to main content

ruststream_sea_file/
subscriber.rs

1//! [`FileSubscriber`]: a stream of deliveries backed by a driver task that also serves
2//! repositioning.
3//!
4//! The client's `seek`/`rewind` need `&mut Consumer` and are explicitly not cancel-safe, so
5//! a driver task owns the consumer: seeks arrive as commands and run to completion outside
6//! any `select!`, while `next()` (which is cancel-safe) feeds the delivery channel.
7
8use std::sync::Arc;
9use std::sync::atomic::{AtomicU64, Ordering};
10
11use futures::Stream;
12use ruststream::Subscriber;
13use sea_streamer_file::{FileConsumer, FileErr};
14use sea_streamer_types::{Consumer as _, SeqPos, StreamErr, Timestamp};
15use tokio::sync::{mpsc, oneshot};
16
17use crate::error::{SeaFileError, box_err};
18use crate::message::{FilePosition, SeaMessage};
19
20/// How many undelivered messages may sit between the driver and the consumer.
21const CHANNEL_CAPACITY: usize = 64;
22
23pub(crate) struct SeekCmd {
24    position: FilePosition,
25    done: oneshot::Sender<Result<(), SeaFileError>>,
26}
27
28pub(crate) struct Stamped {
29    epoch: u64,
30    item: Option<Result<SeaMessage, SeaFileError>>,
31}
32
33/// A subscription to one stream key in the file; yields [`SeaMessage`]s.
34///
35/// Dropping the subscriber stops the driver task. A replay subscription completes (the
36/// stream ends) at the end of the file.
37pub struct FileSubscriber {
38    stream: String,
39    rx: mpsc::Receiver<Stamped>,
40    cmd: mpsc::UnboundedSender<SeekCmd>,
41    epoch: Arc<AtomicU64>,
42}
43
44impl std::fmt::Debug for FileSubscriber {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        f.debug_struct("FileSubscriber")
47            .field("stream", &self.stream)
48            .finish_non_exhaustive()
49    }
50}
51
52impl FileSubscriber {
53    /// The stream key this subscription consumes.
54    #[must_use]
55    pub fn stream_key(&self) -> &str {
56        &self.stream
57    }
58
59    pub(crate) fn spawn(stream: String, consumer: FileConsumer, replay: bool) -> Self {
60        let (out_tx, out_rx) = mpsc::channel(CHANNEL_CAPACITY);
61        let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
62        let epoch = Arc::new(AtomicU64::new(0));
63        tokio::spawn(drive(
64            consumer,
65            out_tx,
66            cmd_rx,
67            stream.clone(),
68            replay,
69            Arc::clone(&epoch),
70        ));
71        Self {
72            stream,
73            rx: out_rx,
74            cmd: cmd_tx,
75            epoch,
76        }
77    }
78}
79
80impl Subscriber for FileSubscriber {
81    type Message = SeaMessage;
82    type Error = SeaFileError;
83
84    fn stream(&mut self) -> impl Stream<Item = Result<SeaMessage, SeaFileError>> + Send + '_ {
85        // Poll the channel in place rather than wrapping it in an owning stream, so `stream`
86        // can be called again after the returned stream is dropped (the runtime and the
87        // conformance helpers re-enter it per call). Items queued under an older generation
88        // (before a seek) are discarded here; `item: None` marks a clean end of a replay.
89        futures::stream::poll_fn(move |cx| {
90            loop {
91                match self.rx.poll_recv(cx) {
92                    std::task::Poll::Ready(Some(stamped)) => {
93                        if stamped.epoch == self.epoch.load(Ordering::Acquire) {
94                            return std::task::Poll::Ready(stamped.item);
95                        }
96                    }
97                    std::task::Poll::Ready(None) => return std::task::Poll::Ready(None),
98                    std::task::Poll::Pending => return std::task::Poll::Pending,
99                }
100            }
101        })
102    }
103}
104
105/// Repositions a [`FileSubscriber`] while its stream runs; minted by
106/// [`Seekable::seeker`](ruststream::Seekable::seeker).
107#[derive(Clone)]
108pub struct FileSeeker {
109    cmd: mpsc::UnboundedSender<SeekCmd>,
110    epoch: Arc<AtomicU64>,
111    stream: String,
112}
113
114impl std::fmt::Debug for FileSeeker {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        f.debug_struct("FileSeeker")
117            .field("stream", &self.stream)
118            .finish_non_exhaustive()
119    }
120}
121
122impl ruststream::Seeker for FileSeeker {
123    type Position = FilePosition;
124    type Error = SeaFileError;
125
126    async fn seek(&self, to: FilePosition) -> Result<(), SeaFileError> {
127        // Bump the generation first: deliveries already queued (or an in-flight forward)
128        // belong to the pre-seek position and are discarded on the way out.
129        self.epoch.fetch_add(1, Ordering::Release);
130        let (done, wait) = oneshot::channel();
131        self.cmd
132            .send(SeekCmd { position: to, done })
133            .map_err(|_| SeaFileError::Seek {
134                stream: self.stream.clone(),
135                source: Box::from("the subscription's driver task has shut down"),
136            })?;
137        wait.await.map_err(|_| SeaFileError::Seek {
138            stream: self.stream.clone(),
139            source: Box::from("the subscription's driver task has shut down"),
140        })?
141    }
142}
143
144impl ruststream::Seekable for FileSubscriber {
145    type Seeker = FileSeeker;
146
147    fn seeker(&self) -> FileSeeker {
148        FileSeeker {
149            cmd: self.cmd.clone(),
150            epoch: Arc::clone(&self.epoch),
151            stream: self.stream.clone(),
152        }
153    }
154}
155
156/// A receive failure that means the stream ended cleanly: the writer's end-of-stream mark,
157/// or the end of a dead file in replay mode.
158fn is_clean_end(err: &StreamErr<FileErr>) -> bool {
159    matches!(
160        err,
161        StreamErr::Backend(FileErr::StreamEnded | FileErr::NotEnoughBytes)
162    )
163}
164
165async fn drive(
166    mut consumer: FileConsumer,
167    out: mpsc::Sender<Stamped>,
168    mut cmd_rx: mpsc::UnboundedReceiver<SeekCmd>,
169    stream: String,
170    replay: bool,
171    epoch: Arc<AtomicU64>,
172) {
173    loop {
174        // Captured before awaiting: a delivery resolved out of `next()` was positioned before
175        // any seek that lands mid-await, so it must carry the pre-await generation - stamping
176        // after the await would let a concurrent seek's bump leak onto a stale delivery.
177        let current = epoch.load(Ordering::Acquire);
178        tokio::select! {
179            biased;
180            cmd = cmd_rx.recv() => {
181                let Some(SeekCmd { position, done }) = cmd else { break };
182                // The client's seek is not cancel-safe: it runs here to completion, never
183                // inside a racing select arm.
184                let result = match position {
185                    FilePosition::Beginning => consumer.rewind(SeqPos::Beginning).await,
186                    FilePosition::End => consumer.rewind(SeqPos::End).await,
187                    FilePosition::Sequence(sequence) => {
188                        consumer.rewind(SeqPos::At(sequence)).await
189                    }
190                    FilePosition::Timestamp(millis) => {
191                        let nanos = i128::from(millis) * 1_000_000;
192                        match Timestamp::from_unix_timestamp_nanos(nanos) {
193                            Ok(timestamp) => consumer.seek(timestamp).await,
194                            Err(err) => {
195                                let _ = done.send(Err(SeaFileError::Invalid(format!(
196                                    "'{millis}' is not a valid timestamp: {err}"
197                                ))));
198                                continue;
199                            }
200                        }
201                    }
202                };
203                let _ = done.send(result.map_err(|e| SeaFileError::Seek {
204                    stream: stream.clone(),
205                    source: box_err(e),
206                }));
207            }
208            () = out.closed() => break,
209            next = consumer.next() => {
210                match next {
211                    Ok(message) => {
212                        let item = Stamped {
213                            epoch: current,
214                            item: Some(Ok(SeaMessage::new(&message))),
215                        };
216                        if out.send(item).await.is_err() {
217                            break;
218                        }
219                    }
220                    Err(err) if is_clean_end(&err) => {
221                        if replay {
222                            // A finished replay completes the subscription.
223                            let _ = out.send(Stamped { epoch: current, item: None }).await;
224                        } else {
225                            let _ = out
226                                .send(Stamped {
227                                    epoch: current,
228                                    item: Some(Err(SeaFileError::Receive {
229                                        stream: stream.clone(),
230                                        source: box_err(err),
231                                    })),
232                                })
233                                .await;
234                        }
235                        break;
236                    }
237                    Err(err) => {
238                        let _ = out
239                            .send(Stamped {
240                                epoch: current,
241                                item: Some(Err(SeaFileError::Receive {
242                                    stream: stream.clone(),
243                                    source: box_err(err),
244                                })),
245                            })
246                            .await;
247                        break;
248                    }
249                }
250            }
251        }
252    }
253}