#[cfg(test)]
pub(crate) mod oracle;
mod postgres;
#[cfg(test)]
mod tests;
mod worker;
pub use postgres::{PostgresJournal, PostgresJournalSettings};
pub use worker::{DRAIN_MARGIN, DeliveryWorker, DrainReport, WorkerHandle, WorkerSettings};
use std::fmt;
use std::time::{Duration, SystemTime};
use async_trait::async_trait;
use super::identity::RequestId;
use super::{ObservedRecord, UsageRecord};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DeliveryMode {
#[default]
TelemetryGrade,
BillingGrade,
}
impl DeliveryMode {
pub fn as_str(self) -> &'static str {
match self {
Self::TelemetryGrade => "telemetry_grade",
Self::BillingGrade => "billing_grade",
}
}
pub fn is_durable(self) -> bool {
matches!(self, Self::BillingGrade)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct IdempotencyKey(String);
impl IdempotencyKey {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<RequestId> for IdempotencyKey {
fn from(id: RequestId) -> Self {
Self(id.to_string())
}
}
impl fmt::Display for IdempotencyKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OrderingKey {
pub namespace: String,
pub subject: String,
}
impl OrderingKey {
fn of(record: &UsageRecord) -> Self {
Self {
namespace: record.namespace.clone(),
subject: record.subject.clone(),
}
}
}
impl fmt::Display for OrderingKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.namespace, self.subject)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct UsageEvent {
id: RequestId,
idempotency_key: IdempotencyKey,
ordering_key: OrderingKey,
record: UsageRecord,
observed_at: SystemTime,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum InvalidEvent {
#[error("`{request_id}` is not a usage event identity: {reason}")]
Identity { request_id: String, reason: String },
}
impl UsageEvent {
pub fn new(observed: ObservedRecord) -> Result<Self, InvalidEvent> {
let id =
RequestId::parse(&observed.record.request_id).map_err(|e| InvalidEvent::Identity {
request_id: observed.record.request_id.clone(),
reason: e.to_string(),
})?;
Ok(Self {
id,
idempotency_key: IdempotencyKey::from(id),
ordering_key: OrderingKey::of(&observed.record),
record: observed.record,
observed_at: observed.observed_at,
})
}
pub fn id(&self) -> RequestId {
self.id
}
pub fn idempotency_key(&self) -> &IdempotencyKey {
&self.idempotency_key
}
pub fn ordering_key(&self) -> &OrderingKey {
&self.ordering_key
}
pub fn record(&self) -> &UsageRecord {
&self.record
}
#[allow(dead_code)]
pub fn is_same_fact_as(&self, other: &Self) -> bool {
self.record == other.record
}
pub fn observed_at(&self) -> SystemTime {
self.observed_at
}
pub fn observed(&self) -> ObservedRecord {
ObservedRecord {
record: self.record.clone(),
observed_at: self.observed_at,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ConsumerId(String);
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum InvalidConsumerId {
#[error("a consumer name must not be empty")]
Empty,
#[error("consumer name `{name}` is over the {max}-character limit")]
TooLong { name: String, max: usize },
#[error(
"consumer name `{name}` contains `{character}`; use lowercase letters, digits, `-`, and `_`"
)]
Character { name: String, character: char },
}
impl ConsumerId {
pub const MAX_LEN: usize = 63;
pub fn parse(name: &str) -> Result<Self, InvalidConsumerId> {
if name.is_empty() {
return Err(InvalidConsumerId::Empty);
}
if name.len() > Self::MAX_LEN {
return Err(InvalidConsumerId::TooLong {
name: name.to_owned(),
max: Self::MAX_LEN,
});
}
if let Some(character) = name
.chars()
.find(|c| !(c.is_ascii_lowercase() || c.is_ascii_digit() || *c == '-' || *c == '_'))
{
return Err(InvalidConsumerId::Character {
name: name.to_owned(),
character,
});
}
Ok(Self(name.to_owned()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ConsumerId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DeliveryId {
pub consumer: ConsumerId,
pub event: RequestId,
pub attempt: u32,
}
impl DeliveryId {
pub fn is_redelivery(&self) -> bool {
self.attempt > 1
}
}
impl fmt::Display for DeliveryId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}#{}", self.consumer, self.event, self.attempt)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Delivery {
pub id: DeliveryId,
pub event: UsageEvent,
pub lease_expires_at: SystemTime,
}
#[derive(Debug, Clone, Copy)]
pub struct Claim {
pub max_events: usize,
pub lease: Duration,
pub now: SystemTime,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Appended {
Accepted {
position: u64,
},
AlreadyPresent { position: u64 },
}
impl Appended {
#[allow(dead_code)]
pub fn position(&self) -> u64 {
match self {
Self::Accepted { position } | Self::AlreadyPresent { position } => *position,
}
}
pub fn is_new(&self) -> bool {
matches!(self, Self::Accepted { .. })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PoisonReason {
Malformed,
Rejected,
AttemptsExhausted,
}
pub const POISON_REASONS: &[&str] = &["malformed", "rejected", "attempts_exhausted"];
impl PoisonReason {
pub fn as_str(self) -> &'static str {
match self {
Self::Malformed => "malformed",
Self::Rejected => "rejected",
Self::AttemptsExhausted => "attempts_exhausted",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CapacityPolicy {
Refuse,
DropOldest,
}
impl CapacityPolicy {
pub fn as_str(self) -> &'static str {
match self {
Self::Refuse => "refuse",
Self::DropOldest => "drop_oldest",
}
}
pub fn can_lose_events(self) -> bool {
matches!(self, Self::DropOldest)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Capacity {
pub max_events: u64,
pub max_delivery_attempts: u32,
pub retain_acknowledged: Duration,
pub policy: CapacityPolicy,
}
impl Capacity {
pub const BILLING_GRADE: Self = Self {
max_events: 1_000_000,
max_delivery_attempts: 8,
retain_acknowledged: Duration::from_secs(24 * 60 * 60),
policy: CapacityPolicy::Refuse,
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct JournalStats {
pub pending: u64,
pub in_flight: u64,
pub quarantined: u64,
pub oldest_pending_age: Option<Duration>,
pub dropped: u64,
pub capacity: Capacity,
}
impl JournalStats {
pub fn is_drained(&self) -> bool {
self.pending == 0 && self.in_flight == 0
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum JournalError {
#[error(
"usage journal is at capacity ({pending} events retained, limit {}); the event was not journaled",
capacity.max_events
)]
AtCapacity {
pending: u64,
capacity: Capacity,
},
#[error("usage event `{key}` was already journaled with different content")]
Conflict { key: IdempotencyKey },
#[error("delivery `{delivery}` is not outstanding")]
NotOutstanding { delivery: DeliveryId },
#[error("delivery `{delivery}` is quarantined; an operator has to release it")]
Quarantined { delivery: DeliveryId },
#[error("delivery `{delivery}` was already acknowledged")]
AlreadyAcknowledged { delivery: DeliveryId },
#[error("usage journal backend: {0}")]
Backend(String),
}
#[async_trait]
pub trait UsageJournal: Send + Sync {
fn name(&self) -> &'static str;
fn capacity(&self) -> Capacity;
fn mode(&self) -> DeliveryMode;
async fn append(&self, event: &UsageEvent) -> Result<Appended, JournalError>;
async fn claim(
&self,
consumer: &ConsumerId,
claim: Claim,
) -> Result<Vec<Delivery>, JournalError>;
async fn ack(&self, delivery: &DeliveryId) -> Result<(), JournalError>;
async fn ack_all(&self, deliveries: &[DeliveryId]) -> Vec<Result<(), JournalError>> {
let mut verdicts = Vec::with_capacity(deliveries.len());
for delivery in deliveries {
verdicts.push(self.ack(delivery).await);
}
verdicts
}
async fn quarantine(
&self,
delivery: &DeliveryId,
reason: PoisonReason,
) -> Result<(), JournalError>;
async fn relinquish(&self, delivery: &DeliveryId) -> Result<(), JournalError>;
async fn stats(&self, consumer: &ConsumerId) -> Result<JournalStats, JournalError>;
async fn maintain(&self, now: SystemTime) -> Result<u64, JournalError> {
let _ = now;
Ok(0)
}
async fn consumers_besides(&self, mine: &ConsumerId) -> Result<Vec<String>, JournalError> {
let _ = mine;
Ok(Vec::new())
}
}