monoloop_loop/transaction/lifecycle/
ledger.rs1use super::capacity::TransactionReservations;
4use super::terminal::TerminalDecision;
5use crate::transaction::sticky_cancel::StickyCancel;
6use monoloop_contracts::{
7 CanonicalInput, ChannelId, EffectiveConfig, InvocationConfig, SessionConfig, SessionKey,
8 ToolId, TransactionCompletionSender, TransactionDelivery, TransactionId, TransactionUsage,
9};
10use std::collections::HashMap;
11use std::sync::Arc;
12
13#[derive(Debug, Clone)]
15pub struct ResourceControls {
16 pub cancel: Arc<StickyCancel>,
18}
19
20impl Default for ResourceControls {
21 fn default() -> Self {
22 Self {
23 cancel: Arc::new(StickyCancel::new()),
24 }
25 }
26}
27
28#[derive(Debug)]
30pub struct LedgerEntry {
31 pub transaction_id: TransactionId,
33 pub channel_id: ChannelId,
35 pub session_key: Option<SessionKey>,
37 pub phase: TransactionPhase,
39 pub terminal: Option<TerminalDecision>,
41 pub pending_worker_proposal: Option<super::terminal::TerminalProposal>,
43 pub event_sequence: u64,
45 pub delivery: Option<TransactionDelivery>,
47 pub completion_tx: Option<TransactionCompletionSender>,
49 pub publisher_cmd_tx: Option<super::event_publisher::OrdinaryCmdAdmit>,
51 pub publisher_seal_tx: Option<tokio::sync::mpsc::Sender<super::event_publisher::SealCommand>>,
53 pub input: CanonicalInput,
55 pub invocation_config: InvocationConfig,
57 pub session_config: Option<SessionConfig>,
59 pub effective_config: EffectiveConfig,
61 pub tools: Vec<ToolId>,
63 pub reservations: Option<TransactionReservations>,
65 pub resources: ResourceControls,
67 pub usage: TransactionUsage,
69 pub diagnostic_count: u32,
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
75pub enum TransactionPhase {
76 Queued,
78 EstablishingSession,
80 Running,
82 Cancelling,
84 Finalizing,
86 CompletionPublished,
88 CleanupPending,
90}
91
92#[derive(Debug, Default)]
94pub struct LifecycleLedger {
95 by_transaction: HashMap<TransactionId, LedgerEntry>,
96 by_session: HashMap<SessionKey, TransactionId>,
97}
98
99impl LifecycleLedger {
100 pub fn new() -> Self {
102 Self::default()
103 }
104
105 pub fn len(&self) -> usize {
107 self.by_transaction.len()
108 }
109
110 pub fn is_empty(&self) -> bool {
112 self.by_transaction.is_empty()
113 }
114
115 pub fn transaction_ids(&self) -> Vec<TransactionId> {
117 self.by_transaction.keys().copied().collect()
118 }
119
120 pub fn get(&self, id: &TransactionId) -> Option<&LedgerEntry> {
122 self.by_transaction.get(id)
123 }
124
125 pub fn get_mut(&mut self, id: &TransactionId) -> Option<&mut LedgerEntry> {
127 self.by_transaction.get_mut(id)
128 }
129
130 pub fn session_active(&self, key: &SessionKey) -> bool {
132 self.by_session.contains_key(key)
133 }
134
135 pub fn transaction_for_session(&self, key: &SessionKey) -> Option<TransactionId> {
137 self.by_session.get(key).copied()
138 }
139
140 pub fn distinct_sessions_on_channel(&self, channel: &ChannelId) -> usize {
142 self.by_session
143 .keys()
144 .filter(|k| k.channel_id == *channel)
145 .count()
146 }
147
148 pub fn insert_queued(
151 &mut self,
152 entry: LedgerEntry,
153 max_distinct_sessions: Option<usize>,
154 ) -> Result<(), LedgerInsertError> {
155 if self.by_transaction.contains_key(&entry.transaction_id) {
156 return Err(LedgerInsertError::DuplicateTransaction);
157 }
158 if let Some(ref key) = entry.session_key {
159 if self.by_session.contains_key(key) {
160 return Err(LedgerInsertError::SessionAlreadyActive);
161 }
162 if let Some(max) = max_distinct_sessions {
163 if self.distinct_sessions_on_channel(&key.channel_id) >= max {
164 return Err(LedgerInsertError::DistinctSessionsExceeded);
165 }
166 }
167 }
168 if let Some(ref key) = entry.session_key {
169 self.by_session.insert(key.clone(), entry.transaction_id);
170 }
171 self.by_transaction.insert(entry.transaction_id, entry);
172 Ok(())
173 }
174
175 pub fn remove(&mut self, id: &TransactionId) -> Option<LedgerEntry> {
177 let entry = self.by_transaction.remove(id)?;
178 if let Some(ref key) = entry.session_key {
179 if self.by_session.get(key) == Some(id) {
180 self.by_session.remove(key);
181 }
182 }
183 Some(entry)
184 }
185
186 pub fn bind_session(
202 &mut self,
203 id: &TransactionId,
204 key: SessionKey,
205 max_distinct_sessions: Option<usize>,
206 ) -> Result<(), LedgerInsertError> {
207 if let Some(holder) = self.by_session.get(&key) {
208 if holder != id {
209 return Err(LedgerInsertError::SessionAlreadyActive);
210 }
211 return Ok(());
212 }
213 let replacing_same_channel = self
214 .by_transaction
215 .get(id)
216 .ok_or(LedgerInsertError::UnknownTransaction)?
217 .session_key
218 .as_ref()
219 .is_some_and(|old| old.channel_id == key.channel_id);
220 if let Some(max) = max_distinct_sessions {
221 if !replacing_same_channel && self.distinct_sessions_on_channel(&key.channel_id) >= max
222 {
223 return Err(LedgerInsertError::DistinctSessionsExceeded);
224 }
225 }
226 let entry = self
227 .by_transaction
228 .get_mut(id)
229 .ok_or(LedgerInsertError::UnknownTransaction)?;
230 if let Some(ref old) = entry.session_key {
231 self.by_session.remove(old);
232 }
233 entry.session_key = Some(key.clone());
234 self.by_session.insert(key, *id);
235 Ok(())
236 }
237}
238
239#[derive(Clone, Copy, Debug, PartialEq, Eq)]
241pub enum LedgerInsertError {
242 DuplicateTransaction,
244 SessionAlreadyActive,
246 DistinctSessionsExceeded,
248 UnknownTransaction,
250}