1use std::{collections::BTreeMap, sync::Arc};
2
3use sim_kernel::{ContentId, Datum, Symbol};
4use sim_lib_journal::{
5 Journal, JournalBackend, JournalEntry, JournalObject, Lease, VerifiedSnapshot,
6};
7
8use crate::{durable::*, operation_error::OperationError};
9
10#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct OperationRecord {
13 intent: OperationIntent,
14 grant: OperationGrant,
15 attempt: Option<OperationAttempt>,
16 dispatch: Option<OperationDispatch>,
17 receipt: Option<PerformerReceipt>,
18}
19
20impl OperationRecord {
21 pub const fn intent(&self) -> &OperationIntent {
23 &self.intent
24 }
25
26 pub const fn grant(&self) -> &OperationGrant {
28 &self.grant
29 }
30
31 pub const fn attempt(&self) -> Option<&OperationAttempt> {
33 self.attempt.as_ref()
34 }
35
36 pub const fn dispatch(&self) -> Option<&OperationDispatch> {
38 self.dispatch.as_ref()
39 }
40
41 pub const fn receipt(&self) -> Option<&PerformerReceipt> {
43 self.receipt.as_ref()
44 }
45
46 pub fn state(&self) -> DurableOperationState {
48 Projection {
49 intent: self.intent.clone(),
50 grant: self.grant.clone(),
51 dispatch: self.dispatch.clone(),
52 attempt: self.attempt.clone(),
53 receipt: self.receipt.clone(),
54 }
55 .state()
56 }
57}
58
59pub struct OperationService<B: JournalBackend> {
61 journal: Journal<Arc<B>>,
62 lease: Option<Lease>,
63}
64
65impl<B: JournalBackend> OperationService<B> {
66 pub fn new(backend: B) -> Self {
68 Self::from_shared(Arc::new(backend))
69 }
70
71 pub fn from_shared(backend: Arc<B>) -> Self {
73 Self {
74 journal: Journal::new(backend),
75 lease: None,
76 }
77 }
78
79 pub fn resume(&mut self) -> Result<(), OperationError> {
81 self.lease = Some(self.journal.acquire_lease()?);
82 Ok(())
83 }
84
85 pub fn state(
87 &self,
88 operation: &OperationId,
89 ) -> Result<Option<DurableOperationState>, OperationError> {
90 let projections = project(self.journal.verified_snapshot()?)?;
91 Ok(projections.get(operation).map(Projection::state))
92 }
93
94 pub fn record(
96 &self,
97 operation: &OperationId,
98 ) -> Result<Option<OperationRecord>, OperationError> {
99 let projections = project(self.journal.verified_snapshot()?)?;
100 Ok(projections.get(operation).map(Projection::record))
101 }
102
103 pub fn execute(
109 &mut self,
110 intent: &OperationIntent,
111 grant: &OperationGrant,
112 attempt: &OperationAttempt,
113 performer: &mut dyn OperationPerformer,
114 ) -> Result<DurableOperationState, OperationError> {
115 self.lease.as_ref().ok_or(OperationError::NotResumed)?;
116 intent.verify()?;
117 grant.verify()?;
118 attempt.verify()?;
119 if grant.operation != intent.id {
120 return Err(OperationError::GrantMismatch);
121 }
122 if attempt.operation != intent.id {
123 return Err(OperationError::AttemptMismatch);
124 }
125
126 let projections = project(self.journal.verified_snapshot()?)?;
127 match projections.get(intent.id()) {
128 Some(existing) if existing.intent != *intent => {
129 return Err(OperationError::ContradictoryIntent);
130 }
131 Some(existing) if existing.dispatch.is_some() => return Ok(existing.state()),
132 Some(existing) if existing.grant != *grant => {
133 return Err(OperationError::GrantMismatch);
134 }
135 Some(_) => {}
136 None => self.append(
137 "intent-persisted",
138 vec![intent.canonical_datum(), grant.canonical_datum()],
139 )?,
140 }
141
142 let dispatch =
143 OperationDispatch::new(intent.id.clone(), grant.id.clone(), attempt.id.clone())?;
144 self.append(
145 "dispatched",
146 vec![dispatch.canonical_datum(), attempt.canonical_datum()],
147 )?;
148 let raw = match performer.perform(&dispatch) {
149 PerformerResponse::Receipt(raw) => raw,
150 PerformerResponse::AcknowledgementMissing => {
151 return Ok(DurableOperationState::Dispatched {
152 intent: intent.id.clone(),
153 dispatch: dispatch.id,
154 });
155 }
156 };
157 let receipt = PerformerReceipt::new(dispatch.id.clone(), raw)?;
158 self.append("receipt-persisted", vec![receipt.canonical_datum()])?;
159 Ok(DurableOperationState::ReceiptPersisted {
160 dispatch: dispatch.id,
161 receipt: receipt.id,
162 })
163 }
164
165 fn append(&self, kind: &'static str, datums: Vec<Datum>) -> Result<(), OperationError> {
166 let lease = self.lease.as_ref().ok_or(OperationError::NotResumed)?;
167 let objects = datums
168 .into_iter()
169 .map(JournalObject::from_datum)
170 .collect::<Result<Vec<_>, _>>()?;
171 let payloads = objects.iter().map(|object| object.id.clone()).collect();
172 let expected = self.journal.head()?;
173 let sequence = expected
174 .as_ref()
175 .map_or(Some(0), |head| head.sequence.checked_add(1))
176 .ok_or(OperationError::SequenceExhausted)?;
177 let entry = JournalEntry::new(
178 sequence,
179 expected.as_ref().map(|head| head.entry.clone()),
180 Symbol::qualified("operation", kind),
181 payloads,
182 );
183 self.journal
184 .publish(lease, expected.as_ref(), objects, vec![entry])?;
185 Ok(())
186 }
187}
188
189#[derive(Clone)]
190struct Projection {
191 intent: OperationIntent,
192 grant: OperationGrant,
193 dispatch: Option<OperationDispatch>,
194 attempt: Option<OperationAttempt>,
195 receipt: Option<PerformerReceipt>,
196}
197
198impl Projection {
199 fn state(&self) -> DurableOperationState {
200 match (&self.dispatch, &self.receipt) {
201 (Some(dispatch), Some(receipt)) => DurableOperationState::ReceiptPersisted {
202 dispatch: dispatch.id.clone(),
203 receipt: receipt.id.clone(),
204 },
205 (Some(dispatch), None) => DurableOperationState::Dispatched {
206 intent: self.intent.id.clone(),
207 dispatch: dispatch.id.clone(),
208 },
209 (None, None) => DurableOperationState::IntentPersisted {
210 intent: self.intent.id.clone(),
211 },
212 (None, Some(_)) => unreachable!("projection construction refuses receipt first"),
213 }
214 }
215
216 fn record(&self) -> OperationRecord {
217 OperationRecord {
218 intent: self.intent.clone(),
219 grant: self.grant.clone(),
220 attempt: self.attempt.clone(),
221 dispatch: self.dispatch.clone(),
222 receipt: self.receipt.clone(),
223 }
224 }
225}
226
227fn project(
228 snapshot: VerifiedSnapshot,
229) -> Result<BTreeMap<OperationId, Projection>, OperationError> {
230 let mut operations = BTreeMap::<OperationId, Projection>::new();
231 for entry in snapshot.entries() {
232 if entry.kind == Symbol::qualified("operation", "intent-persisted") {
233 require_payload_count(entry, 2)?;
234 let intent =
235 OperationIntent::from_datum(snapshot_datum(&snapshot, &entry.payloads[0])?)?;
236 let grant = OperationGrant::from_datum(snapshot_datum(&snapshot, &entry.payloads[1])?)?;
237 if grant.operation != intent.id {
238 return Err(OperationError::GrantMismatch);
239 }
240 if operations
241 .insert(
242 intent.id.clone(),
243 Projection {
244 intent,
245 grant,
246 dispatch: None,
247 attempt: None,
248 receipt: None,
249 },
250 )
251 .is_some()
252 {
253 return Err(OperationError::DuplicateIntent);
254 }
255 } else if entry.kind == Symbol::qualified("operation", "dispatched") {
256 require_payload_count(entry, 2)?;
257 let dispatch =
258 OperationDispatch::from_datum(snapshot_datum(&snapshot, &entry.payloads[0])?)?;
259 let attempt =
260 OperationAttempt::from_datum(snapshot_datum(&snapshot, &entry.payloads[1])?)?;
261 let projection = operations
262 .get_mut(&dispatch.operation)
263 .ok_or(OperationError::InvalidTransition("dispatch before intent"))?;
264 if projection.dispatch.is_some()
265 || dispatch.grant != projection.grant.id
266 || attempt.operation != projection.intent.id
267 || dispatch.attempt != attempt.id
268 {
269 return Err(OperationError::InvalidTransition("conflicting dispatch"));
270 }
271 projection.dispatch = Some(dispatch);
272 projection.attempt = Some(attempt);
273 } else if entry.kind == Symbol::qualified("operation", "receipt-persisted") {
274 require_payload_count(entry, 1)?;
275 let receipt =
276 PerformerReceipt::from_datum(snapshot_datum(&snapshot, &entry.payloads[0])?)?;
277 let projection = operations
278 .values_mut()
279 .find(|projection| {
280 projection
281 .dispatch
282 .as_ref()
283 .is_some_and(|dispatch| dispatch.id == receipt.dispatch)
284 })
285 .ok_or(OperationError::InvalidTransition("receipt before dispatch"))?;
286 if projection.receipt.replace(receipt).is_some() {
287 return Err(OperationError::InvalidTransition("duplicate receipt"));
288 }
289 }
290 }
291 Ok(operations)
292}
293fn snapshot_datum<'a>(
294 snapshot: &'a VerifiedSnapshot,
295 id: &ContentId,
296) -> Result<&'a Datum, OperationError> {
297 snapshot
298 .datum(id)
299 .ok_or(OperationError::NonCanonical("missing operation payload"))
300}
301
302fn require_payload_count(entry: &JournalEntry, expected: usize) -> Result<(), OperationError> {
303 if entry.payloads.len() != expected {
304 return Err(OperationError::NonCanonical("operation event payloads"));
305 }
306 Ok(())
307}