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