use std::time::Duration;
use reliar_core::{Classify, MessageId};
use crate::ordering::Ordering;
use crate::record::{OutboxRecord, truncate_error};
use crate::worker::WorkerId;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct AcquireRequest {
pub worker: WorkerId,
pub batch_size: u32,
pub lease: Duration,
pub ordering: Ordering,
}
impl AcquireRequest {
#[must_use]
pub fn new(worker: WorkerId) -> Self {
Self {
worker,
batch_size: 100,
lease: Duration::from_secs(30),
ordering: Ordering::default(),
}
}
#[must_use]
pub const fn batch_size(mut self, batch_size: u32) -> Self {
self.batch_size = batch_size;
self
}
#[must_use]
pub const fn lease(mut self, lease: Duration) -> Self {
self.lease = lease;
self
}
#[must_use]
pub const fn ordering(mut self, ordering: Ordering) -> Self {
self.ordering = ordering;
self
}
}
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct AcquiredBatch {
pub records: Vec<OutboxRecord>,
pub poisoned: Vec<PoisonedRow>,
}
impl AcquiredBatch {
#[must_use]
pub fn new(records: Vec<OutboxRecord>, poisoned: Vec<PoisonedRow>) -> Self {
Self { records, poisoned }
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.records.is_empty() && self.poisoned.is_empty()
}
}
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct DeadLetterPage {
pub records: Vec<OutboxRecord>,
pub poisoned: Vec<PoisonedRow>,
pub next_after_sequence: Option<i64>,
}
impl DeadLetterPage {
#[must_use]
pub fn new(
records: Vec<OutboxRecord>,
poisoned: Vec<PoisonedRow>,
next_after_sequence: Option<i64>,
) -> Self {
Self {
records,
poisoned,
next_after_sequence,
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.records.is_empty() && self.poisoned.is_empty()
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct PoisonedRow {
pub id: MessageId,
pub sequence: i64,
pub error: String,
}
impl PoisonedRow {
#[must_use]
pub fn new(id: MessageId, sequence: i64, error: impl Into<String>) -> Self {
Self {
id,
sequence,
error: truncate_error(error.into()),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct MessageRef {
pub id: MessageId,
pub created_at: time::OffsetDateTime,
}
impl MessageRef {
#[must_use]
pub const fn new(id: MessageId, created_at: time::OffsetDateTime) -> Self {
Self { id, created_at }
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct CompletedMessage {
pub message: MessageRef,
}
impl CompletedMessage {
#[must_use]
pub const fn new(message: MessageRef) -> Self {
Self { message }
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct FailedMessage {
pub message: MessageRef,
pub error: String,
pub outcome: FailureOutcome,
}
impl FailedMessage {
#[must_use]
pub fn new(message: MessageRef, error: impl Into<String>, outcome: FailureOutcome) -> Self {
Self {
message,
error: truncate_error(error),
outcome,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum FailureOutcome {
Retry {
delay: Duration,
},
Dead {
reason: DeadReason,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum DeadReason {
PermanentError,
AttemptsExhausted,
Expired,
Undecodable,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct PurgeRequest {
pub published_retention: Option<Duration>,
pub dead_retention: Option<Duration>,
pub batch_size: u32,
}
impl Default for PurgeRequest {
fn default() -> Self {
Self {
published_retention: Some(Duration::from_secs(7 * 24 * 60 * 60)),
dead_retention: None,
batch_size: 1_000,
}
}
}
impl PurgeRequest {
#[must_use]
pub const fn published_retention(mut self, retention: Option<Duration>) -> Self {
self.published_retention = retention;
self
}
#[must_use]
pub const fn dead_retention(mut self, retention: Option<Duration>) -> Self {
self.dead_retention = retention;
self
}
#[must_use]
pub const fn batch_size(mut self, batch_size: u32) -> Self {
self.batch_size = batch_size;
self
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct PurgeReport {
pub published_deleted: u64,
pub dead_deleted: u64,
pub expired_to_dead: u64,
}
impl PurgeReport {
#[must_use]
pub const fn new(published_deleted: u64, dead_deleted: u64, expired_to_dead: u64) -> Self {
Self {
published_deleted,
dead_deleted,
expired_to_dead,
}
}
#[must_use]
#[allow(
clippy::cast_lossless,
reason = "widening u32 -> u64; `u64::from` is not callable from a const fn on stable"
)]
pub const fn is_complete(&self, batch_size: u32) -> bool {
self.published_deleted < batch_size as u64
&& self.dead_deleted < batch_size as u64
&& self.expired_to_dead < batch_size as u64
}
}
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct OutboxStats {
pub pending: u64,
pub dead: u64,
pub expired_pending: u64,
pub oldest_pending_available_at: Option<time::OffsetDateTime>,
pub as_of: time::OffsetDateTime,
}
impl OutboxStats {
#[must_use]
pub const fn new(
pending: u64,
dead: u64,
expired_pending: u64,
oldest_pending_available_at: Option<time::OffsetDateTime>,
as_of: time::OffsetDateTime,
) -> Self {
Self {
pending,
dead,
expired_pending,
oldest_pending_available_at,
as_of,
}
}
#[must_use]
pub fn lag(&self) -> Option<Duration> {
let oldest = self.oldest_pending_available_at?;
let diff = self.as_of - oldest;
Some(if diff.is_negative() {
Duration::ZERO
} else {
diff.unsigned_abs()
})
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct DeadQuery {
pub message_type: Option<String>,
pub tenant_id: Option<String>,
pub dead_before: Option<time::OffsetDateTime>,
pub limit: u32,
pub after_sequence: Option<i64>,
}
impl Default for DeadQuery {
fn default() -> Self {
Self {
message_type: None,
tenant_id: None,
dead_before: None,
limit: 100,
after_sequence: None,
}
}
}
impl DeadQuery {
#[must_use]
pub fn message_type(mut self, message_type: impl Into<String>) -> Self {
self.message_type = Some(message_type.into());
self
}
#[must_use]
pub fn tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
self.tenant_id = Some(tenant_id.into());
self
}
#[must_use]
pub const fn dead_before(mut self, dead_before: time::OffsetDateTime) -> Self {
self.dead_before = Some(dead_before);
self
}
#[must_use]
pub const fn limit(mut self, limit: u32) -> Self {
self.limit = limit;
self
}
#[must_use]
pub const fn after_sequence(mut self, after_sequence: i64) -> Self {
self.after_sequence = Some(after_sequence);
self
}
}
pub trait OutboxStore: Send + Sync {
type Error: std::error::Error + Send + Sync + 'static + Classify;
fn acquire(
&self,
request: AcquireRequest,
) -> impl Future<Output = Result<AcquiredBatch, Self::Error>> + Send;
fn complete(
&self,
worker: &WorkerId,
items: &[CompletedMessage],
) -> impl Future<Output = Result<u64, Self::Error>> + Send;
fn fail(
&self,
worker: &WorkerId,
items: &[FailedMessage],
) -> impl Future<Output = Result<u64, Self::Error>> + Send;
fn release(
&self,
worker: &WorkerId,
items: &[MessageRef],
) -> impl Future<Output = Result<u64, Self::Error>> + Send;
fn extend_lease(
&self,
worker: &WorkerId,
items: &[MessageRef],
lease: Duration,
) -> impl Future<Output = Result<u64, Self::Error>> + Send;
fn purge(
&self,
request: PurgeRequest,
) -> impl Future<Output = Result<PurgeReport, Self::Error>> + Send;
fn stats(&self) -> impl Future<Output = Result<OutboxStats, Self::Error>> + Send;
}
pub trait OutboxDeadLetters: Send + Sync {
type Error: std::error::Error + Send + Sync + 'static;
fn list_dead(
&self,
query: DeadQuery,
) -> impl Future<Output = Result<DeadLetterPage, Self::Error>> + Send;
fn retry_dead(
&self,
refs: &[MessageRef],
) -> impl Future<Output = Result<u64, Self::Error>> + Send;
fn purge_dead(
&self,
refs: &[MessageRef],
) -> impl Future<Output = Result<u64, Self::Error>> + Send;
}