use std::any::type_name;
use std::time::Duration;
use ruststream::RetryDeclaration;
use ruststream_sqlx_dialect::{ClaimShape, Form, Role, TableName, TableSpec};
use crate::inbox::InboxRow;
use crate::inbox::database::QueueDatabase;
use crate::inbox::engine::{Events, IdAt, Shape};
use crate::inbox::error::SqlxBrokerError;
use crate::inbox::named::kinds::Kinds;
use crate::inbox::publish::table_of;
use crate::inbox::threads::InboxThreads;
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct Timing {
pub(crate) poll_interval: Option<Duration>,
pub(crate) lease: Option<Duration>,
pub(crate) threads: Option<InboxThreads>,
}
pub(super) fn whole_seconds(lease: Duration) -> Duration {
let seconds = lease
.as_secs()
.saturating_add(u64::from(lease.subsec_nanos() > 0));
Duration::from_secs(seconds.max(1))
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Description {
pub(crate) spec: TableSpec<'static>,
pub(crate) shape: Shape,
pub(crate) claim: ClaimShape,
pub(crate) row: &'static str,
pub(crate) kinds: Option<Kinds>,
}
impl Description {
pub(crate) fn of<DB, Row>() -> Self
where
DB: QueueDatabase,
Row: InboxRow + Events<DB>,
{
let shape = Row::SHAPE;
Self {
spec: Row::SPEC,
shape,
claim: if shape.custom_fetch {
ClaimShape::Ids
} else {
ClaimShape::Rows
},
row: type_name::<Row>(),
kinds: Row::kinds(),
}
}
pub(crate) fn by_role(self) -> Self {
Self {
shape: Shape::default(),
claim: ClaimShape::Roles,
..self
}
}
pub(crate) const fn native_retry_after(&self) -> bool {
self.shape.custom_retry_after || self.spec.column(Role::RetryAfter).is_some()
}
pub(crate) const fn leased(&self) -> bool {
self.spec.column(Role::LockedUntil).is_some()
}
pub(crate) const fn advisory(&self) -> bool {
matches!(self.spec.form(), Form::Advisory(_))
}
pub(crate) const fn id_at(&self) -> IdAt {
match self.claim {
ClaimShape::Roles => IdAt::First,
ClaimShape::Rows | ClaimShape::Ids => IdAt::of(&self.spec),
}
}
}
pub(crate) fn refused_declaration(
name: &str,
declaration: &RetryDeclaration,
description: &Description,
) -> Option<SqlxBrokerError> {
let spec = description.spec;
let refuse = |reason: String| SqlxBrokerError::Declaration {
subscription: name.to_owned(),
table: table_of(&spec),
row: description.row,
reason,
};
match (declaration.max_attempts(), declaration.dead_letter()) {
(Some(_), None) => {
return Some(refuse(
"the registration declares `max_attempts(..)` without `dead_letter(..)`: name \
where a row whose attempts are spent goes"
.to_owned(),
));
}
(None, Some(_)) => {
return Some(refuse(
"the registration declares `dead_letter(..)` without `max_attempts(..)`: name the \
cap, `max_attempts(1)` to move a row at its first failure"
.to_owned(),
));
}
_ => {}
}
if declaration.max_attempts().is_some() && spec.column(Role::Attempt).is_none() {
return Some(refuse(
"`max_attempts(..)` counts deliveries in the `attempt` column: add \
`#[field(attempt)]` to the struct"
.to_owned(),
));
}
match declaration.dead_letter() {
Some(target) if spec.column(Role::Group).is_none() => TableName::parse(target)
.err()
.map(|err| refuse(format!("the dead-letter table {err}"))),
_ => None,
}
}
#[cfg(test)]
pub(super) mod tests {
use std::time::Duration;
use ruststream_sqlx_dialect::{ClaimShape, Column, TableSpec};
use super::{Description, whole_seconds};
use crate::inbox::engine::{IdAt, Shape};
use crate::inbox::form::tests::JOBS;
pub(in crate::inbox) fn described(
spec: &TableSpec<'static>,
shape: Shape,
claim: ClaimShape,
) -> Description {
Description {
spec: *spec,
shape,
claim,
row: "Job",
kinds: None,
}
}
#[test]
fn a_delayed_retry_is_native_with_its_column_or_the_services_event() {
let bare = described(&JOBS, Shape::default(), ClaimShape::Rows);
assert!(!bare.native_retry_after());
let column = described(
&JOBS.retry_after(Column::new("retry_after")),
Shape::default(),
ClaimShape::Rows,
);
assert!(column.native_retry_after());
let custom = Shape {
custom_retry_after: true,
..Shape::default()
};
assert!(described(&JOBS, custom, ClaimShape::Rows).native_retry_after());
}
#[test]
fn a_lease_is_whole_seconds_and_at_least_one() {
assert_eq!(
whole_seconds(Duration::from_secs(30)),
Duration::from_secs(30)
);
assert_eq!(
whole_seconds(Duration::from_millis(1500)),
Duration::from_secs(2)
);
assert_eq!(whole_seconds(Duration::ZERO), Duration::from_secs(1));
assert_eq!(
whole_seconds(Duration::MAX),
Duration::from_secs(u64::MAX),
"the longest lease saturates instead of wrapping"
);
}
#[test]
fn a_claim_by_role_carries_the_id_first_whatever_the_struct_flattens() {
let flat = JOBS.selecting_all();
assert_eq!(
described(&flat, Shape::default(), ClaimShape::Rows).id_at(),
IdAt::Named("job_id")
);
assert_eq!(
described(&flat, Shape::default(), ClaimShape::Roles).id_at(),
IdAt::First
);
assert_eq!(
described(&JOBS, Shape::default(), ClaimShape::Rows).id_at(),
IdAt::First
);
}
}