ruststream_sea_file/
subscriber.rs1use 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
20const 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
33pub 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 #[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 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#[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 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
156fn 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 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 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 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}