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//!
8//! Batches sit on top of that channel rather than in the client, which reads one message at a
9//! time; see [`crate::batching`] for why, and for the deadline that closes a partial one.
10//!
11//! A replay reads the file itself rather than through the client's consumer; see [`Reader`] for
12//! why.
13
14use std::future::Future;
15use std::num::NonZeroUsize;
16use std::sync::Arc;
17use std::sync::atomic::{AtomicU64, Ordering};
18
19use futures::Stream;
20#[cfg(feature = "testing")]
21use futures::future::Either;
22use ruststream::{
23    AckError, BatchSubscriber, BufferedSubscriber, HeaderMap, IncomingMessage, Positioned,
24    Seekable, Seeker, Subscriber,
25};
26use sea_streamer_file::{FileConsumer, FileErr, MessageSource, SeekTarget, is_end_of_stream};
27use sea_streamer_types::{Consumer as _, ShardId, SharedMessage, StreamErr, StreamKey, Timestamp};
28use tokio::runtime::Handle;
29use tokio::sync::mpsc::error::TrySendError;
30use tokio::sync::{mpsc, oneshot};
31
32use crate::batching::BATCH_MAX_WAIT;
33use crate::error::{SeaFileError, box_err};
34#[cfg(feature = "testing")]
35use crate::in_process::{FileQueue, LogSeeker};
36use crate::message::{FilePosition, SeaMessage};
37
38/// How many undelivered messages may sit between the driver and the consumer.
39const CHANNEL_CAPACITY: usize = 64;
40
41pub(crate) struct SeekCmd {
42    position: FilePosition,
43    done: oneshot::Sender<Result<(), SeaFileError>>,
44}
45
46pub(crate) struct Stamped {
47    epoch: u64,
48    item: Option<Result<SeaMessage, SeaFileError>>,
49}
50
51/// A subscription to one stream key in the file; yields [`FileMessage`]s.
52///
53/// Dropping the subscriber stops the driver task. The stream also ends on its own when the file
54/// does: a replay reaches the end of what was retained, and a live subscription reaches the
55/// end-of-stream mark a writer left with
56/// [`end_with_eos`](crate::FileBroker::end_with_eos). A replay that ended can still be moved by
57/// its seeker until the subscriber is dropped, and its stream then delivers again.
58///
59/// A handler taking `&[T]` consumes batches of this subscription, and `batch(n)` at the mount
60/// site says how large one may be. The client reads one entry at a time, so the batch is
61/// assembled here: it holds at most the size the mount site named, and fewer when that is all the
62/// file had. A partial batch goes out 10 ms after its first delivery. The
63/// [Batches](https://powersemmi.github.io/ruststream-sea-file/latest/components/subscriptions/batches/)
64/// page shows it in a service.
65///
66/// # Examples
67///
68/// ```
69/// use ruststream_sea_file::file::prelude::*;
70/// use serde::Deserialize;
71///
72/// #[derive(Debug, Deserialize)]
73/// struct Reading {
74///     millivolts: u32,
75/// }
76///
77/// #[subscriber(FileStream::new("readings"))]
78/// async fn aggregate(batch: &[Reading]) -> Vec<HandlerOutcome> {
79///     let total: u32 = batch.iter().map(|reading| reading.millivolts).sum();
80///     tracing::info!(readings = batch.len(), total, "aggregated");
81///     batch.iter().map(|_| HandlerOutcome::ack()).collect()
82/// }
83///
84/// #[ruststream::app]
85/// fn app() -> impl App {
86///     RustStream::new(AppInfo::new("readings", "0.1.0"))
87///         .with_broker(FileBroker::new("/tmp/readings.ss"), |b| {
88///             b.include(aggregate.batch(nonzero!(4)))
89///                 .max_attempts(nonzero!(3u32))
90///                 .dead_letter("readings.dead");
91///         })
92/// }
93/// ```
94pub struct FileSubscriber {
95    // Kept alongside the buffer so the stream key stays readable without reaching through it.
96    stream: Arc<str>,
97    // The driver task's deliveries plus client-side batching: the file client reads one message
98    // at a time, so batches are assembled here - see the `batching` module.
99    inner: BufferedSubscriber<Deliveries>,
100}
101
102impl std::fmt::Debug for FileSubscriber {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        f.debug_struct("FileSubscriber")
105            .field("stream", &self.stream)
106            .finish_non_exhaustive()
107    }
108}
109
110impl FileSubscriber {
111    /// The stream key this subscription consumes.
112    #[must_use]
113    pub fn stream_key(&self) -> &str {
114        &self.stream
115    }
116
117    /// Starts the driver on `runtime`, the one the broker connected on, whichever runtime opens
118    /// the subscription. `closed` answers whether that connection has shut down, which refuses
119    /// every seek from then on.
120    pub(crate) fn spawn(
121        runtime: &Handle,
122        stream: String,
123        reader: impl Source,
124        closed: impl Fn() -> bool + Send + Sync + 'static,
125    ) -> Self {
126        let (out_tx, out_rx) = mpsc::channel(CHANNEL_CAPACITY);
127        let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
128        let epoch = Arc::new(AtomicU64::new(0));
129        runtime.spawn(drive(
130            reader,
131            out_tx,
132            cmd_rx,
133            Arc::clone(&epoch),
134            stream.clone(),
135            closed,
136        ));
137        let stream: Arc<str> = Arc::from(stream);
138        Self {
139            stream: Arc::clone(&stream),
140            inner: BufferedSubscriber::new(Deliveries::Driver(Driven {
141                stream,
142                rx: out_rx,
143                cmd: cmd_tx,
144                epoch,
145            }))
146            .max_wait(BATCH_MAX_WAIT),
147        }
148    }
149
150    /// A subscription on the in-process transport, which needs no driver task: a seek is applied
151    /// inside the subscription's own poll.
152    #[cfg(feature = "testing")]
153    pub(crate) fn in_process(queue: FileQueue) -> Self {
154        Self {
155            stream: Arc::clone(queue.stream()),
156            inner: BufferedSubscriber::new(Deliveries::InProcess(queue)).max_wait(BATCH_MAX_WAIT),
157        }
158    }
159}
160
161impl Subscriber for FileSubscriber {
162    type Message = FileMessage;
163    type Error = SeaFileError;
164
165    fn stream(&mut self) -> impl Stream<Item = Result<FileMessage, SeaFileError>> + Send + '_ {
166        self.inner.stream()
167    }
168}
169
170impl BatchSubscriber for FileSubscriber {
171    type Batch = Vec<FileMessage>;
172
173    fn batches(
174        &mut self,
175        size: NonZeroUsize,
176    ) -> impl Stream<Item = Result<Self::Batch, SeaFileError>> + Send + '_ {
177        self.inner.batches(size)
178    }
179}
180
181impl Seekable for FileSubscriber {
182    type Seeker = FileSeeker;
183
184    fn seeker(&self) -> FileSeeker {
185        // Batching does not move the subscription: this is the driver's own handle, reached
186        // through the buffer.
187        self.inner.seeker()
188    }
189}
190
191/// A subscription's deliveries, before batching: one message per poll, in the file's order.
192///
193/// Without the `testing` feature there is one variant, so the type is the driver's channel itself
194/// and every `match` on it is irrefutable: a production build carries no second transport and no
195/// branch to it.
196enum Deliveries {
197    /// The driver task's channel, fed from the stream file.
198    Driver(Driven),
199    /// The in-process transport's queue.
200    #[cfg(feature = "testing")]
201    InProcess(FileQueue),
202}
203
204// The zero-cost promise of the in-process mode, held by the compiler: a build without it gives the
205// deliveries exactly the size of the driver's channel they wrap.
206#[cfg(not(feature = "testing"))]
207const _: () = assert!(size_of::<Deliveries>() == size_of::<Driven>());
208
209impl Subscriber for Deliveries {
210    type Message = FileMessage;
211    type Error = SeaFileError;
212
213    #[cfg(not(feature = "testing"))]
214    fn stream(&mut self) -> impl Stream<Item = Result<FileMessage, SeaFileError>> + Send + '_ {
215        let Self::Driver(driven) = self;
216        driven.stream()
217    }
218
219    #[cfg(feature = "testing")]
220    fn stream(&mut self) -> impl Stream<Item = Result<FileMessage, SeaFileError>> + Send + '_ {
221        match self {
222            Self::Driver(driven) => Either::Left(driven.stream()),
223            Self::InProcess(queue) => Either::Right(in_process_stream(queue)),
224        }
225    }
226}
227
228impl Seekable for Deliveries {
229    type Seeker = FileSeeker;
230
231    fn seeker(&self) -> FileSeeker {
232        match self {
233            Self::Driver(driven) => driven.seeker(),
234            #[cfg(feature = "testing")]
235            Self::InProcess(queue) => {
236                FileSeeker::in_process(Arc::clone(queue.stream()), queue.seeker())
237            }
238        }
239    }
240}
241
242/// The in-process queue's deliveries, each carrying the subscription's seeker the way a file's do.
243#[cfg(feature = "testing")]
244fn in_process_stream(
245    queue: &mut FileQueue,
246) -> impl Stream<Item = Result<FileMessage, SeaFileError>> + Send + '_ {
247    let seeker = Arc::new(FileSeeker::in_process(
248        Arc::clone(queue.stream()),
249        queue.seeker(),
250    ));
251    futures::stream::poll_fn(move |cx| {
252        queue.poll_next(cx).map(|next| {
253            next.map(|message| {
254                Ok(FileMessage {
255                    message,
256                    seeker: Arc::clone(&seeker),
257                })
258            })
259        })
260    })
261}
262
263/// The driver task's deliveries: one message per poll, in publish order.
264struct Driven {
265    stream: Arc<str>,
266    rx: mpsc::Receiver<Stamped>,
267    cmd: mpsc::UnboundedSender<SeekCmd>,
268    epoch: Arc<AtomicU64>,
269}
270
271impl Driven {
272    fn stream(&mut self) -> impl Stream<Item = Result<FileMessage, SeaFileError>> + Send + '_ {
273        // Minted once per opened stream, before the closure takes the receiver: every delivery
274        // then carries a reference-counted clone, so the per-delivery context that reads the
275        // seek handle by key allocates nothing.
276        let seeker = Arc::new(self.seeker());
277        // Poll the channel in place rather than wrapping it in an owning stream, so `stream`
278        // can be called again after the returned stream is dropped (the runtime and the
279        // conformance helpers re-enter it per call). Items queued under an older generation
280        // (before a seek) are discarded here; `item: None` marks the clean end of the stream.
281        futures::stream::poll_fn(move |cx| {
282            loop {
283                match self.rx.poll_recv(cx) {
284                    std::task::Poll::Ready(Some(stamped)) => {
285                        if stamped.epoch == self.epoch.load(Ordering::Acquire) {
286                            return std::task::Poll::Ready(stamped.item.map(|item| {
287                                item.map(|message| FileMessage {
288                                    message,
289                                    seeker: Arc::clone(&seeker),
290                                })
291                            }));
292                        }
293                    }
294                    std::task::Poll::Ready(None) => return std::task::Poll::Ready(None),
295                    std::task::Poll::Pending => return std::task::Poll::Pending,
296                }
297            }
298        })
299    }
300
301    fn seeker(&self) -> FileSeeker {
302        FileSeeker {
303            stream: Arc::clone(&self.stream),
304            backend: SeekBackend::Driver {
305                cmd: self.cmd.clone(),
306            },
307        }
308    }
309}
310
311/// A delivery from a stream file: the transport's [`SeaMessage`] plus the subscription's
312/// reposition handle.
313///
314/// The handle is what makes the file transport's per-delivery context
315/// ([`FileContext`](crate::FileContext)) buildable, and carrying it in the message type is what
316/// keeps that context off the transports that cannot seek: standard input yields a plain
317/// [`SeaMessage`], so a handler declaring the seeking context does not compile against it.
318pub struct FileMessage {
319    message: SeaMessage,
320    seeker: Arc<FileSeeker>,
321}
322
323impl std::fmt::Debug for FileMessage {
324    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325        f.debug_struct("FileMessage")
326            .field("message", &self.message)
327            .finish_non_exhaustive()
328    }
329}
330
331impl FileMessage {
332    /// The stream key this message was published to.
333    #[must_use]
334    pub fn stream(&self) -> &str {
335        self.message.stream()
336    }
337
338    /// The reposition handle of the subscription that delivered this message.
339    pub(crate) fn seeker(&self) -> &FileSeeker {
340        &self.seeker
341    }
342}
343
344impl Positioned for FileMessage {
345    type Position = FilePosition;
346
347    fn position(&self) -> FilePosition {
348        self.message.position()
349    }
350}
351
352impl IncomingMessage for FileMessage {
353    fn payload(&self) -> &[u8] {
354        self.message.payload()
355    }
356
357    fn headers(&self) -> &HeaderMap {
358        self.message.headers()
359    }
360
361    fn ack(self) -> impl Future<Output = Result<(), AckError>> {
362        self.message.ack()
363    }
364
365    fn nack(self, requeue: bool) -> impl Future<Output = Result<(), AckError>> {
366        self.message.nack(requeue)
367    }
368}
369
370/// What a [`FileSeeker`] repositions: the subscription it was minted from.
371///
372/// One variant per transport that delivers under the file form's contexts, each carrying only
373/// its own machinery, so a handle can never hold the wrong half. The seeker itself is one type
374/// on purpose: it is the value the [`SeekHandle`](crate::SeekHandle) key yields, so a handler
375/// that seeks reads the same way against a stream file and against the in-process transport its
376/// tests run on.
377#[derive(Clone)]
378enum SeekBackend {
379    /// The stream file's driver task, which owns the client consumer.
380    Driver { cmd: mpsc::UnboundedSender<SeekCmd> },
381    /// The in-process transport's log, repositioned inside the subscription's own poll.
382    #[cfg(feature = "testing")]
383    Log(LogSeeker),
384}
385
386/// Repositions a [`FileSubscriber`] while its stream runs; minted by
387/// [`Seekable::seeker`](ruststream::Seekable::seeker), and carried to handlers by the
388/// [`SeekHandle`](crate::SeekHandle) context key.
389///
390/// A seek through a handle that outlived the broker's shutdown returns an error.
391#[derive(Clone)]
392pub struct FileSeeker {
393    // Arc rather than String: the per-delivery context clones the handle, and the clone must
394    // stay allocation-free on the dispatch path.
395    stream: Arc<str>,
396    backend: SeekBackend,
397}
398
399impl FileSeeker {
400    /// The seeker of an in-process subscription.
401    #[cfg(feature = "testing")]
402    pub(crate) fn in_process(stream: Arc<str>, log: LogSeeker) -> Self {
403        Self {
404            stream,
405            backend: SeekBackend::Log(log),
406        }
407    }
408
409    /// The stream key of the subscription this handle repositions.
410    #[must_use]
411    pub fn stream_key(&self) -> &str {
412        &self.stream
413    }
414
415    fn dead(&self, why: &'static str) -> SeaFileError {
416        SeaFileError::Seek {
417            stream: self.stream.to_string(),
418            source: Box::from(why),
419        }
420    }
421}
422
423impl std::fmt::Debug for FileSeeker {
424    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
425        f.debug_struct("FileSeeker")
426            .field("stream", &self.stream)
427            .finish_non_exhaustive()
428    }
429}
430
431impl Seeker for FileSeeker {
432    type Position = FilePosition;
433    type Error = SeaFileError;
434
435    async fn seek(&self, to: FilePosition) -> Result<(), SeaFileError> {
436        match &self.backend {
437            SeekBackend::Driver { cmd } => {
438                let (done, wait) = oneshot::channel();
439                cmd.send(SeekCmd { position: to, done })
440                    .map_err(|_| self.dead("the subscription's driver task has shut down"))?;
441                wait.await
442                    .map_err(|_| self.dead("the subscription's driver task has shut down"))?
443            }
444            // The in-process transport needs no task: the target is handed to the subscriber,
445            // which applies it at the top of its next poll, inside the reaction the test
446            // harness drives to quiescence.
447            #[cfg(feature = "testing")]
448            SeekBackend::Log(log) => log.request(to),
449        }
450    }
451}
452
453/// The shard every message of a stream file is written to: the file transport has one per key.
454const SHARD: ShardId = ShardId::new(0);
455
456/// What a subscription's driver task reads the file through.
457pub(crate) enum Reader {
458    /// The client's consumer, which follows the live tail.
459    Tail(FileConsumer),
460    /// The file read directly, from the beginning, one message at a time.
461    ///
462    /// The client's own replay reads ahead of its consumer and, at the end of a file with no
463    /// end-of-stream mark, reports the end while that read-ahead is still undelivered, so the
464    /// tail of the file never reaches the handler. Read here, nothing sits between the file and
465    /// the delivery channel, and the end is reported after the last message was sent on.
466    Replay {
467        source: Box<MessageSource>,
468        key: StreamKey,
469    },
470}
471
472/// What the driver task reads a subscription through and repositions: the stream file's
473/// [`Reader`] in a service, a scripted log in this module's tests.
474pub(crate) trait Source: Send + 'static {
475    /// The next message of the subscription's stream key.
476    fn next(&mut self) -> impl Future<Output = Result<SharedMessage, StreamErr<FileErr>>> + Send;
477
478    /// Moves the subscription; not cancel-safe, so it runs to completion on the driver task.
479    fn reposition(
480        &mut self,
481        target: SeekTarget,
482    ) -> impl Future<Output = Result<(), FileErr>> + Send;
483
484    /// Whether a seek can still move the reader once it reported the end of the stream.
485    fn repositions_after_end(&self) -> bool;
486}
487
488impl Source for Reader {
489    async fn next(&mut self) -> Result<SharedMessage, StreamErr<FileErr>> {
490        match self {
491            Self::Tail(consumer) => consumer.next().await,
492            Self::Replay { source, key } => loop {
493                let message = source.next().await.map_err(StreamErr::Backend)?.message;
494                if is_end_of_stream(&message) {
495                    return Err(StreamErr::Backend(FileErr::StreamEnded));
496                }
497                // One file holds every stream key, the client's internal ones included.
498                if message.header().stream_key() == key {
499                    return Ok(message.to_shared());
500                }
501            },
502        }
503    }
504
505    async fn reposition(&mut self, target: SeekTarget) -> Result<(), FileErr> {
506        match self {
507            Self::Tail(consumer) => consumer.seek_to(target).await,
508            Self::Replay { source, key } => source.seek(key, &SHARD, target).await,
509        }
510    }
511
512    fn repositions_after_end(&self) -> bool {
513        // The client's consumer task ends with the stream it tails; the file itself stays
514        // readable from anywhere.
515        matches!(self, Self::Replay { .. })
516    }
517}
518
519/// A receive failure that means the stream ended cleanly: the writer's end-of-stream mark,
520/// or the end of the file in replay mode.
521fn is_clean_end(err: &StreamErr<FileErr>) -> bool {
522    matches!(
523        err,
524        StreamErr::Backend(FileErr::StreamEnded | FileErr::NotEnoughBytes)
525    )
526}
527
528/// What the driver does once a read's outcome is delivered.
529enum After {
530    /// Reads on.
531    Read,
532    /// The stream ended cleanly: nothing is read until a seek moves the reader again.
533    Idle,
534    /// The stream ended or the read failed, and the reader cannot move again: the driver stops.
535    Stop,
536}
537
538/// The delivery one outcome of the reader's `next` produces, and what the driver does after it.
539fn delivery(
540    next: Result<SharedMessage, StreamErr<FileErr>>,
541    reader: &impl Source,
542    stream: &str,
543) -> (Option<Result<SeaMessage, SeaFileError>>, After) {
544    match next {
545        Ok(message) => (Some(Ok(SeaMessage::new(&message))), After::Read),
546        // A stream that ended is not a failure. Both endings arrive here: the writer's
547        // end-of-stream mark, which is the only thing that ends a live subscription, and the end
548        // of the retained file, which ends a replay.
549        Err(err) if is_clean_end(&err) => {
550            let after = if reader.repositions_after_end() {
551                After::Idle
552            } else {
553                After::Stop
554            };
555            (None, after)
556        }
557        Err(err) => (
558            Some(Err(SeaFileError::Receive {
559                stream: stream.to_owned(),
560                source: box_err(err),
561            })),
562            After::Stop,
563        ),
564    }
565}
566
567async fn drive(
568    mut reader: impl Source,
569    out: mpsc::Sender<Stamped>,
570    mut cmd_rx: mpsc::UnboundedReceiver<SeekCmd>,
571    epoch: Arc<AtomicU64>,
572    stream: String,
573    closed: impl Fn() -> bool + Send + Sync,
574) {
575    // The generation the reader is positioned in, published through `epoch` to the subscription's
576    // filter. Only this task advances it, and only once a reposition has run.
577    let mut generation = 0;
578    // Set once the stream ended. The driver then serves seeks only, and the subscription stays
579    // repositionable until it is dropped.
580    let mut idle = false;
581    'read: loop {
582        let next = tokio::select! {
583            biased;
584            cmd = cmd_rx.recv() => {
585                let Some(cmd) = cmd else { break };
586                if serve(&mut reader, cmd, &mut generation, &epoch, &stream, &closed).await {
587                    idle = false;
588                }
589                continue;
590            }
591            () = out.closed() => break,
592            next = reader.next(), if !idle => next,
593        };
594        let (item, after) = delivery(next, &reader, &stream);
595        let permit = match out.try_reserve() {
596            Ok(permit) => permit,
597            Err(TrySendError::Closed(())) => break,
598            // The subscription is behind. The handler that stopped draining the channel may be
599            // the one waiting on a seek, so seeks are served while the driver waits for room.
600            Err(TrySendError::Full(())) => loop {
601                tokio::select! {
602                    biased;
603                    cmd = cmd_rx.recv() => {
604                        let Some(cmd) = cmd else { break 'read };
605                        if serve(&mut reader, cmd, &mut generation, &epoch, &stream, &closed).await {
606                            // What was read belongs to the old position and goes with it.
607                            idle = false;
608                            continue 'read;
609                        }
610                    }
611                    permit = out.reserve() => match permit {
612                        Ok(permit) => break permit,
613                        Err(_) => break 'read,
614                    },
615                }
616            },
617        };
618        permit.send(Stamped {
619            epoch: generation,
620            item,
621        });
622        match after {
623            After::Read => {}
624            After::Idle => idle = true,
625            After::Stop => break,
626        }
627    }
628}
629
630/// Runs one seek on the reader and answers it; `true` when the reader moved.
631///
632/// A reader that moved opens a new generation: everything read before it, queued for the handler
633/// or still held by the driver, is discarded on the way out. A refused seek leaves the reader and
634/// every queued delivery where they were. Once the connection has shut down every seek is
635/// refused: a seeker that outlived the broker must not report a reposition.
636async fn serve(
637    reader: &mut impl Source,
638    SeekCmd { position, done }: SeekCmd,
639    generation: &mut u64,
640    epoch: &AtomicU64,
641    stream: &str,
642    closed: impl Fn() -> bool + Send + Sync,
643) -> bool {
644    if closed() {
645        let _ = done.send(Err(SeaFileError::Seek {
646            stream: stream.to_owned(),
647            source: Box::from("the broker has shut down"),
648        }));
649        return false;
650    }
651    // Whether the reader reads one message past the target before the subscription delivers.
652    let mut skip_one = false;
653    let target = match position {
654        // A stream key is numbered from one, so its first message is where the file begins.
655        FilePosition::Beginning | FilePosition::Sequence(0 | 1) => SeekTarget::Beginning,
656        FilePosition::End => SeekTarget::End,
657        // The file finds a sequence by reading forward to it, so it refuses the one its next
658        // append carries, which is where a service that handled everything resumes. The message
659        // before it is there to find, and reading it leaves the reader where `sequence` is or
660        // will be: the file numbers a stream key without gaps.
661        FilePosition::Sequence(sequence) => {
662            skip_one = true;
663            SeekTarget::SeqNo(sequence - 1)
664        }
665        FilePosition::Timestamp(millis) => {
666            let nanos = i128::from(millis) * 1_000_000;
667            match Timestamp::from_unix_timestamp_nanos(nanos) {
668                Ok(timestamp) => SeekTarget::Timestamp(timestamp),
669                Err(err) => {
670                    let _ = done.send(Err(SeaFileError::Invalid(format!(
671                        "'{millis}' is not a valid timestamp: {err}"
672                    ))));
673                    return false;
674                }
675            }
676        }
677    };
678    // The client's seek is not cancel-safe: it runs here to completion, never inside a racing
679    // select arm.
680    let mut result = reader.reposition(target).await.map_err(box_err);
681    if result.is_ok() && skip_one {
682        result = reader.next().await.map(drop).map_err(box_err);
683    }
684    let moved = result.is_ok();
685    if moved {
686        // Published before the answer (Release, paired with the filter's Acquire load), so once
687        // the seek returns, nothing read at the old position reaches the handler.
688        *generation += 1;
689        epoch.store(*generation, Ordering::Release);
690    }
691    let _ = done.send(result.map_err(|source| SeaFileError::Seek {
692        stream: stream.to_owned(),
693        source,
694    }));
695    moved
696}
697
698#[cfg(test)]
699mod tests {
700    use std::future::ready;
701    use std::time::Duration;
702
703    use futures::StreamExt;
704    use sea_streamer_file::SeekErr;
705    use sea_streamer_types::{MessageHeader, SeqNo};
706
707    use super::*;
708    use crate::wire;
709
710    /// Longer than anything the driver waits for on its own: on the paused clock it elapses only
711    /// once the runtime has nothing left to run, so reaching it means the seek can never finish.
712    const STUCK: Duration = Duration::from_mins(1);
713
714    /// A stream key's log held in memory, read the way a replay reads a file: every message in
715    /// order, then the end of the file. A read is always ready, so the driver fills the delivery
716    /// channel without yielding and parks only where it waits for room.
717    struct Log {
718        messages: Vec<SharedMessage>,
719        next: usize,
720    }
721
722    impl Log {
723        fn of(count: u8) -> Self {
724            let key = StreamKey::new("orders").expect("the key is valid");
725            let messages = (1..=count)
726                .map(|body| {
727                    let bytes = wire::encode(&HeaderMap::new(), &[body], false);
728                    let length = bytes.len();
729                    SharedMessage::new(
730                        MessageHeader::new(
731                            key.clone(),
732                            SHARD,
733                            SeqNo::from(body),
734                            Timestamp::now_utc(),
735                        ),
736                        bytes,
737                        0,
738                        length,
739                    )
740                })
741                .collect();
742            Self { messages, next: 0 }
743        }
744    }
745
746    impl Source for Log {
747        fn next(&mut self) -> impl Future<Output = Result<SharedMessage, StreamErr<FileErr>>> {
748            let message = self.messages.get(self.next).cloned();
749            if message.is_some() {
750                self.next += 1;
751            }
752            ready(message.ok_or(StreamErr::Backend(FileErr::NotEnoughBytes)))
753        }
754
755        fn reposition(&mut self, target: SeekTarget) -> impl Future<Output = Result<(), FileErr>> {
756            let next = match target {
757                SeekTarget::Beginning => 0,
758                SeekTarget::End => self.messages.len(),
759                // Past the last message, as the file refuses it: the reader stays where it was.
760                SeekTarget::SeqNo(sequence) if sequence > self.messages.len() as u64 => {
761                    return ready(Err(FileErr::SeekErr(SeekErr::OutOfBound)));
762                }
763                SeekTarget::SeqNo(sequence) => {
764                    usize::try_from(sequence.saturating_sub(1)).expect("the sequence fits")
765                }
766                SeekTarget::Timestamp(_) => unreachable!("these tests seek by sequence"),
767            };
768            self.next = next;
769            ready(Ok(()))
770        }
771
772        fn repositions_after_end(&self) -> bool {
773            true
774        }
775    }
776
777    /// What the subscription delivers next: the body of a message, or `None` at its end.
778    async fn next_body<S>(stream: &mut S) -> Option<u8>
779    where
780        S: Stream<Item = Result<FileMessage, SeaFileError>> + Unpin,
781    {
782        tokio::time::timeout(STUCK, stream.next())
783            .await
784            .expect("the subscription moves on")
785            .map(|next| next.expect("the delivery is ok").payload()[0])
786    }
787
788    /// A handler seeking while more messages are queued than the delivery channel holds: the
789    /// driver is parked on the full channel, and the seek must still reach it.
790    #[tokio::test(start_paused = true)]
791    async fn a_seek_completes_while_the_delivery_channel_is_full() {
792        let backlog = u8::try_from(CHANNEL_CAPACITY * 2).expect("the backlog fits");
793        let mut subscriber = FileSubscriber::spawn(
794            &Handle::current(),
795            "orders".to_owned(),
796            Log::of(backlog),
797            || false,
798        );
799        let seeker = subscriber.seeker();
800        let mut stream = std::pin::pin!(subscriber.stream());
801        assert_eq!(next_body(&mut stream).await, Some(1));
802        // Lets the driver run until it parks: the channel is full and a read waits for room.
803        tokio::task::yield_now().await;
804
805        tokio::time::timeout(STUCK, seeker.seek(FilePosition::sequence(100)))
806            .await
807            .expect("a seek completes while the delivery channel is full")
808            .expect("the seek succeeds");
809
810        for expected in 100..=backlog {
811            assert_eq!(next_body(&mut stream).await, Some(expected));
812        }
813        assert_eq!(next_body(&mut stream).await, None, "the replay ends");
814    }
815
816    /// A handler seeking once the replay has read to the end of the file, while the end it
817    /// reported is still queued behind the last message: the end belongs to the old position,
818    /// and the replay goes on from the new one.
819    #[tokio::test(start_paused = true)]
820    async fn a_seek_after_the_replay_reached_the_end_replays_again() {
821        let mut subscriber =
822            FileSubscriber::spawn(&Handle::current(), "orders".to_owned(), Log::of(3), || {
823                false
824            });
825        let seeker = subscriber.seeker();
826        let mut stream = std::pin::pin!(subscriber.stream());
827        for expected in 1..=3 {
828            assert_eq!(next_body(&mut stream).await, Some(expected));
829        }
830        // Lets the driver read past the last message and report the end.
831        tokio::task::yield_now().await;
832
833        tokio::time::timeout(STUCK, seeker.seek(FilePosition::beginning()))
834            .await
835            .expect("the seek completes")
836            .expect("a seek after the end of the file succeeds");
837
838        for expected in 1..=3 {
839            assert_eq!(next_body(&mut stream).await, Some(expected));
840        }
841        assert_eq!(next_body(&mut stream).await, None, "the replay ends again");
842    }
843
844    /// A seek after the subscription already yielded its end: the subscription is still open, so
845    /// the seek moves it and its stream delivers again.
846    #[tokio::test(start_paused = true)]
847    async fn a_seek_after_the_stream_ended_reopens_it() {
848        let mut subscriber =
849            FileSubscriber::spawn(&Handle::current(), "orders".to_owned(), Log::of(2), || {
850                false
851            });
852        let seeker = subscriber.seeker();
853        let mut stream = std::pin::pin!(subscriber.stream());
854        assert_eq!(next_body(&mut stream).await, Some(1));
855        assert_eq!(next_body(&mut stream).await, Some(2));
856        assert_eq!(next_body(&mut stream).await, None);
857
858        tokio::time::timeout(STUCK, seeker.seek(FilePosition::sequence(2)))
859            .await
860            .expect("the seek completes")
861            .expect("a seek after the stream ended succeeds");
862
863        assert_eq!(next_body(&mut stream).await, Some(2));
864        assert_eq!(next_body(&mut stream).await, None);
865    }
866
867    /// The sequence after the last message is where the next append arrives: the seek succeeds
868    /// and the subscription delivers nothing the file already holds.
869    #[tokio::test(start_paused = true)]
870    async fn a_seek_to_the_sequence_after_the_last_lands_at_the_tip() {
871        let mut subscriber =
872            FileSubscriber::spawn(&Handle::current(), "orders".to_owned(), Log::of(3), || {
873                false
874            });
875        let seeker = subscriber.seeker();
876        let mut stream = std::pin::pin!(subscriber.stream());
877        assert_eq!(next_body(&mut stream).await, Some(1));
878
879        tokio::time::timeout(STUCK, seeker.seek(FilePosition::sequence(4)))
880            .await
881            .expect("the seek completes")
882            .expect("the sequence the file writes next is a position");
883
884        assert_eq!(next_body(&mut stream).await, None);
885    }
886
887    /// The first sequence of a stream key is the start of the file, and a seek there replays
888    /// everything from it.
889    #[tokio::test(start_paused = true)]
890    async fn a_seek_to_the_first_sequence_replays_from_the_start() {
891        let mut subscriber =
892            FileSubscriber::spawn(&Handle::current(), "orders".to_owned(), Log::of(3), || {
893                false
894            });
895        let seeker = subscriber.seeker();
896        let mut stream = std::pin::pin!(subscriber.stream());
897        assert_eq!(next_body(&mut stream).await, Some(1));
898        assert_eq!(next_body(&mut stream).await, Some(2));
899
900        tokio::time::timeout(STUCK, seeker.seek(FilePosition::sequence(1)))
901            .await
902            .expect("the seek completes")
903            .expect("the first sequence is a position");
904
905        for expected in 1..=3 {
906            assert_eq!(next_body(&mut stream).await, Some(expected));
907        }
908        assert_eq!(next_body(&mut stream).await, None);
909    }
910
911    /// A refused seek leaves the subscription where it was: what was already read and queued
912    /// for the handler is still delivered, then the rest of the file.
913    #[tokio::test(start_paused = true)]
914    async fn a_refused_seek_keeps_what_was_queued() {
915        let mut subscriber =
916            FileSubscriber::spawn(&Handle::current(), "orders".to_owned(), Log::of(5), || {
917                false
918            });
919        let seeker = subscriber.seeker();
920        let mut stream = std::pin::pin!(subscriber.stream());
921        assert_eq!(next_body(&mut stream).await, Some(1));
922        // Lets the driver queue the rest of the file and its end.
923        tokio::task::yield_now().await;
924
925        tokio::time::timeout(STUCK, seeker.seek(FilePosition::sequence(9)))
926            .await
927            .expect("the seek completes")
928            .expect_err("no message carries that sequence");
929
930        for expected in 2..=5 {
931            assert_eq!(next_body(&mut stream).await, Some(expected));
932        }
933        assert_eq!(next_body(&mut stream).await, None);
934    }
935}