Skip to main content

ruststream_sea_file/
message.rs

1//! [`SeaMessage`]: a delivered message, shared by the file and stdio transports.
2
3use std::future::{Future, ready};
4
5use bytes::Bytes;
6use ruststream::{AckError, HeaderMap, IncomingMessage, Positioned, Str};
7use sea_streamer_types::{Buffer as _, Message as _, SharedMessage};
8
9#[cfg(feature = "testing")]
10use crate::in_process::Release;
11use crate::wire;
12
13/// Header exposing the message's sequence number within its stream.
14pub const SEQUENCE_HEADER: &str = "stream-sequence";
15
16/// A position in a stream file's retained log: where `start_at(..)` opens a subscription, and
17/// where [`Seeker::seek`](ruststream::Seeker::seek) moves a running one.
18///
19/// Captured positions ([`Positioned::position`]) carry the pinned semantics the framework
20/// defines: seeking to one redelivers exactly that message (the transport's sequence rewind
21/// is inclusive). The other forms keep the transport's own semantics: `Beginning` replays
22/// everything retained, `End` skips to the tip, and `Timestamp` resumes at the earliest
23/// message strictly later than the instant (milliseconds since the Unix epoch).
24///
25/// Seek to a position the stream has reached, or to the sequence it writes next, which is the
26/// tip: a service that handled every message resumes there. A sequence further ahead, and an
27/// instant later than every message it holds, are both refused - the transport reads forward
28/// looking for the message and runs out of file - and the subscription ends with that refusal
29/// rather than staying where it was. A captured position, the beginning and the tip are always
30/// safe; a number a service worked out for itself is what needs the care.
31///
32/// The
33/// [FilePosition](https://powersemmi.github.io/ruststream-sea-file/latest/components/positions/)
34/// page shows it in a service.
35///
36/// # Examples
37///
38/// A handler reads its delivery's place in the file with `Ctx<Position>` and moves its own
39/// subscription with `Ctx<SeekHandle>`:
40///
41/// ```
42/// use ruststream_sea_file::file::prelude::*;
43/// use serde::Deserialize;
44///
45/// #[derive(Debug, Deserialize)]
46/// struct Job {
47///     id: u64,
48///     poisoned: bool,
49/// }
50///
51/// #[subscriber(FileStream::new("jobs"), start_at(FilePosition::beginning()))]
52/// async fn work(
53///     job: &Job,
54///     Ctx(at): Ctx<Position>,
55///     Ctx(seeker): Ctx<SeekHandle>,
56/// ) -> HandlerOutcome {
57///     if job.poisoned && seeker.seek(FilePosition::end()).await.is_err() {
58///         return HandlerOutcome::retry();
59///     }
60///     tracing::info!(job.id, position = ?at, "worked");
61///     HandlerOutcome::ack()
62/// }
63///
64/// #[ruststream::app]
65/// fn app() -> impl App {
66///     RustStream::new(AppInfo::new("jobs", "0.1.0"))
67///         .with_broker(FileBroker::new("/tmp/jobs.ss"), |b| {
68///             b.include(work);
69///         })
70/// }
71/// ```
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum FilePosition {
74    /// Everything retained.
75    Beginning,
76    /// The tip of the stream.
77    End,
78    /// A message sequence (inclusive). A stream key is numbered from one, and the numbering
79    /// carries on when a later connection appends to the same file.
80    Sequence(u64),
81    /// Milliseconds since the Unix epoch (exclusive).
82    Timestamp(u64),
83}
84
85// Constructor forms of the variants: the `start_at(..)` macro clause resolves the position's
86// type from a constructor call, which a bare unit-variant path does not provide.
87impl FilePosition {
88    /// Everything retained: [`FilePosition::Beginning`].
89    #[must_use]
90    pub const fn beginning() -> Self {
91        Self::Beginning
92    }
93
94    /// The tip of the stream: [`FilePosition::End`].
95    #[must_use]
96    pub const fn end() -> Self {
97        Self::End
98    }
99
100    /// A message sequence, redelivered inclusively: [`FilePosition::Sequence`].
101    #[must_use]
102    pub const fn sequence(sequence: u64) -> Self {
103        Self::Sequence(sequence)
104    }
105
106    /// Milliseconds since the Unix epoch, resuming strictly later:
107    /// [`FilePosition::Timestamp`].
108    #[must_use]
109    pub const fn timestamp(millis: u64) -> Self {
110        Self::Timestamp(millis)
111    }
112}
113
114/// A message delivered by one of this crate's subscribers.
115///
116/// This is the whole delivery on standard input; a stream file wraps it in a
117/// [`FileMessage`](crate::FileMessage), which adds the subscription's reposition handle.
118///
119/// The transport keeps no consumer positions (its resumable mode is unimplemented upstream),
120/// so acknowledgement reports [`AckError::Unsupported`] rather than pretending; resume
121/// explicitly via the descriptor's start position or a captured [`FilePosition`].
122pub struct SeaMessage {
123    payload: Bytes,
124    headers: HeaderMap,
125    stream: String,
126    sequence: u64,
127    /// Tells the test harness this delivery is done when it is dropped: the in-process transport
128    /// counts its deliveries in flight. The field is there only with the `testing` feature.
129    #[cfg(feature = "testing")]
130    release: Option<Release>,
131}
132
133// The zero-cost promise of the in-process mode, held by the compiler: a build without it gives the
134// delivery exactly the fields a delivery off a file or a pipe needs.
135#[cfg(not(feature = "testing"))]
136const _: () = assert!(size_of::<SeaMessage>() == size_of::<(Bytes, HeaderMap, String, u64)>());
137
138impl std::fmt::Debug for SeaMessage {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        f.debug_struct("SeaMessage")
141            .field("stream", &self.stream)
142            .field("sequence", &self.sequence)
143            .field("payload_len", &self.payload.len())
144            .finish_non_exhaustive()
145    }
146}
147
148impl SeaMessage {
149    pub(crate) fn new(message: &SharedMessage) -> Self {
150        let (mut headers, payload) = wire::decode(message.message().as_bytes());
151        let sequence = message.sequence();
152        headers.insert(Str::from_static(SEQUENCE_HEADER), sequence.to_string());
153        Self {
154            payload,
155            headers,
156            stream: message.stream_key().name().to_owned(),
157            sequence,
158            #[cfg(feature = "testing")]
159            release: None,
160        }
161    }
162
163    /// The same delivery, counted in flight by the harness until it is dropped.
164    #[cfg(feature = "testing")]
165    pub(crate) fn released_by(mut self, release: Option<Release>) -> Self {
166        self.release = release;
167        self
168    }
169
170    /// The stream key this message was published to.
171    #[must_use]
172    pub fn stream(&self) -> &str {
173        &self.stream
174    }
175}
176
177impl Positioned for SeaMessage {
178    type Position = FilePosition;
179
180    fn position(&self) -> FilePosition {
181        FilePosition::Sequence(self.sequence)
182    }
183}
184
185impl IncomingMessage for SeaMessage {
186    fn payload(&self) -> &[u8] {
187        &self.payload
188    }
189
190    fn headers(&self) -> &HeaderMap {
191        &self.headers
192    }
193
194    fn ack(self) -> impl Future<Output = Result<(), AckError>> {
195        ready(Err(AckError::Unsupported))
196    }
197
198    fn nack(self, _requeue: bool) -> impl Future<Output = Result<(), AckError>> {
199        ready(Err(AckError::Unsupported))
200    }
201}