Skip to main content

ruststream_kinesis/
context.rs

1//! The delivery context this broker publishes, and the keys that read it.
2//!
3//! Kinesis is a repositionable log, so a handler may want two things the payload does not carry:
4//! where this record sits, and the handle that moves the subscription. Both ride the framework's
5//! typed context - [`KinesisContext`] per delivery, [`KinesisBatchContext`] per batch - and are
6//! read by the compile-time keys [`Position`] and [`SeekHandle`], so a key this broker does not
7//! carry is a compile error rather than a lookup that finds nothing.
8
9use std::sync::Arc;
10
11use ruststream::{BuildBatchContext, BuildContext, ContextField, Field};
12
13use crate::message::{KinesisMessage, KinesisPosition};
14use crate::subscriber::KinesisSeeker;
15
16/// This broker's per-delivery context: where the record sits in the log, and the handle that
17/// repositions the subscription it arrived on.
18///
19/// The runtime builds one per dispatched record, so a handler names it as the context type of
20/// its body and reads the fields by key - [`Position`] and [`SeekHandle`] - through the
21/// `Ctx` extractor or `ctx.context(..)`. Building it costs three reference-count bumps: the
22/// shard id and the sequence number are shared with the delivery, and the seeker is the one the
23/// subscription minted when it opened.
24///
25/// # Examples
26///
27/// ```
28/// # mod demo {
29/// use ruststream_kinesis::prelude::*;
30/// use serde::Deserialize;
31///
32/// #[derive(Deserialize)]
33/// struct Job {
34///     id: u64,
35/// }
36///
37/// /// Logs where each job sits, and follows the tip from the marker job on.
38/// #[subscriber(KinesisStream::new("jobs"))]
39/// async fn follow(job: &Job, ctx: &mut Context<'_, KinesisContext>) -> HandlerOutcome {
40///     tracing::info!(job.id, position = ?ctx.context(Position), "job read");
41///     if job.id == u64::MAX
42///         && ctx
43///             .context(SeekHandle)
44///             .seek(KinesisPosition::latest())
45///             .await
46///             .is_err()
47///     {
48///         return HandlerOutcome::retry();
49///     }
50///     HandlerOutcome::ack()
51/// }
52///
53/// #[ruststream::app]
54/// fn app() -> impl App {
55///     RustStream::new(AppInfo::new("jobs", "0.1.0")).with_broker(KinesisBroker::new(), |b| {
56///         b.include(follow);
57///     })
58/// }
59/// # }
60/// # fn main() {}
61/// ```
62#[derive(Debug, Clone)]
63pub struct KinesisContext {
64    shard: Arc<str>,
65    sequence: Arc<str>,
66    seeker: KinesisSeeker,
67}
68
69impl BuildContext<KinesisMessage> for KinesisContext {
70    fn build(msg: &KinesisMessage) -> Self {
71        Self {
72            shard: Arc::clone(msg.shard()),
73            sequence: Arc::clone(msg.sequence()),
74            seeker: msg.seeker().clone(),
75        }
76    }
77}
78
79/// This broker's batch context: the subscription's reposition handle, shared by every record of
80/// the batch.
81///
82/// A batch spans many records, so there is no single position to report here - a body that reacts
83/// to one reads it off the elements (the `kinesis-sequence-number` and `kinesis-shard-id`
84/// headers each record carries). Keeping this a type of its own is what makes that hold at
85/// compile time: [`KinesisContext`] does not build per batch, so a batch body cannot ask for
86/// per-delivery fields.
87///
88/// # Examples
89///
90/// ```
91/// # mod demo {
92/// use ruststream_kinesis::prelude::*;
93/// use serde::Deserialize;
94///
95/// #[derive(Deserialize)]
96/// struct Job {
97///     id: u64,
98/// }
99///
100/// /// A batch carrying the rewind marker moves the whole subscription once it settles.
101/// #[subscriber(KinesisStream::new("jobs"))]
102/// async fn rewind(batch: &[Job], ctx: &mut Context<'_, KinesisBatchContext>) -> HandlerOutcome {
103///     if batch.iter().any(|job| job.id == u64::MAX)
104///         && ctx
105///             .context(SeekHandle)
106///             .seek(KinesisPosition::horizon())
107///             .await
108///             .is_err()
109///     {
110///         return HandlerOutcome::retry();
111///     }
112///     HandlerOutcome::ack()
113/// }
114///
115/// #[ruststream::app]
116/// fn app() -> impl App {
117///     RustStream::new(AppInfo::new("jobs", "0.1.0")).with_broker(KinesisBroker::new(), |b| {
118///         b.include(rewind.batch(nonzero!(100)));
119///     })
120/// }
121/// # }
122/// # fn main() {}
123/// ```
124#[derive(Debug, Clone)]
125pub struct KinesisBatchContext {
126    seeker: KinesisSeeker,
127}
128
129impl BuildBatchContext<KinesisMessage> for KinesisBatchContext {
130    fn build(first: &KinesisMessage) -> Self {
131        Self {
132            seeker: first.seeker().clone(),
133        }
134    }
135}
136
137/// The key reading this record's [`KinesisPosition`] out of [`KinesisContext`].
138///
139/// The value is the pinned shard-scoped form: seeking back to it redelivers exactly this record.
140///
141/// # Examples
142///
143/// ```
144/// use ruststream::prelude::*;
145/// use ruststream::subscriber;
146/// use ruststream_kinesis::{KinesisStream, Position};
147/// # #[derive(serde::Deserialize)]
148/// # struct Job { id: u64 }
149///
150/// #[subscriber(KinesisStream::new("jobs"))]
151/// async fn audit(job: &Job, Ctx(at): Ctx<Position>) -> HandlerOutcome {
152///     tracing::info!(job.id, position = ?at, "job read");
153///     HandlerOutcome::ack()
154/// }
155/// ```
156#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
157pub struct Position;
158
159impl ContextField for Position {
160    type Context = KinesisContext;
161    type Value = KinesisPosition;
162
163    fn read(self, src: &KinesisContext) -> KinesisPosition {
164        KinesisPosition::sequence(&*src.shard, &*src.sequence)
165    }
166}
167
168impl Field<KinesisContext> for Position {
169    // Owned rather than borrowed: the position is assembled from the two shared strings, so
170    // there is nothing in the context to hand back a reference to.
171    type Value<'a> = KinesisPosition;
172
173    fn get(self, src: &KinesisContext) -> KinesisPosition {
174        self.read(src)
175    }
176}
177
178/// The key reading the subscription's [`KinesisSeeker`] out of either context.
179///
180/// The handle is the subscription's own, minted when it opened, so a stream-wide position moves
181/// every shard it owns and a captured one moves the shard it names.
182///
183/// # Examples
184///
185/// ```
186/// use ruststream::prelude::*;
187/// use ruststream::{Seeker, subscriber};
188/// use ruststream_kinesis::{KinesisPosition, KinesisStream, SeekHandle};
189/// # #[derive(serde::Deserialize)]
190/// # struct Job { id: u64, replay_from_horizon: bool }
191///
192/// /// Rewinds the whole subscription when the producer asks for a replay.
193/// #[subscriber(KinesisStream::new("jobs"))]
194/// async fn work(job: &Job, Ctx(seeker): Ctx<SeekHandle>) -> HandlerOutcome {
195///     if job.replay_from_horizon && seeker.seek(KinesisPosition::horizon()).await.is_err() {
196///         return HandlerOutcome::retry();
197///     }
198///     HandlerOutcome::ack()
199/// }
200/// ```
201#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
202pub struct SeekHandle;
203
204impl ContextField for SeekHandle {
205    type Context = KinesisContext;
206    type Value = KinesisSeeker;
207
208    fn read(self, src: &KinesisContext) -> KinesisSeeker {
209        src.seeker.clone()
210    }
211}
212
213impl Field<KinesisContext> for SeekHandle {
214    type Value<'a> = &'a KinesisSeeker;
215
216    fn get(self, src: &KinesisContext) -> &KinesisSeeker {
217        &src.seeker
218    }
219}
220
221impl Field<KinesisBatchContext> for SeekHandle {
222    type Value<'a> = &'a KinesisSeeker;
223
224    fn get(self, src: &KinesisBatchContext) -> &KinesisSeeker {
225        &src.seeker
226    }
227}