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}