1use std::time::Duration;
2
3use serde_json::{Value, json};
4use sofie_core::variants::{self, Variants};
5use sofie_core::{Cast, ColumnReader, Error, Result, SqlValue, Timestamp, Uuid};
6use sofie_telemetry::TraceParent;
7
8use crate::heard::{Known, Listeners};
9use crate::{BoxFuture, Db, Dialect, Stamp, Step, Wakeups, query_decoded, query_values};
10
11pub struct MessageSql {
14 pub insert: &'static str,
15 pub fan_out: &'static str,
17 pub fan_out_events: &'static str,
20 pub claim: &'static str,
23 pub renew: &'static str,
25 pub complete: &'static str,
26 pub retry: &'static str,
27 pub bury: &'static str,
29 pub to_job: &'static str,
31 pub add_schedule: &'static str,
32 pub fire_schedule: &'static str,
35 pub notify: Option<&'static str>,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum MessageKind {
42 Event,
43 Job,
44}
45
46impl Variants for MessageKind {
47 const TYPE: &'static str = "message kind";
48 const VARIANTS: &'static [(&'static str, Self)] = &[("event", Self::Event), ("job", Self::Job)];
49}
50
51#[derive(Debug)]
53pub struct Claimed {
54 pub id: Uuid,
55 pub kind: MessageKind,
56 pub name: String,
57 pub payload: Value,
58 pub attempt: i64,
59 pub traceparent: Option<TraceParent>,
60 pub available_at: Timestamp,
61 pub actor: Option<Uuid>,
62}
63
64impl Claimed {
65 fn decode(row: &mut dyn ColumnReader) -> Result<Self> {
66 Ok(Self {
67 id: row.uuid()?,
68 kind: variants::decode(row)?,
69 name: row.text()?.to_owned(),
70 payload: row.json()?,
71 attempt: row.int()?,
72 traceparent: traceparent(row)?,
73 available_at: row.timestamp()?,
74 actor: Option::<Uuid>::decode(row)?,
75 })
76 }
77
78 fn fenced(&self, more: impl IntoIterator<Item = SqlValue>) -> Vec<SqlValue> {
79 let mut params = vec![SqlValue::Uuid(self.id), SqlValue::Int(self.attempt)];
80 params.extend(more);
81 params
82 }
83}
84
85fn traceparent(row: &mut dyn ColumnReader) -> Result<Option<TraceParent>> {
86 if row.is_null()? {
87 row.skip()?;
88 return Ok(None);
89 }
90 Ok(TraceParent::parse(row.text()?))
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub struct Fired {
95 pub fired: bool,
96 pub next_at: Timestamp,
97}
98
99impl Fired {
100 fn decode(row: &mut dyn ColumnReader) -> Result<Self> {
101 Ok(Self {
102 next_at: row.timestamp()?,
103 fired: row.bool()?,
104 })
105 }
106}
107
108struct Message<'a> {
109 kind: MessageKind,
110 name: &'a str,
111 payload: Value,
112 available_at: Timestamp,
113 actor: Option<Uuid>,
114}
115
116impl Message<'_> {
117 fn columns(self) -> Result<Vec<SqlValue>> {
118 Ok(vec![
119 SqlValue::Uuid(Uuid::now_v7()?),
120 SqlValue::Text(self.kind.name().to_owned()),
121 SqlValue::Text(self.name.to_owned()),
122 SqlValue::Json(self.payload),
123 SqlValue::Timestamp(self.available_at),
124 current_traceparent(),
125 self.actor.map_or(SqlValue::Null, SqlValue::Uuid),
126 ])
127 }
128}
129
130pub(crate) fn keyed_job(
131 dialect: &dyn Dialect,
132 name: &str,
133 payload: Value,
134 key: Uuid,
135 available_at: Timestamp,
136 actor: Option<Uuid>,
137) -> Result<Step> {
138 let mut params = Message {
139 kind: MessageKind::Job,
140 name,
141 payload,
142 available_at,
143 actor,
144 }
145 .columns()?;
146 params.push(SqlValue::Uuid(key));
147 Ok(Step::statement(dialect.messages().fan_out, params, None))
148}
149
150#[derive(Clone)]
152pub struct Messages {
153 db: Db,
154}
155
156impl Messages {
157 #[must_use]
158 pub fn new(db: Db) -> Self {
159 Self { db }
160 }
161
162 fn sql(&self) -> &'static MessageSql {
163 self.db.driver().dialect().messages()
164 }
165
166 pub async fn claim(&self, lease: Duration, limit: i64) -> Result<Vec<Claimed>> {
167 let now = self.db.clock().now().timestamp();
168 claim_known(&self.db, lease, limit, &Known::default(), now).await
169 }
170
171 pub async fn renew(&self, message: &Claimed, lease: Duration) -> Result<()> {
173 let params = message.fenced([milliseconds(lease)?]);
174 let step = Step::statement(self.sql().renew, params, Some(1));
175 self.db.transaction(vec![step]).await
176 }
177
178 #[must_use]
180 pub fn completion(&self, message: &Claimed) -> Step {
181 Step::statement(self.sql().complete, message.fenced([]), Some(1))
182 }
183
184 pub async fn retry(
186 &self,
187 message: &Claimed,
188 listener: Option<&str>,
189 error: &str,
190 delay: Duration,
191 ) -> Result<()> {
192 let due = later(self.db.clock().now().timestamp(), delay)?;
193 let due = Some(SqlValue::Timestamp(due));
194 self.release(self.sql().retry, message, listener, error, due)
195 .await
196 }
197
198 pub async fn bury(&self, message: &Claimed, listener: Option<&str>, error: &str) -> Result<()> {
199 self.release(self.sql().bury, message, listener, error, None)
200 .await
201 }
202
203 async fn release(
206 &self,
207 sql: &'static str,
208 message: &Claimed,
209 listener: Option<&str>,
210 error: &str,
211 due: Option<SqlValue>,
212 ) -> Result<()> {
213 let params = message.fenced(std::iter::once(SqlValue::Text(error.to_owned())).chain(due));
214 let mut steps = Vec::with_capacity(2);
215 if let Some(listener) = listener {
216 let params = message.fenced([SqlValue::Text(listener.to_owned())]);
217 steps.push(Step::statement(self.sql().to_job, params, Some(1)));
218 }
219 steps.push(Step::statement(sql, params, Some(1)));
220 self.db.transaction(steps).await
221 }
222
223 pub async fn add_schedule(
224 &self,
225 name: &str,
226 first_at: Timestamp,
227 cadence: &str,
228 previous_cadence: &str,
229 ) -> Result<()> {
230 let params = [
231 SqlValue::Text(name.to_owned()),
232 SqlValue::Timestamp(first_at),
233 SqlValue::Text(cadence.to_owned()),
234 SqlValue::Text(previous_cadence.to_owned()),
235 ];
236 let sql = self.sql().add_schedule;
237 query_values(self.db.driver(), sql, ¶ms).await.map(drop)
238 }
239
240 pub async fn queue_schedule(&self, name: &str) -> Result<()> {
242 let dialect = self.db.driver().dialect();
243 let payload = Value::Object(serde_json::Map::new());
244 let now = self.db.clock().now().timestamp();
245 let mut steps = Vec::new();
246 let queuing = Queuing {
247 kind: MessageKind::Job,
248 at: now,
249 actor: None,
250 };
251 queue_steps(dialect, &queuing, [(name, payload)], None, &mut steps)?;
252 if let Some(notify) = dialect.messages().notify {
254 steps.push(Step::Script(notify.to_owned()));
255 }
256 self.db.transaction(steps).await
257 }
258
259 pub async fn fire_schedule(
261 &self,
262 name: &str,
263 now: Timestamp,
264 next_at: Timestamp,
265 ) -> Result<Option<Fired>> {
266 let payload = Value::Object(serde_json::Map::new());
267 let mut params = Message {
268 kind: MessageKind::Job,
269 name,
270 payload,
271 available_at: now,
272 actor: None,
273 }
274 .columns()?;
275 params.push(SqlValue::Timestamp(next_at));
276 let sql = self.sql().fire_schedule;
277 let fired =
278 query_decoded(self.db.driver(), sql, ¶ms, Vec::new(), Fired::decode).await?;
279 Ok(fired.into_iter().next())
280 }
281
282 pub(crate) async fn arrived(&self) {
283 self.db.arrivals().arrived().await;
284 }
285
286 #[must_use]
287 pub fn wakeups(&self) -> Option<BoxFuture<'_, Result<Box<dyn Wakeups>>>> {
288 self.db.driver().wakeups()
289 }
290}
291
292pub(crate) async fn claim_known(
293 db: &Db,
294 lease: Duration,
295 limit: i64,
296 mine: &Known,
297 held_until: Timestamp,
298) -> Result<Vec<Claimed>> {
299 let params = [
300 milliseconds(lease)?,
301 SqlValue::Timestamp(db.clock().now().timestamp()),
302 SqlValue::Int(limit),
303 SqlValue::Json(mine.to_json()),
304 SqlValue::Timestamp(held_until),
305 ];
306 let sql = db.driver().dialect().messages().claim;
307 query_decoded(db.driver(), sql, ¶ms, Vec::new(), Claimed::decode).await
308}
309
310pub async fn ping_queue(db: &Db) -> Result<()> {
311 let messages = db.driver().dialect().qualified("sofie", "messages");
312 let sql = format!("SELECT 1 FROM {messages} LIMIT 1");
313 query_values(db.driver(), &sql, &[]).await.map(drop)
314}
315
316pub(crate) struct Queuing {
317 kind: MessageKind,
318 at: Timestamp,
319 actor: Option<Uuid>,
320}
321
322impl Stamp {
323 pub(crate) fn queuing(&self, kind: MessageKind, at: Timestamp) -> Queuing {
324 Queuing {
325 kind,
326 at,
327 actor: self.actor,
328 }
329 }
330}
331
332pub(crate) fn queue_steps<'a>(
333 dialect: &dyn Dialect,
334 queuing: &Queuing,
335 messages: impl IntoIterator<Item = (&'a str, Value)>,
336 heard: Option<&Known>,
337 steps: &mut Vec<Step>,
338) -> Result<bool> {
339 if let Some(heard) = heard.filter(|_| queuing.kind == MessageKind::Event) {
340 return fan_out_raised(dialect, queuing, messages, &heard.listeners, steps);
341 }
342 let mut queued = false;
343 for (name, payload) in messages {
344 let params = Message {
345 kind: queuing.kind,
346 name,
347 payload,
348 available_at: queuing.at,
349 actor: queuing.actor,
350 }
351 .columns()?;
352 steps.push(Step::statement(dialect.messages().insert, params, None));
353 queued = true;
354 }
355 Ok(queued)
356}
357
358fn fan_out_raised<'a>(
359 dialect: &dyn Dialect,
360 queuing: &Queuing,
361 events: impl IntoIterator<Item = (&'a str, Value)>,
362 listeners: &Listeners,
363 steps: &mut Vec<Step>,
364) -> Result<bool> {
365 let traceparent = sofie_telemetry::current_traceparent().map(|parent| parent.to_string());
366 let mut raised = Vec::new();
367 for (name, payload) in events {
368 raised.push(Fanned {
369 id: Uuid::now_v7()?,
370 name,
371 payload,
372 traceparent: traceparent.clone(),
373 });
374 }
375 let Some(step) = fan_out_events(dialect, raised, queuing.at, queuing.actor, listeners)? else {
376 return Ok(false);
377 };
378 steps.push(step);
379 Ok(true)
380}
381
382pub(crate) struct Fanned<'a> {
383 pub(crate) id: Uuid,
384 pub(crate) name: &'a str,
385 pub(crate) payload: Value,
386 pub(crate) traceparent: Option<String>,
387}
388
389pub(crate) fn fan_out_events<'a>(
390 dialect: &dyn Dialect,
391 events: impl IntoIterator<Item = Fanned<'a>>,
392 at: Timestamp,
393 actor: Option<Uuid>,
394 listeners: &Listeners,
395) -> Result<Option<Step>> {
396 let (mut taken, mut jobs) = (Vec::new(), Vec::new());
397 for event in events {
398 let taking = listeners.taking(event.name, &event.payload);
399 if taking.is_empty() {
400 continue;
401 }
402 let id = event.id.to_string();
403 for listener in taking.iter() {
404 let job = Uuid::now_v7()?.to_string();
405 jobs.push(json!({ "id": job, "name": listener, "event": id }));
406 }
407 taken.push(json!({ "id": id, "payload": event.payload, "traceparent": event.traceparent }));
408 }
409 if jobs.is_empty() {
410 return Ok(None);
411 }
412 let params = vec![
413 SqlValue::Json(Value::Array(taken)),
414 SqlValue::Json(Value::Array(jobs)),
415 SqlValue::Timestamp(at),
416 actor.map_or(SqlValue::Null, SqlValue::Uuid),
417 ];
418 let sql = dialect.messages().fan_out_events;
419 Ok(Some(Step::statement(sql, params, None)))
420}
421
422pub(crate) fn current_traceparent() -> SqlValue {
423 sofie_telemetry::current_traceparent().map_or(SqlValue::Null, |traceparent| {
424 SqlValue::Text(traceparent.to_string())
425 })
426}
427
428fn milliseconds(lease: Duration) -> Result<SqlValue> {
429 i64::try_from(lease.as_millis())
430 .map(SqlValue::Int)
431 .map_err(|error| Error::internal("a lease too long to store", error))
432}
433
434fn later(now: Timestamp, by: Duration) -> Result<Timestamp> {
435 now.checked_add(by)
436 .map_err(|error| Error::internal("computing a message time", error))
437}