1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
//! The contexts a Pulsar subscription hands its handlers, and the keys that read them.
//!
//! Pulsar topics are a retained log, so the two things a body needs to reposition itself are
//! per-delivery state: where this message sits ([`Position`]) and the handle that moves the
//! subscription ([`SeekHandle`]). Both ride [`PulsarContext`], built once per delivery; a batch
//! spans many deliveries, so [`PulsarBatchContext`] carries only the subscription-scoped half.
use ;
use crate;
use cratePulsarSeeker;
/// The per-delivery context of a Pulsar subscription: this message's position in the topic's
/// retained log, and the subscription's own seeker.
///
/// The runtime builds one per dispatched delivery (see [`BuildContext`]), and handlers read its
/// fields by key - [`Position`] and [`SeekHandle`] - through the `Ctx` extractor or
/// `ctx.context(..)`. A body that repositions its own subscription names this type as its `C`
/// axis and needs nothing else.
///
/// Building it costs no allocation: the position is the delivery's message id, and the seeker
/// is a clone of the channel to the subscription's driver task.
///
/// # Examples
///
/// ```
/// # mod demo {
/// use ruststream_pulsar::prelude::*;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Job {
/// id: u64,
/// rewind: bool,
/// }
///
/// /// Logs where each job sits, and replays the retained backlog when a job asks for it.
/// #[subscriber(PulsarSubscription::new("jobs", "workers"))]
/// async fn work(job: &Job, ctx: &mut Context<'_, PulsarContext>) -> HandlerOutcome {
/// tracing::info!(job = job.id, position = ?ctx.context(Position), "job received");
/// if job.rewind
/// && ctx
/// .context(SeekHandle)
/// .seek(PulsarPosition::earliest())
/// .await
/// .is_err()
/// {
/// return HandlerOutcome::retry();
/// }
/// HandlerOutcome::ack()
/// }
/// # }
/// # fn main() {}
/// ```
/// The subscription-scoped batch context of a Pulsar subscription: its seeker, shared by every
/// delivery of the batch.
///
/// The runtime builds one per dispatched batch from the batch's first delivery (see
/// [`BuildBatchContext`]), and a batch body reads it by key - [`SeekHandle`] - through
/// `ctx.context(..)`. A [`Position`] has no place here: a batch spans many deliveries, so where
/// to seek rides the elements themselves (a `&[Message<H, T>]` batch reads it off each element's
/// header contract). Keeping this a separate type from [`PulsarContext`] is what rejects a batch
/// body asking for per-delivery fields at compile time.
///
/// Pulsar's client hands over one delivery at a time, so a batch here is assembled on the client
/// from the size the mount site's `batch(n)` names. The body sees the same context, seeker and
/// settlement as on a broker that batches on the wire.
///
/// # Examples
///
/// ```
/// # mod demo {
/// use ruststream_pulsar::prelude::*;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Job {
/// id: u64,
/// rewind: bool,
/// }
///
/// /// A batch that carries the rewind marker replays the retained backlog once it is settled:
/// /// the next batch opens at the beginning of the log.
/// #[subscriber(PulsarSubscription::new("jobs.bulk", "bulk-workers"))]
/// async fn work(jobs: &[Job], ctx: &mut Context<'_, PulsarBatchContext>) -> HandlerOutcome {
/// let first = jobs.first().map(|job| job.id);
/// tracing::info!(count = jobs.len(), ?first, "jobs received");
/// if jobs.iter().any(|job| job.rewind)
/// && ctx
/// .context(SeekHandle)
/// .seek(PulsarPosition::earliest())
/// .await
/// .is_err()
/// {
/// return HandlerOutcome::retry();
/// }
/// HandlerOutcome::ack()
/// }
///
/// #[ruststream::app]
/// fn app() -> impl App {
/// RustStream::new(AppInfo::new("jobs", "0.1.0")).with_broker(
/// PulsarBroker::new("pulsar://localhost:6650"),
/// |b| {
/// b.include(work.batch(nonzero!(100)));
/// },
/// )
/// }
/// # }
/// # fn main() {}
/// ```
/// The key reading this delivery's [`PulsarPosition`] out of [`PulsarContext`].
///
/// The value is the delivery's own message id, so it carries the framework's pinned contract:
/// seeking to it redelivers exactly this message.
///
/// A message id is not `Copy`, so `ctx.context(Position)` borrows it and only the `Ctx<Position>`
/// extractor - which must yield an owned value - copies one per delivery.
///
/// # Examples
///
/// ```
/// use ruststream_pulsar::Position;
/// use ruststream_pulsar::prelude::*;
/// # #[derive(serde::Deserialize)]
/// # struct Job { id: u64 }
///
/// #[subscriber(PulsarSubscription::new("jobs", "auditors"))]
/// async fn audit(job: &Job, Ctx(at): Ctx<Position>) -> HandlerOutcome {
/// tracing::info!(job = job.id, position = ?at, "job audited");
/// HandlerOutcome::ack()
/// }
/// ```
;
/// The key reading the subscription's [`PulsarSeeker`] out of [`PulsarContext`] (and out of
/// [`PulsarBatchContext`] on the batch path): the reposition handle every delivery of the
/// subscription shares.
///
/// One seek covers every topic and every partition of the subscription's consumer.
///
/// # Examples
///
/// ```
/// use ruststream_pulsar::SeekHandle;
/// use ruststream_pulsar::prelude::*;
/// # #[derive(serde::Deserialize)]
/// # struct Job { id: u64, poisoned_until: Option<u64> }
///
/// /// Skips forward past a region the producer marked poisoned.
/// #[subscriber(PulsarSubscription::new("jobs", "workers"))]
/// async fn work(job: &Job, Ctx(seeker): Ctx<SeekHandle>) -> HandlerOutcome {
/// if let Some(resume_at) = job.poisoned_until
/// && seeker.seek(PulsarPosition::timestamp(resume_at)).await.is_err()
/// {
/// return HandlerOutcome::retry();
/// }
/// HandlerOutcome::ack()
/// }
/// ```
;