Skip to main content

liminal_protocol/lifecycle/
observer_recovery_retire.rs

1//! Retirement of one closed conversation from the observer-recovery aggregate
2//! (DUR-027 R2).
3//!
4//! A retired conversation's progress row is removed, so the aggregate holds
5//! live rows only and its snapshot does not grow with finished conversations.
6//! The aggregate keeps no retired set: what keeps a retired id from coming back
7//! is the server's tombstone, and an advance or arm presenting it afterwards is
8//! answered `ConversationUnknown`, as for any untracked conversation.
9
10use super::ObserverRecoveryAggregate;
11use crate::wire::{ConversationId, ObserverEpoch};
12
13/// Failure while retiring a conversation from the aggregate.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum ObserverProgressRetireError {
16    /// The conversation has no authoritative progress row.
17    ConversationUnknown {
18        /// Unknown conversation.
19        conversation_id: ConversationId,
20    },
21    /// An observer arm is installed on the conversation: an observer is still
22    /// waiting on it, and retiring would answer it by deleting what it waits on.
23    Armed {
24        /// Armed conversation.
25        conversation_id: ConversationId,
26        /// Epoch of the installed arm.
27        epoch: ObserverEpoch,
28    },
29}
30
31/// Total transactional retirement decision against the owned aggregate.
32#[derive(Debug, PartialEq, Eq)]
33pub enum ObserverProgressRetireDecision {
34    /// The retirement was refused; the aggregate is unchanged.
35    Refuse {
36        /// Unchanged aggregate returned to its owner.
37        aggregate: ObserverRecoveryAggregate,
38        /// Exact refusal.
39        error: ObserverProgressRetireError,
40    },
41    /// The retirement validated; the progress row goes when it commits.
42    Commit(ObserverProgressRetireTransaction),
43}
44
45/// Ownership barrier between a validated retirement and its durable append.
46///
47/// The aggregate is unreachable while the transaction is pending, so no arm
48/// can be installed on the conversation between the check and the removal.
49/// Consuming [`Self::commit`] removes the progress row after the caller has
50/// confirmed the durable retire row; consuming [`Self::abort`] returns the
51/// aggregate unchanged.
52#[derive(Debug, PartialEq, Eq)]
53pub struct ObserverProgressRetireTransaction {
54    aggregate: ObserverRecoveryAggregate,
55    conversation_id: ConversationId,
56}
57
58impl ObserverProgressRetireTransaction {
59    /// Returns the conversation whose retire row the durable append writes.
60    #[must_use]
61    pub const fn conversation_id(&self) -> ConversationId {
62        self.conversation_id
63    }
64
65    /// Removes the conversation's progress row after a confirmed durable append.
66    #[must_use]
67    pub fn commit(mut self) -> ObserverRecoveryAggregate {
68        self.aggregate.progress.remove(&self.conversation_id);
69        self.aggregate
70    }
71
72    /// Cancels a failed durable append; the row stays.
73    #[must_use]
74    pub fn abort(self) -> ObserverRecoveryAggregate {
75        self.aggregate
76    }
77}
78
79impl ObserverRecoveryAggregate {
80    /// Consumes the aggregate into one retirement transaction.
81    ///
82    /// An untracked conversation refuses `ConversationUnknown` and an armed one
83    /// refuses `Armed`, each with the aggregate unchanged. Nothing mutates until
84    /// [`ObserverProgressRetireTransaction::commit`].
85    #[must_use]
86    pub fn decide_retire(self, conversation_id: ConversationId) -> ObserverProgressRetireDecision {
87        if !self.progress.contains_key(&conversation_id) {
88            return ObserverProgressRetireDecision::Refuse {
89                aggregate: self,
90                error: ObserverProgressRetireError::ConversationUnknown { conversation_id },
91            };
92        }
93        if let Some(&epoch) = self.armed.get(&conversation_id) {
94            return ObserverProgressRetireDecision::Refuse {
95                aggregate: self,
96                error: ObserverProgressRetireError::Armed {
97                    conversation_id,
98                    epoch,
99                },
100            };
101        }
102        ObserverProgressRetireDecision::Commit(ObserverProgressRetireTransaction {
103            aggregate: self,
104            conversation_id,
105        })
106    }
107}