made-core 0.8.0

Domain core of MADE: entities, value objects, events, ports. No IO.
Documentation
//! Durable delivery of one item to one host destination.

use async_trait::async_trait;
use time::OffsetDateTime;

use crate::error::DomainError;
use crate::value_objects::{
    CeremonyId, DeliveryExpiryCause, DeliveryFailureReason, DurationMs, FollowReplacement,
    HostAgentIncarnation, HostDeliveryId, HostDeliveryLease, HostDeliveryObservation,
    HostDeliveryRecord, HostDeliveryTarget, IntegratorFence, ProcessedActionRef,
};

use super::{
    AckOutcome, DeliveryFailureOutcome, EnqueueOutcome, HostActivationOutcome, HostDeliveryFilter,
    HostDeliveryPage, HostDeliveryPageLimit, HostDeliveryQuery, LeasedDelivery, ProcessedOutcome,
    RecordedActivation, SupersessionOutcome,
};

/// Persistence contract for work handed out to hosts.
///
/// Modelled on the named event cursor rather than on a queue: an
/// exclusive expiring lease, a counted attempt, and an end that stays
/// visible. A queue whose acknowledgement consumes the item cannot
/// answer the question an operator actually asks, which is what
/// happened to the question nobody ever came back about.
#[async_trait]
pub trait HostDeliveryLedgerPort: Send + Sync {
    /// Offer a delivery, or recognise the one already held.
    async fn enqueue(&self, record: HostDeliveryRecord) -> Result<EnqueueOutcome, DomainError>;

    /// Hand out, exclusively and with an expiry, what a host can take.
    ///
    /// A delivery whose lease has expired is offerable again: the lease
    /// bounds the exclusion, so a host that died holding one strands
    /// nothing.
    async fn lease(
        &self,
        filter: &HostDeliveryFilter,
        owner: &HostAgentIncarnation,
        now: OffsetDateTime,
        duration: DurationMs,
        limit: HostDeliveryPageLimit,
    ) -> Result<Vec<LeasedDelivery>, DomainError>;

    /// Record what the host said about a delivery it holds.
    async fn acknowledge(
        &self,
        lease: &HostDeliveryLease,
        observation: &HostDeliveryObservation,
        now: OffsetDateTime,
    ) -> Result<AckOutcome, DomainError>;

    /// Close a delivery the host has acted on.
    ///
    /// Only from acknowledged, because acting on something never
    /// received is not a thing that can have happened. The fence, when
    /// the caller has one, is checked before anything is written.
    async fn mark_processed(
        &self,
        delivery_id: &HostDeliveryId,
        owner: &HostAgentIncarnation,
        fence: Option<IntegratorFence>,
        action: &ProcessedActionRef,
        now: OffsetDateTime,
    ) -> Result<ProcessedOutcome, DomainError>;

    /// Write down what an activation adapter did with an envelope.
    ///
    /// Not the leased path, and deliberately so. An activation is a
    /// push: the engine woke a host and holds nothing, so requiring a
    /// lease here would mean taking one out against itself to record
    /// that it made a phone call. Reaching a host is transport and
    /// never processing, so a delivered record stays offerable and the
    /// host still comes and takes the work under its own lease.
    async fn record_activation(
        &self,
        delivery_id: &HostDeliveryId,
        outcome: &HostActivationOutcome,
        now: OffsetDateTime,
    ) -> Result<RecordedActivation, DomainError>;

    /// Count a failed attempt, and retry or give up by the policy.
    async fn mark_failed(
        &self,
        lease: &HostDeliveryLease,
        reason: &DeliveryFailureReason,
        now: OffsetDateTime,
    ) -> Result<DeliveryFailureOutcome, DomainError>;

    /// Give a lease back without counting an attempt against it.
    async fn release(
        &self,
        lease: &HostDeliveryLease,
        now: OffsetDateTime,
    ) -> Result<(), DomainError>;

    /// Expire what has run out: leases, and acknowledgements nobody closed.
    async fn expire(&self, now: OffsetDateTime) -> Result<Vec<HostDeliveryId>, DomainError>;

    /// Give up on one offer that is no longer worth making.
    ///
    /// The sweeps below are about a reason that stopped applying to a
    /// whole ceremony; this is about one item, and the only caller
    /// that has one is the thing keeping a queue bounded. It stays in
    /// the ledger with its cause, because a host that was never told
    /// something needs somebody to be able to read why.
    async fn abandon(
        &self,
        delivery_id: &HostDeliveryId,
        cause: DeliveryExpiryCause,
        now: OffsetDateTime,
    ) -> Result<Option<HostDeliveryRecord>, DomainError>;

    /// Give up on everything still open for one ceremony, naming why.
    ///
    /// The sweep above is about time running out; this is about the
    /// reason for asking running out — a ceremony that ended, a
    /// binding that was revoked. The offers stay in the ledger with
    /// the cause on them, because "nobody was ever asked this" is
    /// exactly what an operator needs to be able to read afterwards,
    /// and a row that quietly stayed queued forever says the opposite.
    /// Terminal records are untouched.
    async fn expire_ceremony(
        &self,
        ceremony_id: &CeremonyId,
        cause: DeliveryExpiryCause,
        now: OffsetDateTime,
    ) -> Result<Vec<HostDeliveryId>, DomainError>;

    /// Follow a destination that was replaced, or leave its work behind.
    async fn supersede(
        &self,
        previous: &HostDeliveryTarget,
        replacement: &HostDeliveryTarget,
        follow: FollowReplacement,
        now: OffsetDateTime,
    ) -> Result<SupersessionOutcome, DomainError>;

    /// One delivery, whatever state it is in.
    async fn get(&self, id: &HostDeliveryId) -> Result<Option<HostDeliveryRecord>, DomainError>;

    /// One page of what the ledger holds.
    async fn list(&self, query: &HostDeliveryQuery) -> Result<HostDeliveryPage, DomainError>;
}