1use std::collections::{BTreeMap, BTreeSet};
2use std::future::Future;
3use std::num::NonZeroUsize;
4use std::pin::Pin;
5use std::sync::{Arc, Mutex};
6
7use serde::de::DeserializeOwned;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use storage_kit::state::record::{
11 StateKey, StateKeyPrefix, StateListRequest, StateRecord, StateRecordSnapshot,
12};
13use storage_kit::{StateStoreBackend, StateStoreError, StateTransactionBackend};
14use uuid::Uuid;
15
16use crate::app::{AppRecord, AppTrace, MAX_AGENTS, MAX_RUNS, ShutdownRecord};
17use crate::workflow::MAX_FRONTIER;
18use crate::workflow::definition::{CompiledDefinition, SchemaText};
19use crate::workflow::run::{
20 CheckpointPhaseRecord, ControlRecordState, InteractionSettlement, MAX_INTERACTION_SETTLEMENTS,
21 MAX_LIST_LIMIT, RunKey, RunRecord, RuntimeOperationFailure,
22};
23use crate::workflow::{
24 CheckpointKind, CheckpointSelector, HistoryCursor, RunId, RunSelector, StagedOutcomeRecord,
25 WorkflowId,
26};
27use crate::workspace::{MAX_WORKSPACES, WorkspaceRecord};
28use crate::{AppError, WorkspaceId};
29
30mod migration;
31mod occurrence;
32
33pub(crate) use migration::AppMigration;
34pub(crate) use occurrence::RunCopy;
35use occurrence::{RunIndex, RunProjection};
36
37pub(crate) const EPOCH: &str = "app/v7";
38const LEGACY_EPOCH: &str = "app/v6";
39const NAMESPACE: &str = "app-kit";
40const PAGE_SIZE: usize = 256;
41pub(crate) const MAX_RECORD_BYTES: usize = 4 * 1_048_576;
42
43pub(crate) struct AppStore<CS: StateStoreBackend> {
44 inner: CS,
45 decoded_runs: Mutex<Vec<DecodedRun>>,
46 retired_receipts: Mutex<Vec<String>>,
47}
48
49struct DecodedRun {
51 source: Arc<Value>,
52 pages: Vec<Arc<Value>>,
53 record: RunRecord,
54}
55
56impl DecodedRun {
57 fn matches(&self, source: &Arc<Value>, pages: &[Arc<Value>]) -> bool {
58 Arc::ptr_eq(&self.source, source)
59 && self.pages.len() == pages.len()
60 && self
61 .pages
62 .iter()
63 .zip(pages)
64 .all(|(left, right)| Arc::ptr_eq(left, right))
65 }
66}
67
68pub(crate) struct AppMutation {
69 id: String,
70 kind: AppMutationKind,
71}
72
73pub(crate) enum StorageFailure {
74 Recoverable(AppError),
75 Permanent(AppError),
76 CleanupFailed(AppError),
77}
78
79pub(crate) enum AppCommitOutcome {
80 Applied,
81 Rejected(StorageFailure),
82 Unknown(StorageFailure),
83}
84
85enum AppMutationKind {
86 ClaimApp {
87 fence: AppClaimFence,
88 app: AppStorageRecord,
89 definitions: Vec<AppStorageRecord>,
90 },
91 AdvanceLifecycle {
92 expected_app_revision: u64,
93 generation: u64,
94 shutdown: ShutdownRecord,
95 },
96 AdmitWorkspace {
97 expected_app_revision: u64,
98 app: AppStorageRecord,
99 workspace: AppStorageRecord,
100 },
101 AdvanceWorkspace {
102 expected_app_revision: u64,
103 expected_workspace_revision: u64,
104 workspace: AppStorageRecord,
105 },
106 AdvanceWorkspaceWithApp {
107 expected_app_revision: u64,
108 expected_workspace_revision: u64,
109 app: AppStorageRecord,
110 workspace: AppStorageRecord,
111 },
112 DeleteWorkspace {
113 expected_app_revision: u64,
114 expected_workspace_revision: u64,
115 workspace: RecordKey,
116 },
117 AdmitRun {
118 expected_app_revision: u64,
119 expected_workspace_revision: u64,
120 app: AppStorageRecord,
121 workspace: AppStorageRecord,
122 run: AppStorageRecord,
123 head: AppStorageRecord,
124 source: Option<Box<RunCopy>>,
125 },
126 AdvanceRun {
127 expected_app_revision: u64,
128 expected_run_revision: u64,
129 app: AppStorageRecord,
130 run: AppStorageRecord,
131 predecessor: RunRecord,
132 children: Vec<(RunKey, String)>,
133 },
134 AdmitChildren {
135 expected_app_revision: u64,
136 expected_workspace_revision: u64,
137 expected_parent_revision: u64,
138 app: AppStorageRecord,
139 workspace: AppStorageRecord,
140 parent: AppStorageRecord,
141 predecessor: RunRecord,
142 children: Vec<(AppStorageRecord, AppStorageRecord, Option<RunCopy>)>,
143 },
144 RetainRun {
145 expected_app_revision: u64,
146 expected_run_revision: u64,
147 app: AppStorageRecord,
148 run: AppStorageRecord,
149 },
150 ReadmitRun {
151 expected_app_revision: u64,
152 expected_workspace_revision: u64,
153 expected_run_revision: u64,
154 app: AppStorageRecord,
155 workspace: AppStorageRecord,
156 run: AppStorageRecord,
157 predecessor: RunRecord,
158 },
159 SealRun {
160 expected_app_revision: u64,
161 expected_run_revision: u64,
162 run: AppStorageRecord,
163 parent: Option<(AppStorageRecord, u64)>,
164 },
165 DeleteRunPage {
166 expected_app_revision: u64,
167 expected_run_revision: u64,
168 head: AppStorageRecord,
169 keys: Vec<RecordKey>,
170 complete: bool,
171 },
172}
173
174enum AppClaimFence {
175 Missing,
176 Revision(u64),
177}
178
179enum AppStorageRecord {
180 Value {
181 key: RecordKey,
182 value: Value,
183 },
184 Checkpoint {
185 key: RecordKey,
186 value: Value,
187 record: Box<RunRecord>,
188 pages: Vec<StateRecord>,
189 },
190}
191
192#[derive(Deserialize, Serialize)]
193#[serde(deny_unknown_fields)]
194struct SettlementPages {
195 count: usize,
196 pages: Vec<u64>,
197}
198
199#[derive(Serialize)]
200struct SettlementPage<'record> {
201 epoch: String,
202 workspace_id: WorkspaceId,
203 workflow_id: WorkflowId,
204 id: RunId,
205 occurrence: String,
206 index: usize,
207 sequence: u64,
208 entries: &'record [InteractionSettlement],
209}
210
211#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
212enum RecordKey {
213 App,
214 Definition {
215 workflow: WorkflowId,
216 revision: String,
217 },
218 Workspace(WorkspaceId),
219 Run {
220 workspace: WorkspaceId,
221 workflow: WorkflowId,
222 run: RunId,
223 occurrence: String,
224 },
225 Checkpoint {
226 workspace: WorkspaceId,
227 workflow: WorkflowId,
228 run: RunId,
229 occurrence: String,
230 sequence: u64,
231 },
232 Ledger {
233 workspace: WorkspaceId,
234 workflow: WorkflowId,
235 run: RunId,
236 occurrence: String,
237 index: usize,
238 sequence: u64,
239 },
240 ActionObservation {
241 workspace: WorkspaceId,
242 workflow: WorkflowId,
243 run: RunId,
244 occurrence: String,
245 ordinal: u64,
246 sequence: u64,
247 },
248 ActiveRun {
249 workspace: WorkspaceId,
250 workflow: WorkflowId,
251 run: RunId,
252 occurrence: String,
253 },
254 ProjectionTail {
255 workspace: WorkspaceId,
256 workflow: WorkflowId,
257 run: RunId,
258 projection: RunProjection,
259 },
260 ProjectionLink {
261 key: RunKey,
262 projection: RunProjection,
263 sequence: u64,
264 },
265 Receipt(String),
266}
267
268#[derive(Deserialize, Serialize)]
269#[serde(deny_unknown_fields)]
270struct DefinitionRecord {
271 epoch: String,
272 workflow_id: WorkflowId,
273 revision: String,
274 canonical: String,
275}
276
277impl SettlementPages {
278 fn validate(&self, sequence: u64) -> Result<(), AppError> {
279 if self.count > MAX_INTERACTION_SETTLEMENTS
280 || self.pages.len() != self.count.div_ceil(64)
281 || self.pages.iter().any(|published| *published > sequence)
282 {
283 return Err(AppError::Integration(
284 "settlement page references are invalid".into(),
285 ));
286 }
287 Ok(())
288 }
289}
290
291impl SettlementPage<'_> {
292 fn matches(key: &RecordKey, value: &Value) -> bool {
293 let RecordKey::Ledger {
294 workspace,
295 workflow,
296 run,
297 occurrence,
298 index,
299 sequence,
300 } = key
301 else {
302 return false;
303 };
304 value.as_object().is_some_and(|value| value.len() == 8)
305 && value.get("epoch").and_then(Value::as_str) == Some(EPOCH)
306 && value.get("workspace_id").and_then(Value::as_str) == Some(workspace.as_str())
307 && value.get("workflow_id").and_then(Value::as_str) == Some(workflow.as_str())
308 && value.get("id").and_then(Value::as_str) == Some(run.as_str())
309 && value.get("occurrence").and_then(Value::as_str) == Some(occurrence.as_str())
310 && value.get("index").and_then(Value::as_u64) == Some(*index as u64)
311 && value.get("sequence").and_then(Value::as_u64) == Some(*sequence)
312 && value
313 .get("entries")
314 .and_then(Value::as_array)
315 .is_some_and(|entries| !entries.is_empty() && entries.len() <= 64)
316 }
317}
318
319impl DefinitionRecord {
320 fn key(&self) -> RecordKey {
321 RecordKey::Definition {
322 workflow: self.workflow_id.clone(),
323 revision: self.revision.clone(),
324 }
325 }
326
327 fn validate(&self) -> Result<(), AppError> {
328 let compiled = CompiledDefinition::parse(&self.canonical)
329 .map_err(|_| AppError::Integration("durable workflow definition is corrupt".into()))?;
330 if self.epoch != EPOCH
331 || self.workflow_id.as_str() != compiled.id()
332 || self.revision != compiled.revision()
333 || self.canonical != compiled.fingerprint()
334 {
335 return Err(AppError::Integration(
336 "durable workflow definition identity is inconsistent".into(),
337 ));
338 }
339 Ok(())
340 }
341}
342
343#[derive(Deserialize, Serialize)]
344#[serde(deny_unknown_fields)]
345struct RunHead {
346 epoch: String,
347 id: RunId,
348 workspace_id: WorkspaceId,
349 workflow_id: WorkflowId,
350 definition_revision: String,
351 child_revisions: BTreeMap<WorkflowId, String>,
352 agent_revisions: BTreeMap<String, String>,
353 creation: String,
354 sequence: u64,
355 unresolved_failures: BTreeMap<u64, u64>,
356 retention: RunRetention,
357}
358
359#[derive(Deserialize, Serialize)]
360#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
361enum RunRetention {
362 Active {},
363 Retained {},
364 Deleting { after: Option<String> },
365}
366
367#[derive(Deserialize, Serialize)]
368struct ReceiptRecord {
369 epoch: String,
370 id: String,
371}
372
373pub(crate) struct RecordCodec {
374 bytes: usize,
375}
376
377impl RunHead {
378 fn definition_pins(&self) -> impl Iterator<Item = (&WorkflowId, &str)> {
379 std::iter::once((&self.workflow_id, self.definition_revision.as_str())).chain(
380 self.child_revisions
381 .iter()
382 .map(|(id, revision)| (id, revision.as_str())),
383 )
384 }
385
386 fn advance(&mut self, record: &RunRecord, prior: &RunRecord) -> Result<(), AppError> {
387 if !matches!(self.retention, RunRetention::Active {})
388 || self.sequence.checked_add(1) != Some(record.checkpoint.sequence)
389 || self.id != record.id
390 || self.creation != record.occurrence
391 || self.workspace_id != record.workspace_id
392 || self.workflow_id != record.workflow_id
393 {
394 return Err(AppError::Conflict(
395 "run head cannot accept this checkpoint".into(),
396 ));
397 }
398 if let Some(committed) = record.checkpoint.committed_ordinal() {
399 self.unresolved_failures
400 .retain(|ordinal, _| *ordinal > committed);
401 }
402 if let Some(observation) = record.checkpoint.failed_observation()
403 && prior.checkpoint.failed_observation() != Some(observation)
404 {
405 self.unresolved_failures
406 .insert(observation.ordinal, record.checkpoint.sequence);
407 }
408 self.sequence = record.checkpoint.sequence;
409 self.validate()?;
410 self.validate_row(record)
411 }
412
413 fn head_key(&self) -> RecordKey {
414 RecordKey::Run {
415 workspace: self.workspace_id.clone(),
416 workflow: self.workflow_id.clone(),
417 run: self.id.clone(),
418 occurrence: self.creation.clone(),
419 }
420 }
421
422 fn checkpoint_key(&self, sequence: u64) -> RecordKey {
423 RecordKey::Checkpoint {
424 workspace: self.workspace_id.clone(),
425 workflow: self.workflow_id.clone(),
426 run: self.id.clone(),
427 occurrence: self.creation.clone(),
428 sequence,
429 }
430 }
431
432 fn active_key(&self) -> RecordKey {
433 RecordKey::ActiveRun {
434 workspace: self.workspace_id.clone(),
435 workflow: self.workflow_id.clone(),
436 run: self.id.clone(),
437 occurrence: self.creation.clone(),
438 }
439 }
440
441 fn history_prefix(&self) -> String {
442 format!(
443 "history/{}/{}/{}/{}/",
444 self.workspace_id, self.workflow_id, self.id, self.creation
445 )
446 }
447
448 fn ensure_readable(&self) -> Result<(), AppError> {
449 if matches!(self.retention, RunRetention::Deleting { .. }) {
450 Err(AppError::Closed(
451 "run history deletion has been admitted".into(),
452 ))
453 } else {
454 Ok(())
455 }
456 }
457
458 fn admitted(record: &RunRecord) -> Result<Self, AppError> {
459 if record.checkpoint.sequence != 0 || record.previous.is_some() {
460 return Err(AppError::Integration(
461 "run admission must publish checkpoint zero".into(),
462 ));
463 }
464 let head = Self {
465 epoch: record.epoch.clone(),
466 id: record.id.clone(),
467 workspace_id: record.workspace_id.clone(),
468 workflow_id: record.workflow_id.clone(),
469 definition_revision: record.definition_revision.clone(),
470 child_revisions: record.child_revisions.clone(),
471 agent_revisions: record.agent_revisions.clone(),
472 creation: record.occurrence.clone(),
473 sequence: record.checkpoint.sequence,
474 unresolved_failures: BTreeMap::new(),
475 retention: RunRetention::Active {},
476 };
477 head.validate()?;
478 head.validate_row(record)?;
479 Ok(head)
480 }
481
482 fn decode(value: &Value) -> Result<Self, AppError> {
483 RecordCodec::preflight_epoch(value)?;
484 let head: Self = RecordCodec::decode(value)?;
485 head.validate()?;
486 Ok(head)
487 }
488
489 fn validate(&self) -> Result<(), AppError> {
490 if self.epoch != EPOCH
491 || self.definition_pins().any(|(_, revision)| {
492 revision.is_empty()
493 || revision.len() > 256
494 || revision.trim() != revision
495 || revision.chars().any(char::is_control)
496 })
497 || self.agent_revisions.len() > MAX_AGENTS
498 || self.agent_revisions.iter().any(|(key, revision)| {
499 SchemaText::node_or_state("agent key", key).is_err()
500 || SchemaText::revision(revision).is_err()
501 })
502 || self.creation.len() != 32
503 || !self
504 .creation
505 .bytes()
506 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
507 {
508 return Err(AppError::Integration("durable run head is invalid".into()));
509 }
510 if self.unresolved_failures.len() > MAX_FRONTIER
511 || self
512 .unresolved_failures
513 .values()
514 .any(|sequence| *sequence == 0 || *sequence > self.sequence)
515 || self
516 .unresolved_failures
517 .values()
518 .collect::<BTreeSet<_>>()
519 .len()
520 != self.unresolved_failures.len()
521 {
522 return Err(AppError::Integration(
523 "durable failed observation evidence is invalid".into(),
524 ));
525 }
526 if let RunRetention::Deleting { after: Some(after) } = &self.retention {
527 match RecordKey::history_key(after)? {
528 RecordKey::Checkpoint {
529 workspace,
530 workflow,
531 run,
532 occurrence,
533 sequence,
534 }
535 | RecordKey::Ledger {
536 workspace,
537 workflow,
538 run,
539 occurrence,
540 sequence,
541 ..
542 }
543 | RecordKey::ActionObservation {
544 workspace,
545 workflow,
546 run,
547 occurrence,
548 sequence,
549 ..
550 } if workspace == self.workspace_id
551 && workflow == self.workflow_id
552 && run == self.id
553 && occurrence == self.creation
554 && sequence <= self.sequence => {}
555 RecordKey::ProjectionLink { key, sequence, .. }
556 if key.workspace() == &self.workspace_id
557 && key.workflow() == &self.workflow_id
558 && key.run() == &self.id
559 && key.occurrence() == self.creation
560 && sequence <= self.sequence => {}
561 _ => {
562 return Err(AppError::Integration(
563 "run deletion cursor has foreign scope".into(),
564 ));
565 }
566 }
567 }
568 Ok(())
569 }
570
571 fn validate_row(&self, record: &RunRecord) -> Result<(), AppError> {
572 if record.epoch != self.epoch
573 || record.id != self.id
574 || record.occurrence != self.creation
575 || record.workspace_id != self.workspace_id
576 || record.workflow_id != self.workflow_id
577 || self.definition_revision != record.definition_revision
578 || self.child_revisions != record.child_revisions
579 || self.agent_revisions != record.agent_revisions
580 || record.checkpoint.sequence != self.sequence
581 || self.unresolved_failures.keys().any(|ordinal| {
582 match record.checkpoint.committed_ordinal() {
583 Some(committed) => ordinal
584 .checked_sub(committed)
585 .is_none_or(|offset| offset == 0 || offset > MAX_FRONTIER as u64),
586 None => *ordinal >= MAX_FRONTIER as u64,
587 }
588 })
589 || record
590 .checkpoint
591 .failed_observation()
592 .is_some_and(|observation| {
593 !self.unresolved_failures.contains_key(&observation.ordinal)
594 })
595 || (self.sequence == 0 && !self.unresolved_failures.is_empty())
596 || (!matches!(self.retention, RunRetention::Active {})
597 && !record.checkpoint.is_terminal())
598 {
599 return Err(AppError::Integration(
600 "durable run head does not identify its checkpoint".into(),
601 ));
602 }
603 Ok(())
604 }
605
606 fn record(&self) -> Result<StateRecord, AppError> {
607 self.validate()?;
608 self.head_key().record(self)
609 }
610}
611
612impl StorageFailure {
613 fn from_app(error: AppError) -> Self {
614 if matches!(error, AppError::Busy(_) | AppError::Timeout(_)) {
615 Self::Recoverable(error)
616 } else {
617 Self::Permanent(error)
618 }
619 }
620
621 pub(crate) fn permanent(error: AppError) -> Self {
622 Self::Permanent(error)
623 }
624
625 pub(crate) fn is_recoverable(&self) -> bool {
626 matches!(self, Self::Recoverable(_))
627 }
628
629 pub(crate) fn is_cleanup_failed(&self) -> bool {
630 matches!(self, Self::CleanupFailed(_))
631 }
632
633 pub(crate) fn is_retryable_conflict(&self) -> bool {
634 !self.is_cleanup_failed() && matches!(self.error(), AppError::Conflict(_))
635 }
636
637 pub(crate) fn error(&self) -> &AppError {
638 match self {
639 Self::Recoverable(error) | Self::Permanent(error) | Self::CleanupFailed(error) => error,
640 }
641 }
642
643 pub(crate) fn into_error(self) -> AppError {
644 match self {
645 Self::Recoverable(error) | Self::Permanent(error) | Self::CleanupFailed(error) => error,
646 }
647 }
648}
649
650impl<CS: StateStoreBackend> AppStore<CS> {
651 pub(crate) fn new(inner: CS) -> Self {
652 Self {
653 inner,
654 decoded_runs: Mutex::new(Vec::new()),
655 retired_receipts: Mutex::new(Vec::new()),
656 }
657 }
658
659 async fn decode_run<T: StateTransactionBackend>(
660 &self,
661 transaction: &mut T,
662 snapshot: StateRecordSnapshot,
663 admit: bool,
664 observed_pages: &mut Vec<StateRecordSnapshot>,
665 ) -> Result<RunRecord, StorageFailure> {
666 let key = snapshot.record.key;
667 let value = snapshot.record.value;
668 RecordCodec::preflight_epoch(&value).map_err(StorageFailure::permanent)?;
669 RecordKey::validate_value(key.key(), &value).map_err(StorageFailure::permanent)?;
670 let RecordKey::Checkpoint {
671 workspace,
672 workflow,
673 run,
674 occurrence,
675 sequence,
676 } = RecordKey::history_key(key.key()).map_err(StorageFailure::permanent)?
677 else {
678 return Err(StorageFailure::permanent(AppError::Integration(
679 "expected checkpoint evidence".into(),
680 )));
681 };
682 let descriptor: SettlementPages = RecordCodec::decode(&value["checkpoint"]["settlements"])
683 .map_err(StorageFailure::permanent)?;
684 descriptor
685 .validate(sequence)
686 .map_err(StorageFailure::permanent)?;
687 let mut pages = Vec::with_capacity(descriptor.pages.len());
688 for (index, sequence) in descriptor.pages.iter().copied().enumerate() {
689 let key = RecordKey::Ledger {
690 workspace: workspace.clone(),
691 workflow: workflow.clone(),
692 run: run.clone(),
693 occurrence: occurrence.clone(),
694 index,
695 sequence,
696 }
697 .state_key()
698 .map_err(StorageFailure::permanent)?;
699 let snapshot = match observed_pages
700 .iter()
701 .find(|snapshot| snapshot.record.key == key)
702 {
703 Some(snapshot) => snapshot.clone(),
704 None => {
705 let snapshot = transaction
706 .load(key.clone())
707 .await
708 .map_err(StorageFailure::from)?
709 .ok_or_else(|| {
710 StorageFailure::permanent(AppError::Integration(
711 "referenced settlement page is missing".into(),
712 ))
713 })?;
714 if observed_pages.len() == 128 {
718 observed_pages.remove(0);
719 }
720 observed_pages.push(snapshot.clone());
721 snapshot
722 }
723 };
724 if snapshot.record.key != key {
725 return Err(StorageFailure::permanent(AppError::Integration(
726 "settlement page has foreign storage identity".into(),
727 )));
728 }
729 RecordKey::validate_value(key.key(), &snapshot.record.value)
730 .map_err(StorageFailure::permanent)?;
731 if snapshot
732 .record
733 .value
734 .get("entries")
735 .and_then(Value::as_array)
736 .map(Vec::len)
737 != Some((descriptor.count - index * 64).min(64))
738 {
739 return Err(StorageFailure::permanent(AppError::Integration(
740 "settlement page count is inconsistent".into(),
741 )));
742 }
743 pages.push(snapshot.record.value);
744 }
745 let cached = {
746 let mut rows = self
747 .decoded_runs
748 .lock()
749 .unwrap_or_else(|poisoned| poisoned.into_inner());
750 rows.iter()
751 .position(|row| row.matches(&value, &pages))
752 .map(|index| {
753 let row = rows.remove(index);
754 let cached = row.record.clone();
755 rows.push(row);
756 cached
757 })
758 };
759 if let Some(record) = cached {
760 return Ok(record);
761 }
762 let mut entries_size = 2usize;
766 for (index, page) in pages.iter().enumerate() {
767 let size =
768 RecordCodec::value_len(&page["entries"]).map_err(StorageFailure::permanent)?;
769 entries_size = entries_size
770 .checked_add(size.saturating_sub(2))
771 .and_then(|size| size.checked_add(usize::from(index > 0)))
772 .ok_or_else(|| {
773 StorageFailure::permanent(AppError::Integration(
774 "app durable record encoding failed".into(),
775 ))
776 })?;
777 }
778 let size = RecordCodec::value_len(&value)
779 .map_err(StorageFailure::permanent)?
780 .checked_sub(
781 RecordCodec::value_len(&value["checkpoint"]["settlements"])
782 .map_err(StorageFailure::permanent)?,
783 )
784 .and_then(|size| size.checked_add(entries_size))
785 .ok_or_else(|| {
786 StorageFailure::permanent(AppError::Integration(
787 "app durable record encoding failed".into(),
788 ))
789 })?;
790 if size > MAX_RECORD_BYTES {
791 return Err(StorageFailure::permanent(AppError::Integration(
792 "app durable record exceeds the private byte bound".into(),
793 )));
794 }
795 let mut record = RecordCodec::parse_run(&value).map_err(StorageFailure::permanent)?;
796 record
797 .checkpoint
798 .hydrate_settlements(&pages, descriptor.pages, descriptor.count)
799 .map_err(StorageFailure::permanent)?;
800 RecordCodec::validate_run(&record, size).map_err(StorageFailure::permanent)?;
801 record
802 .checkpoint
803 .validate_settlements()
804 .map_err(StorageFailure::permanent)?;
805 if admit {
806 let mut rows = self
807 .decoded_runs
808 .lock()
809 .unwrap_or_else(|poisoned| poisoned.into_inner());
810 if !rows.iter().any(|row| row.matches(&value, &pages)) {
811 if rows.len() == 4 {
812 rows.remove(0);
813 }
814 rows.push(DecodedRun {
815 source: value,
816 pages,
817 record: record.clone(),
818 });
819 }
820 }
821 Ok(record)
822 }
823
824 pub(crate) async fn validate_definition_inventory(
825 &self,
826 definitions: &BTreeMap<WorkflowId, Vec<Arc<CompiledDefinition>>>,
827 application: Option<&AppRecord>,
828 configured_agents: &BTreeSet<(&str, &str)>,
829 ) -> Result<(), AppError> {
830 for workspace in self.list_workspaces().await? {
831 workspace.validate(&workspace.id, configured_agents)?;
832 }
833 if let Some(application) = application {
834 for (id, revision) in &application.definitions {
835 let key = RecordKey::Definition {
836 workflow: id.clone(),
837 revision: revision.clone(),
838 };
839 if self
840 .load_record::<DefinitionRecord>(key)
841 .await
842 .map_err(StorageFailure::into_error)?
843 .is_none()
844 {
845 return Err(AppError::Integration(
846 "current workflow revision has no durable definition".into(),
847 ));
848 }
849 }
850 }
851 let mut catalogued = BTreeSet::new();
854 for (id, revisions) in definitions {
855 for definition in revisions {
856 let key = RecordKey::Definition {
857 workflow: id.clone(),
858 revision: definition.revision().to_owned(),
859 };
860 if let Some((record, _)) = self
861 .load_record::<DefinitionRecord>(key)
862 .await
863 .map_err(StorageFailure::into_error)?
864 {
865 if record.canonical != definition.fingerprint() {
866 return Err(AppError::Conflict(
867 "configured workflow revision differs from its durable definition"
868 .into(),
869 ));
870 }
871 catalogued.insert((id, definition.revision()));
872 }
873 }
874 }
875 let mut after = None;
876 loop {
877 let (page, next) = self
878 .list_page(Some("run/"), after.as_deref(), PAGE_SIZE)
879 .await
880 .map_err(StorageFailure::into_error)?;
881 for snapshot in page {
882 let head = RunHead::decode(&snapshot.record.value)?;
883 if matches!(head.retention, RunRetention::Deleting { .. }) {
884 continue;
885 }
886 if head.agent_revisions.iter().any(|(key, revision)| {
887 !configured_agents.contains(&(key.as_str(), revision.as_str()))
888 }) {
889 return Err(AppError::Conflict(
890 "retained agent revision is not configured".into(),
891 ));
892 }
893 let latest = self
894 .load_checkpoint(&head, head.sequence)
895 .await
896 .map_err(StorageFailure::into_error)?;
897 head.validate_row(&latest)?;
898 for (id, revision) in latest.definition_pins() {
901 if !catalogued.contains(&(id, revision)) {
902 let key = RecordKey::Definition {
903 workflow: id.clone(),
904 revision: revision.to_owned(),
905 };
906 if self
907 .load_record::<DefinitionRecord>(key)
908 .await
909 .map_err(StorageFailure::into_error)?
910 .is_none()
911 {
912 return Err(AppError::Integration(
913 "retained workflow revision has no durable definition".into(),
914 ));
915 }
916 return Err(AppError::Conflict(
917 "retained workflow revision is not configured".into(),
918 ));
919 }
920 }
921 let definition = definitions
922 .get(&head.workflow_id)
923 .and_then(|revisions| {
924 revisions
925 .iter()
926 .find(|definition| definition.revision() == head.definition_revision)
927 })
928 .ok_or_else(|| {
929 AppError::Conflict("retained workflow revision is not configured".into())
930 })?;
931 definition
932 .validate_child_relationships(definitions, &head.child_revisions)
933 .map_err(|_| {
934 AppError::Integration(
935 "retained workflow child revision relationships are inconsistent"
936 .into(),
937 )
938 })?;
939 latest.validate_recovery(definition)?;
940 if !self
941 .head_unchanged(&head, snapshot.revision.get())
942 .await
943 .map_err(StorageFailure::into_error)?
944 {
945 return Err(AppError::Busy(
946 "retained run changed during startup validation".into(),
947 ));
948 }
949 }
950 match next {
951 Some(next) => after = Some(next),
952 None => return Ok(()),
953 }
954 }
955 }
956
957 pub(crate) async fn load_app(&self) -> Result<Option<(AppRecord, u64)>, AppError> {
958 self.load_app_for_recovery()
959 .await
960 .map_err(StorageFailure::into_error)
961 }
962
963 pub(crate) async fn load_app_for_recovery(
964 &self,
965 ) -> Result<Option<(AppRecord, u64)>, StorageFailure> {
966 self.load_record(RecordKey::App).await
967 }
968
969 pub(crate) async fn load_workspace(
970 &self,
971 id: &WorkspaceId,
972 ) -> Result<Option<(WorkspaceRecord, u64)>, AppError> {
973 self.load_workspace_for_recovery(id)
974 .await
975 .map_err(StorageFailure::into_error)
976 }
977
978 pub(crate) async fn load_workspace_for_recovery(
979 &self,
980 id: &WorkspaceId,
981 ) -> Result<Option<(WorkspaceRecord, u64)>, StorageFailure> {
982 self.load_record(RecordKey::Workspace(id.clone())).await
983 }
984
985 pub(crate) async fn list_workspaces(&self) -> Result<Vec<WorkspaceRecord>, AppError> {
986 let mut after = None;
987 let mut records = Vec::new();
988 loop {
989 let (page, next) = self
990 .list_page(Some("workspace/"), after.as_deref(), PAGE_SIZE)
991 .await
992 .map_err(StorageFailure::into_error)?;
993 for snapshot in page {
994 if records.len() == MAX_WORKSPACES {
995 return Err(AppError::Integration(
996 "durable workspace inventory exceeds the private bound".into(),
997 ));
998 }
999 records.push(RecordCodec::decode(&snapshot.record.value)?);
1000 }
1001 match next {
1002 Some(next) => after = Some(next),
1003 None => return Ok(records),
1004 }
1005 }
1006 }
1007
1008 pub(crate) async fn load_run(
1009 &self,
1010 key: &RunKey,
1011 ) -> Result<Option<(RunRecord, u64)>, AppError> {
1012 self.load_run_for_recovery(key)
1013 .await
1014 .map_err(StorageFailure::into_error)
1015 }
1016
1017 pub(crate) async fn load_run_for_recovery(
1018 &self,
1019 key: &RunKey,
1020 ) -> Result<Option<(RunRecord, u64)>, StorageFailure> {
1021 let state_key = RecordKey::exact_run(key)
1022 .state_key()
1023 .map_err(StorageFailure::permanent)?;
1024 for _ in 0..8 {
1025 let mut transaction = self
1026 .inner
1027 .begin_transaction()
1028 .await
1029 .map_err(StorageFailure::from)?;
1030 let mut observed_pages = Vec::new();
1031 let mut missing_row_head = None;
1032 let observed: Result<Option<(RunHead, u64, RunRecord)>, StorageFailure> = async {
1033 let Some(snapshot) = transaction
1034 .load(state_key.clone())
1035 .await
1036 .map_err(StorageFailure::from)?
1037 else {
1038 return Ok(None);
1039 };
1040 RecordKey::validate_value(state_key.key(), &snapshot.record.value)
1041 .map_err(StorageFailure::permanent)?;
1042 let head =
1043 RunHead::decode(&snapshot.record.value).map_err(StorageFailure::permanent)?;
1044 let revision = snapshot.revision.get();
1045 head.ensure_readable().map_err(StorageFailure::permanent)?;
1046 let mut current: Option<RunRecord> = None;
1047 for selected in std::iter::once(head.sequence).chain(head.sequence.checked_sub(1)) {
1048 let key = head
1049 .checkpoint_key(selected)
1050 .state_key()
1051 .map_err(StorageFailure::permanent)?;
1052 let Some(snapshot) = transaction
1053 .load(key.clone())
1054 .await
1055 .map_err(StorageFailure::from)?
1056 else {
1057 missing_row_head = Some(head);
1058 return Err(StorageFailure::permanent(AppError::Integration(
1059 if current.is_some() {
1060 "retained checkpoint predecessor is missing"
1061 } else {
1062 "retained checkpoint is missing"
1063 }
1064 .into(),
1065 )));
1066 };
1067 current = Some(
1068 self.decode_checkpoint_row(
1069 &mut transaction,
1070 &key,
1071 snapshot,
1072 current,
1073 &mut observed_pages,
1074 )
1075 .await?,
1076 );
1077 }
1078 let record = current.expect("the selected checkpoint is always observed");
1079 head.validate_row(&record)
1080 .map_err(StorageFailure::permanent)?;
1081 Ok(Some((head, revision, record)))
1082 }
1083 .await;
1084 let observed = match observed {
1085 Ok(observed) => match transaction.commit().await {
1086 Ok(()) => observed,
1087 Err(StateStoreError::TransactionConflict { .. }) => continue,
1090 Err(error) => return Err(StorageFailure::from(error)),
1091 },
1092 Err(primary) => {
1093 if transaction.rollback().await.is_err() {
1094 AppTrace::event(
1095 || tracing::warn!(target: "app_kit", event = "app.storage.read_rollback_failed"),
1096 );
1097 return Err(StorageFailure::CleanupFailed(primary.into_error()));
1099 }
1100 if let Some(head) = missing_row_head {
1101 self.validate_history_head(&head).await?;
1102 }
1103 return Err(primary);
1104 }
1105 };
1106 let Some((head, revision, record)) = observed else {
1107 return Ok(None);
1108 };
1109 if self.head_unchanged(&head, revision).await? {
1111 return Ok(Some((record, revision)));
1112 }
1113 }
1114 Err(StorageFailure::Recoverable(AppError::Busy(
1115 "run head changed during observation".into(),
1116 )))
1117 }
1118
1119 async fn load_head(&self, key: RecordKey) -> Result<Option<(RunHead, u64)>, StorageFailure> {
1120 let state_key = key.state_key().map_err(StorageFailure::permanent)?;
1121 let snapshot = self
1122 .inner
1123 .load(state_key.clone())
1124 .await
1125 .map_err(StorageFailure::from)?;
1126 snapshot
1127 .map(|snapshot| {
1128 RecordKey::validate_value(state_key.key(), &snapshot.record.value)
1129 .map_err(StorageFailure::permanent)?;
1130 RunHead::decode(&snapshot.record.value)
1131 .map(|head| (head, snapshot.revision.get()))
1132 .map_err(StorageFailure::permanent)
1133 })
1134 .transpose()
1135 }
1136
1137 async fn head_unchanged(&self, head: &RunHead, revision: u64) -> Result<bool, StorageFailure> {
1138 let Some((current, observed)) = self.load_head(head.head_key()).await? else {
1139 return Err(StorageFailure::permanent(AppError::Closed(
1140 "workflow run was removed during observation".into(),
1141 )));
1142 };
1143 current
1144 .ensure_readable()
1145 .map_err(StorageFailure::permanent)?;
1146 Ok(observed == revision
1147 && current.creation == head.creation
1148 && current.sequence == head.sequence)
1149 }
1150
1151 async fn validate_history_head(&self, head: &RunHead) -> Result<(), StorageFailure> {
1152 let Some((current, _)) = self.load_head(head.head_key()).await? else {
1153 return Err(StorageFailure::permanent(AppError::Closed(
1154 "workflow run was removed during observation".into(),
1155 )));
1156 };
1157 current
1158 .ensure_readable()
1159 .map_err(StorageFailure::permanent)?;
1160 if current.creation != head.creation || current.sequence < head.sequence {
1161 return Err(StorageFailure::permanent(AppError::Conflict(
1162 "run history scope changed during observation".into(),
1163 )));
1164 }
1165 Ok(())
1166 }
1167
1168 async fn decode_checkpoint_row<T: StateTransactionBackend>(
1169 &self,
1170 transaction: &mut T,
1171 key: &StateKey,
1172 snapshot: StateRecordSnapshot,
1173 current: Option<RunRecord>,
1174 observed_pages: &mut Vec<StateRecordSnapshot>,
1175 ) -> Result<RunRecord, StorageFailure> {
1176 RecordCodec::preflight_epoch(&snapshot.record.value).map_err(StorageFailure::permanent)?;
1177 RecordKey::validate_value(key.key(), &snapshot.record.value)
1178 .map_err(StorageFailure::permanent)?;
1179 let record = self
1180 .decode_run(transaction, snapshot, true, observed_pages)
1181 .await?;
1182 match current {
1183 Some(mut current) => {
1184 if !current.same_run(&record) {
1185 return Err(StorageFailure::permanent(AppError::Integration(
1186 "retained checkpoint predecessor has foreign identity".into(),
1187 )));
1188 }
1189 current.previous = Some(record.checkpoint);
1190 Ok(current)
1191 }
1192 None => Ok(record),
1193 }
1194 }
1195
1196 async fn load_checkpoint(
1197 &self,
1198 head: &RunHead,
1199 sequence: u64,
1200 ) -> Result<RunRecord, StorageFailure> {
1201 let mut transaction = self
1202 .inner
1203 .begin_transaction()
1204 .await
1205 .map_err(StorageFailure::from)?;
1206 let mut observed_pages = Vec::new();
1207 let mut missing = false;
1208 let observed = async {
1209 let mut current: Option<RunRecord> = None;
1210 for selected in std::iter::once(sequence).chain(sequence.checked_sub(1)) {
1211 let key = head
1212 .checkpoint_key(selected)
1213 .state_key()
1214 .map_err(StorageFailure::permanent)?;
1215 let Some(snapshot) = transaction
1216 .load(key.clone())
1217 .await
1218 .map_err(StorageFailure::from)?
1219 else {
1220 missing = true;
1221 return Err(StorageFailure::permanent(AppError::Integration(
1222 if current.is_some() {
1223 "retained checkpoint predecessor is missing"
1224 } else {
1225 "retained checkpoint is missing"
1226 }
1227 .into(),
1228 )));
1229 };
1230 current = Some(
1231 self.decode_checkpoint_row(
1232 &mut transaction,
1233 &key,
1234 snapshot,
1235 current,
1236 &mut observed_pages,
1237 )
1238 .await?,
1239 );
1240 }
1241 Ok(current.expect("the selected checkpoint is always observed"))
1242 }
1243 .await;
1244 match observed {
1245 Ok(record) => {
1246 transaction.commit().await.map_err(StorageFailure::from)?;
1247 Ok(record)
1248 }
1249 Err(primary) => {
1250 if transaction.rollback().await.is_err() {
1251 AppTrace::event(
1252 || tracing::warn!(target: "app_kit", event = "app.storage.read_rollback_failed"),
1253 );
1254 return Err(StorageFailure::CleanupFailed(primary.into_error()));
1255 }
1256 if missing {
1257 self.validate_history_head(head).await?;
1258 }
1259 Err(primary)
1260 }
1261 }
1262 }
1263
1264 pub(crate) async fn run_is_active(&self, record: &RunRecord) -> Result<bool, StorageFailure> {
1265 let Some((head, revision)) = self.load_head(RecordKey::run(record)).await? else {
1266 return Ok(false);
1267 };
1268 head.ensure_readable().map_err(StorageFailure::permanent)?;
1269 let active = matches!(head.retention, RunRetention::Active {});
1270 let marker: Option<(Value, u64)> = self.load_record(head.active_key()).await?;
1271 if !self.head_unchanged(&head, revision).await? {
1272 return Err(StorageFailure::Recoverable(AppError::Busy(
1273 "run capacity changed during observation".into(),
1274 )));
1275 }
1276 if active != marker.is_some() {
1277 return Err(StorageFailure::permanent(AppError::Integration(
1278 "run capacity index is inconsistent".into(),
1279 )));
1280 }
1281 Ok(active)
1282 }
1283
1284 pub(crate) async fn checkpoint(
1285 &self,
1286 current: &RunRecord,
1287 selector: &CheckpointSelector,
1288 definition: &CompiledDefinition,
1289 ) -> Result<Option<(RunRecord, u64)>, RuntimeOperationFailure> {
1290 let Some((head, revision)) = self.load_head(RecordKey::run(current)).await? else {
1291 return Ok(None);
1292 };
1293 head.ensure_readable()?;
1294 let validate = |record: &RunRecord| {
1295 if !record.same_run(current) {
1296 return Err(AppError::Integration(
1297 "retained checkpoint has foreign identity".into(),
1298 ));
1299 }
1300 record.validate_recovery(definition)
1301 };
1302 let latest = self.load_checkpoint(&head, head.sequence).await?;
1303 head.validate_row(&latest)?;
1304 validate(&latest)?;
1305 let selected = match selector {
1306 CheckpointSelector::Latest => Some(latest),
1307 CheckpointSelector::Sequence(sequence) if *sequence == head.sequence => Some(latest),
1308 CheckpointSelector::Sequence(sequence) if *sequence <= head.sequence => {
1309 Some(self.load_checkpoint(&head, *sequence).await?)
1310 }
1311 CheckpointSelector::Sequence(_) => None,
1312 CheckpointSelector::Action(ordinal) => {
1313 self.action_checkpoint(&head, &latest, *ordinal, definition)
1314 .await?
1315 }
1316 };
1317 if let Some(record) = &selected {
1318 validate(record)?;
1319 }
1320 self.validate_history_head(&head).await?;
1321 Ok(selected.map(|record| (record, revision)))
1322 }
1323
1324 async fn action_checkpoint(
1325 &self,
1326 head: &RunHead,
1327 latest: &RunRecord,
1328 ordinal: u64,
1329 definition: &CompiledDefinition,
1330 ) -> Result<Option<RunRecord>, RuntimeOperationFailure> {
1331 let ordinal = &ordinal;
1332 let validate = |record: &RunRecord| -> Result<(), RuntimeOperationFailure> {
1333 if !record.same_run(latest) {
1334 return Err(StorageFailure::permanent(AppError::Integration(
1335 "retained checkpoint has foreign identity".into(),
1336 ))
1337 .into());
1338 }
1339 record
1340 .validate_recovery(definition)
1341 .map_err(|error| StorageFailure::permanent(error).into())
1342 };
1343
1344 let prefix = format!("{}action/{ordinal:020}/", head.history_prefix());
1345 let reverse = u64::MAX - head.sequence;
1346 let after = (reverse > 0).then(|| format!("{prefix}{:020}", reverse - 1));
1347 let (rows, _) = self.list_page(Some(&prefix), after.as_deref(), 1).await?;
1348 let indexed = rows
1349 .first()
1350 .map(
1351 |snapshot| match RecordKey::history_key(snapshot.record.key.key())? {
1352 RecordKey::ActionObservation {
1353 workspace,
1354 workflow,
1355 run,
1356 occurrence,
1357 ordinal: actual,
1358 sequence,
1359 } if workspace == head.workspace_id
1360 && workflow == head.workflow_id
1361 && run == head.id
1362 && occurrence == head.creation
1363 && actual == *ordinal
1364 && sequence <= head.sequence =>
1365 {
1366 Ok(sequence)
1367 }
1368 _ => Err(AppError::Integration(
1369 "action observation index is invalid".into(),
1370 )),
1371 },
1372 )
1373 .transpose()
1374 .map_err(StorageFailure::permanent)?;
1375 let committed = latest
1376 .checkpoint
1377 .committed_ordinal()
1378 .is_some_and(|position| *ordinal <= position);
1379 let expected = if committed {
1380 let initial = self.load_checkpoint(head, 0).await?;
1381 validate(&initial)?;
1382 if initial
1383 .checkpoint
1384 .committed_ordinal()
1385 .is_some_and(|inherited| *ordinal <= inherited)
1386 {
1387 None
1389 } else {
1390 let mut lower = 1;
1394 let mut upper = head.sequence;
1395 let mut boundary = latest.clone();
1396 for _ in 0..u64::BITS {
1397 if lower >= upper {
1398 break;
1399 }
1400 let middle = lower + (upper - lower) / 2;
1401 let record = self.load_checkpoint(head, middle).await?;
1402 validate(&record)?;
1403 if record
1404 .checkpoint
1405 .committed_ordinal()
1406 .is_some_and(|position| position >= *ordinal)
1407 {
1408 upper = middle;
1409 boundary = record;
1410 } else {
1411 lower = middle + 1;
1412 }
1413 }
1414 if lower != upper
1415 || boundary.checkpoint.sequence != lower
1416 || !matches!(boundary.checkpoint_kind(), CheckpointKind::Action)
1417 || !boundary
1418 .checkpoint
1419 .outcomes()
1420 .iter()
1421 .any(|outcome| outcome.ordinal == *ordinal)
1422 {
1423 return Err(StorageFailure::permanent(AppError::Integration(
1424 "committed action has no valid observation boundary".into(),
1425 ))
1426 .into());
1427 }
1428 Some(boundary)
1429 }
1430 } else if let Some(sequence) = head.unresolved_failures.get(ordinal) {
1431 let record = self.load_checkpoint(head, *sequence).await?;
1432 validate(&record)?;
1433 if !matches!(record.checkpoint_kind(),
1434 CheckpointKind::Failed { ordinal: actual, .. } if actual == *ordinal)
1435 {
1436 return Err(StorageFailure::permanent(AppError::Integration(
1437 "failed observation evidence identifies another checkpoint".into(),
1438 ))
1439 .into());
1440 }
1441 Some(record)
1442 } else {
1443 None
1444 };
1445 if indexed != expected.as_ref().map(|record| record.checkpoint.sequence) {
1446 self.validate_history_head(head).await?;
1449 return Err(StorageFailure::permanent(AppError::Integration(
1450 "action observation index is inconsistent".into(),
1451 ))
1452 .into());
1453 }
1454 Ok(expected)
1455 }
1456
1457 pub(crate) async fn history(
1458 &self,
1459 current: &RunRecord,
1460 generation: u64,
1461 after: Option<&HistoryCursor>,
1462 limit: NonZeroUsize,
1463 ) -> Result<(Vec<RunRecord>, Option<HistoryCursor>), AppError> {
1464 if limit.get() > MAX_LIST_LIMIT {
1465 return Err(AppError::InvalidInput(
1466 "history page limit exceeds the supported bound".into(),
1467 ));
1468 }
1469 let Some((head, _)) = self
1470 .load_head(RecordKey::run(current))
1471 .await
1472 .map_err(StorageFailure::into_error)?
1473 else {
1474 return Err(AppError::NotFound("workflow run does not exist".into()));
1475 };
1476 head.ensure_readable()?;
1477 let latest = self
1478 .load_checkpoint(&head, head.sequence)
1479 .await
1480 .map_err(StorageFailure::into_error)?;
1481 head.validate_row(&latest)?;
1482 if !latest.same_run(current) {
1483 return Err(AppError::Integration(
1484 "run history has foreign identity".into(),
1485 ));
1486 }
1487 let (cutoff, mut next) = match after {
1488 Some(cursor) => {
1489 let (cutoff, after) = cursor.continue_from(&latest, generation, &head.creation)?;
1490 (
1491 cutoff,
1492 after.checked_add(1).ok_or_else(|| {
1493 AppError::InvalidInput("history cursor is exhausted".into())
1494 })?,
1495 )
1496 }
1497 None => (head.sequence, 0),
1498 };
1499 let prefix = format!("{}checkpoint/", head.history_prefix());
1500 let mut rows: Vec<RunRecord> = Vec::new();
1501 let mut previous = if next > 0 {
1502 Some(
1503 self.load_checkpoint(&head, next - 1)
1504 .await
1505 .map_err(StorageFailure::into_error)?
1506 .checkpoint,
1507 )
1508 } else {
1509 None
1510 };
1511 while next <= cutoff && rows.len() < limit.get() {
1512 let after_key = (next > 0).then(|| format!("{prefix}{:020}", next - 1));
1513 let count = (limit.get() - rows.len()).min(PAGE_SIZE).min(
1514 usize::try_from(cutoff - next)
1515 .unwrap_or(usize::MAX)
1516 .saturating_add(1),
1517 );
1518 let (page, _) = self
1519 .list_page(Some(&prefix), after_key.as_deref(), count)
1520 .await
1521 .map_err(StorageFailure::into_error)?;
1522 if page.is_empty() {
1523 self.validate_history_head(&head)
1524 .await
1525 .map_err(StorageFailure::into_error)?;
1526 return Err(AppError::Integration(
1527 "retained history contains a gap".into(),
1528 ));
1529 }
1530 let mut transaction = self
1531 .inner
1532 .begin_transaction()
1533 .await
1534 .map_err(AppError::from)?;
1535 let mut observed_pages = Vec::new();
1536 let mut missing = false;
1537 let hydrated = async {
1538 for listed in page {
1539 let Some(snapshot) = transaction
1540 .load(listed.record.key.clone())
1541 .await
1542 .map_err(AppError::from)?
1543 else {
1544 missing = true;
1545 return Err(AppError::Integration(
1546 "retained history contains a gap".into(),
1547 ));
1548 };
1549 RecordKey::validate_value(snapshot.record.key.key(), &snapshot.record.value)?;
1550 let mut record = self
1551 .decode_run(&mut transaction, snapshot, true, &mut observed_pages)
1552 .await
1553 .map_err(StorageFailure::into_error)?;
1554 if record.checkpoint.sequence != next || !record.same_run(current) {
1555 return Err(AppError::Integration(
1556 "retained history is not contiguous".into(),
1557 ));
1558 }
1559 record.previous = previous.take();
1560 previous = Some(record.checkpoint.clone());
1561 rows.push(record);
1562 if next == cutoff {
1563 break;
1564 }
1565 next += 1;
1566 }
1567 Ok(())
1568 }
1569 .await;
1570 match hydrated {
1571 Ok(()) => transaction.commit().await.map_err(AppError::from)?,
1572 Err(primary) => {
1573 if transaction.rollback().await.is_err() {
1574 AppTrace::event(
1575 || tracing::warn!(target: "app_kit", event = "app.storage.read_rollback_failed"),
1576 );
1577 return Err(primary);
1578 }
1579 if missing {
1580 self.validate_history_head(&head)
1581 .await
1582 .map_err(StorageFailure::into_error)?;
1583 }
1584 return Err(primary);
1585 }
1586 }
1587 if rows
1588 .last()
1589 .is_some_and(|record| record.checkpoint.sequence == cutoff)
1590 {
1591 break;
1592 }
1593 }
1594 self.validate_history_head(&head)
1595 .await
1596 .map_err(StorageFailure::into_error)?;
1597 let last = rows.last().map(|record| record.checkpoint.sequence);
1598 let cursor = match last {
1599 Some(last) if last < cutoff => Some(match after {
1600 Some(cursor) => cursor.advance_to(last)?,
1601 None => HistoryCursor::issue(&latest, generation, &head.creation, last)?,
1602 }),
1603 _ => None,
1604 };
1605 Ok((rows, cursor))
1606 }
1607
1608 async fn list_page(
1609 &self,
1610 prefix: Option<&str>,
1611 after: Option<&str>,
1612 limit: usize,
1613 ) -> Result<(Vec<StateRecordSnapshot>, Option<String>), StorageFailure> {
1614 let page = self.list_scoped_page(prefix, after, limit).await?;
1615 for snapshot in &page.0 {
1616 RecordCodec::preflight_epoch(&snapshot.record.value)
1617 .map_err(StorageFailure::permanent)?;
1618 RecordKey::validate_value(snapshot.record.key.key(), &snapshot.record.value)
1619 .map_err(StorageFailure::permanent)?;
1620 }
1621 Ok(page)
1622 }
1623
1624 async fn list_scoped_page(
1625 &self,
1626 prefix: Option<&str>,
1627 after: Option<&str>,
1628 limit: usize,
1629 ) -> Result<(Vec<StateRecordSnapshot>, Option<String>), StorageFailure> {
1630 let mut request = StateListRequest::new(NAMESPACE)
1631 .map_err(StorageFailure::from)?
1632 .with_limit(limit)
1633 .map_err(StorageFailure::from)?;
1634 if let Some(prefix) = prefix {
1635 request =
1636 request.with_key_prefix(StateKeyPrefix::new(prefix).map_err(StorageFailure::from)?);
1637 }
1638 if let Some(after) = after {
1639 request = request.start_after(after).map_err(StorageFailure::from)?;
1640 }
1641 let page = self
1642 .inner
1643 .list(request)
1644 .await
1645 .map_err(StorageFailure::from)?;
1646 if page.records.len() > limit {
1647 return Err(StorageFailure::permanent(AppError::Integration(
1648 "storage page exceeds its requested bound".into(),
1649 )));
1650 }
1651 let mut previous = after;
1652 for snapshot in &page.records {
1653 if snapshot.record.key.namespace().as_str() != NAMESPACE
1654 || prefix.is_some_and(|prefix| !snapshot.record.key.key().starts_with(prefix))
1655 || previous.is_some_and(|previous| previous >= snapshot.record.key.key())
1656 {
1657 return Err(StorageFailure::permanent(AppError::Integration(
1658 "storage page has invalid scope or order".into(),
1659 )));
1660 }
1661 previous = Some(snapshot.record.key.key());
1662 }
1663 if page
1664 .next_start_after
1665 .as_deref()
1666 .is_some_and(|next| Some(next) != previous)
1667 || (page.records.is_empty() && page.next_start_after.is_some())
1668 {
1669 return Err(StorageFailure::permanent(AppError::Integration(
1670 "storage page cursor does not advance".into(),
1671 )));
1672 }
1673 Ok((page.records, page.next_start_after))
1674 }
1675
1676 pub(crate) async fn select_run(
1677 &self,
1678 workspace: &WorkspaceId,
1679 workflow: &WorkflowId,
1680 selector: &RunSelector,
1681 ) -> Result<Option<RunKey>, AppError> {
1682 selector.validate()?;
1683 let occurrence = match selector {
1684 RunSelector::Exact { occurrence, .. } => occurrence.clone(),
1685 RunSelector::Latest(id) => {
1686 let Some(last) = self
1687 .projection_tail(workspace, workflow, id, RunProjection::Checkpoint)
1688 .await
1689 .map_err(StorageFailure::into_error)?
1690 else {
1691 return Ok(None);
1692 };
1693 last.occurrence
1694 }
1695 };
1696 let key = RunKey::new(
1697 workspace.clone(),
1698 workflow.clone(),
1699 selector.id().clone(),
1700 occurrence,
1701 );
1702 let Some((head, _)) = self
1703 .load_head(RecordKey::exact_run(&key))
1704 .await
1705 .map_err(StorageFailure::into_error)?
1706 else {
1707 return match selector {
1708 RunSelector::Exact { .. } => Ok(None),
1709 RunSelector::Latest(_) => Err(AppError::Integration(
1710 "saved logical run points to a missing occurrence".into(),
1711 )),
1712 };
1713 };
1714 head.ensure_readable()?;
1715 Ok(Some(key))
1716 }
1717
1718 pub(crate) async fn list_runs(
1719 &self,
1720 workspace: &WorkspaceId,
1721 workflow: &WorkflowId,
1722 after: Option<&RunSelector>,
1723 limit: NonZeroUsize,
1724 ) -> Result<Vec<RunRecord>, AppError> {
1725 if limit.get() > MAX_LIST_LIMIT {
1726 return Err(AppError::InvalidInput(
1727 "run list limit exceeds the supported bound".into(),
1728 ));
1729 }
1730 let prefix = format!("run/{workspace}/{workflow}/");
1731 let mut cursor = match after {
1732 Some(selector) => {
1733 self.select_run(workspace, workflow, selector)
1734 .await?
1735 .ok_or_else(|| AppError::NotFound("run cursor does not exist".into()))?;
1736 Some(match selector {
1737 RunSelector::Latest(id) => format!("{prefix}{id}/~"),
1738 RunSelector::Exact { id, occurrence } => format!("{prefix}{id}/{occurrence}"),
1739 })
1740 }
1741 None => None,
1742 };
1743 let mut records = Vec::new();
1744 loop {
1745 let (page, next) = self
1746 .list_page(
1747 Some(&prefix),
1748 cursor.as_deref(),
1749 (limit.get() - records.len()).min(PAGE_SIZE),
1750 )
1751 .await
1752 .map_err(StorageFailure::into_error)?;
1753 for snapshot in page {
1754 let head = RunHead::decode(&snapshot.record.value)?;
1755 if matches!(head.retention, RunRetention::Deleting { .. }) {
1756 continue;
1757 }
1758 if let Some((record, _)) = self
1759 .load_run(&RunKey::new(
1760 workspace.clone(),
1761 workflow.clone(),
1762 head.id.clone(),
1763 head.creation.clone(),
1764 ))
1765 .await?
1766 {
1767 records.push(record);
1768 }
1769 }
1770 if records.len() == limit.get() || next.is_none() {
1771 return Ok(records);
1772 }
1773 cursor = next;
1774 }
1775 }
1776
1777 pub(crate) async fn has_retained_history(
1778 &self,
1779 workspace: &WorkspaceId,
1780 workflow: Option<&WorkflowId>,
1781 ) -> Result<bool, AppError> {
1782 let prefix = match workflow {
1783 Some(workflow) => format!("run/{workspace}/{workflow}/"),
1784 None => format!("run/{workspace}/"),
1785 };
1786 let mut after = None;
1787 loop {
1788 let (page, next) = self
1789 .list_page(Some(&prefix), after.as_deref(), PAGE_SIZE)
1790 .await
1791 .map_err(StorageFailure::into_error)?;
1792 for snapshot in page {
1793 let revision = snapshot.revision.get();
1794 let head = RunHead::decode(&snapshot.record.value)?;
1795 if !matches!(head.retention, RunRetention::Retained {}) {
1796 continue;
1797 }
1798 let record = self
1799 .load_checkpoint(&head, head.sequence)
1800 .await
1801 .map_err(StorageFailure::into_error)?;
1802 head.validate_row(&record)?;
1803 if !self
1804 .head_unchanged(&head, revision)
1805 .await
1806 .map_err(StorageFailure::into_error)?
1807 {
1808 return Err(AppError::Busy(
1809 "workspace history changed during observation".into(),
1810 ));
1811 }
1812 return Ok(true);
1813 }
1814 match next {
1815 Some(next) => after = Some(next),
1816 None => return Ok(false),
1817 }
1818 }
1819 }
1820
1821 pub(crate) async fn list_all_runs(&self) -> Result<Vec<RunRecord>, AppError> {
1822 self.list_all_runs_for_recovery()
1823 .await
1824 .map_err(StorageFailure::into_error)
1825 }
1826
1827 pub(crate) async fn list_all_runs_for_recovery(
1828 &self,
1829 ) -> Result<Vec<RunRecord>, StorageFailure> {
1830 let mut after = None;
1831 let mut records = Vec::new();
1832 loop {
1833 let (page, next) = self
1834 .list_page(Some("active/"), after.as_deref(), PAGE_SIZE)
1835 .await?;
1836 for snapshot in page {
1837 if records.len() == MAX_RUNS {
1838 return Err(StorageFailure::permanent(AppError::Integration(
1839 "durable run inventory exceeds the private bound".into(),
1840 )));
1841 }
1842 let RecordKey::ActiveRun {
1843 workspace,
1844 workflow,
1845 run,
1846 occurrence,
1847 } = RecordKey::history_key(snapshot.record.key.key())
1848 .map_err(StorageFailure::permanent)?
1849 else {
1850 unreachable!()
1851 };
1852 let observed = async {
1853 let Some((record, _)) = self
1854 .load_run_for_recovery(&RunKey::new(
1855 workspace.clone(),
1856 workflow.clone(),
1857 run.clone(),
1858 occurrence.clone(),
1859 ))
1860 .await?
1861 else {
1862 return Ok(None);
1863 };
1864 if !self.run_is_active(&record).await? {
1865 return Err(StorageFailure::Recoverable(AppError::Busy(
1866 "active run inventory changed".into(),
1867 )));
1868 }
1869 Ok(Some(record))
1870 }
1871 .await;
1872 match observed {
1873 Ok(Some(record)) => records.push(record),
1874 Ok(None)
1875 | Err(StorageFailure::Permanent(AppError::Closed(_) | AppError::NotFound(_))) =>
1876 {
1877 let marker: Option<(Value, u64)> = self
1881 .load_record(RecordKey::ActiveRun {
1882 workspace,
1883 workflow,
1884 run,
1885 occurrence,
1886 })
1887 .await?;
1888 if marker.is_some_and(|(_, revision)| revision == snapshot.revision.get()) {
1889 return Err(StorageFailure::permanent(AppError::Integration(
1890 "active run head is missing or inconsistent".into(),
1891 )));
1892 }
1893 return Err(StorageFailure::Recoverable(AppError::Busy(
1894 "active run inventory changed".into(),
1895 )));
1896 }
1897 Err(failure) => return Err(failure),
1898 }
1899 }
1900 match next {
1901 Some(next) => after = Some(next),
1902 None => return Ok(records),
1903 }
1904 }
1905 }
1906
1907 pub(crate) async fn preflight_records(&self) -> Result<(), AppError> {
1908 let mut after = None;
1909 loop {
1910 let (page, next) = self
1911 .list_page(None, after.as_deref(), PAGE_SIZE)
1912 .await
1913 .map_err(StorageFailure::into_error)?;
1914 for snapshot in page {
1915 if snapshot.record.key.key().starts_with("run/") {
1916 RunHead::decode(&snapshot.record.value)?;
1917 }
1918 }
1919 match next {
1920 Some(next) => after = Some(next),
1921 None => return Ok(()),
1922 }
1923 }
1924 }
1925
1926 pub(crate) async fn pending_deletions(
1927 &self,
1928 after: Option<&str>,
1929 ) -> Result<(Vec<RunKey>, Option<String>), AppError> {
1930 let (page, next) = self
1931 .list_page(Some("run/"), after, PAGE_SIZE)
1932 .await
1933 .map_err(StorageFailure::into_error)?;
1934 let mut deletions = Vec::new();
1935 for snapshot in page {
1936 let head = RunHead::decode(&snapshot.record.value)?;
1937 if matches!(head.retention, RunRetention::Deleting { .. }) {
1938 deletions.push(RunKey::new(
1939 head.workspace_id,
1940 head.workflow_id,
1941 head.id,
1942 head.creation,
1943 ));
1944 }
1945 }
1946 Ok((deletions, next))
1947 }
1948
1949 pub(crate) async fn deletion_checkpoint(
1950 &self,
1951 key: &RunKey,
1952 ) -> Result<Option<u64>, StorageFailure> {
1953 let key = RecordKey::exact_run(key);
1954 Ok(self.load_head(key).await?.and_then(|(head, _)| {
1955 matches!(head.retention, RunRetention::Deleting { .. }).then_some(head.sequence)
1956 }))
1957 }
1958
1959 pub(crate) async fn delete_run_page(
1960 &self,
1961 expected_app_revision: u64,
1962 run: &RunKey,
1963 ) -> Result<Option<AppMutation>, StorageFailure> {
1964 let key = RecordKey::exact_run(run);
1965 let Some((head, revision)) = self.load_head(key.clone()).await? else {
1966 return Ok(None);
1967 };
1968 let RunRetention::Deleting { after } = &head.retention else {
1969 return Err(StorageFailure::permanent(AppError::Conflict(
1970 "run deletion has not been sealed".into(),
1971 )));
1972 };
1973 let (page, next) = self
1976 .list_page(Some(&head.history_prefix()), None, PAGE_SIZE)
1977 .await?;
1978 let mut keys = Vec::with_capacity(page.len());
1979 for snapshot in page {
1980 if after
1981 .as_ref()
1982 .is_some_and(|after| snapshot.record.key.key() <= after.as_str())
1983 {
1984 return Err(StorageFailure::permanent(AppError::Integration(
1985 "run deletion progress skipped a retained record".into(),
1986 )));
1987 }
1988 keys.push(
1989 RecordKey::history_key(snapshot.record.key.key())
1990 .map_err(StorageFailure::permanent)?,
1991 );
1992 }
1993 Ok(Some(AppMutation::new(AppMutationKind::DeleteRunPage {
1994 expected_app_revision,
1995 expected_run_revision: revision,
1996 head: AppStorageRecord::new(key, &head, 0).map_err(StorageFailure::permanent)?,
1997 keys,
1998 complete: next.is_none(),
1999 })))
2000 }
2001
2002 pub(crate) async fn commit(&self, mutation: AppMutation) -> AppCommitOutcome {
2003 let AppMutation { id, kind } = mutation;
2004 let receipt = match RecordKey::Receipt(id.clone()).record(&ReceiptRecord {
2005 epoch: EPOCH.to_owned(),
2006 id: id.clone(),
2007 }) {
2008 Ok(receipt) => receipt,
2009 Err(error) => {
2010 return AppCommitOutcome::Rejected(StorageFailure::permanent(error));
2011 }
2012 };
2013 let mut transaction = match self.inner.begin_transaction().await {
2014 Ok(transaction) => transaction,
2015 Err(error) => return AppCommitOutcome::Rejected(StorageFailure::from(error)),
2016 };
2017 let prepared: Result<(), StorageFailure> = async {
2018 if transaction
2019 .load(receipt.key.clone())
2020 .await
2021 .map_err(StorageFailure::from)?
2022 .is_some()
2023 {
2024 return Err(StorageFailure::permanent(AppError::Conflict(
2025 "mutation identity already exists".to_owned(),
2026 )));
2027 }
2028 kind.stage(&mut transaction, self).await?;
2029 transaction.put(receipt).await.map_err(StorageFailure::from)
2030 }
2031 .await;
2032 if let Err(primary) = prepared {
2033 if transaction.rollback().await.is_err() {
2034 AppTrace::event(
2035 || tracing::warn!(target: "app_kit", event = "app.storage.mutation_rollback_failed"),
2036 );
2037 return AppCommitOutcome::Rejected(StorageFailure::CleanupFailed(
2038 primary.into_error(),
2039 ));
2040 }
2041 return AppCommitOutcome::Rejected(primary);
2042 }
2043 match transaction.commit().await {
2044 Ok(()) => {
2045 self.cleanup_receipt(&id).await;
2046 AppCommitOutcome::Applied
2047 }
2048 Err(
2049 error @ (StateStoreError::TransactionConflict { .. }
2050 | StateStoreError::InvalidConfig(_)
2051 | StateStoreError::InvalidRequest(_)
2052 | StateStoreError::Serialization(_)),
2053 ) => AppCommitOutcome::Rejected(StorageFailure::from(error)),
2054 Err(
2055 error @ (StateStoreError::Unavailable { .. }
2056 | StateStoreError::BackendOperation { .. }
2057 | StateStoreError::TimedOut { .. }),
2058 ) => {
2059 let unknown = StorageFailure::from(error);
2060 let key = match RecordKey::Receipt(id.clone()).state_key() {
2061 Ok(key) => key,
2062 Err(error) => {
2063 return AppCommitOutcome::Unknown(StorageFailure::permanent(error));
2064 }
2065 };
2066 let snapshot = match self.inner.load(key).await {
2067 Ok(Some(snapshot)) => snapshot,
2068 Ok(None) => return AppCommitOutcome::Unknown(unknown),
2069 Err(error) => {
2070 return AppCommitOutcome::Unknown(StorageFailure::from(error));
2071 }
2072 };
2073 if let Err(error) = RecordCodec::preflight_epoch(&snapshot.record.value) {
2074 return AppCommitOutcome::Unknown(StorageFailure::permanent(error));
2075 }
2076 match RecordCodec::decode::<ReceiptRecord>(&snapshot.record.value) {
2077 Ok(receipt) if receipt.id == id => {
2078 self.cleanup_receipt(&receipt.id).await;
2079 AppCommitOutcome::Applied
2080 }
2081 Ok(_) | Err(_) => AppCommitOutcome::Unknown(StorageFailure::permanent(
2082 AppError::Integration("mutation receipt is invalid".to_owned()),
2083 )),
2084 }
2085 }
2086 }
2087 }
2088
2089 async fn load_record<T>(&self, key: RecordKey) -> Result<Option<(T, u64)>, StorageFailure>
2090 where
2091 T: DeserializeOwned,
2092 {
2093 let state_key = key.state_key().map_err(StorageFailure::permanent)?;
2094 let snapshot = self
2095 .inner
2096 .load(state_key.clone())
2097 .await
2098 .map_err(StorageFailure::from)?;
2099 snapshot
2100 .map(|snapshot| {
2101 RecordCodec::preflight_epoch(&snapshot.record.value)
2102 .map_err(StorageFailure::permanent)?;
2103 RecordKey::validate_value(state_key.key(), &snapshot.record.value)
2104 .map_err(StorageFailure::permanent)?;
2105 let revision = snapshot.revision.get();
2106 RecordCodec::decode(&snapshot.record.value)
2107 .map(|record| (record, revision))
2108 .map_err(StorageFailure::permanent)
2109 })
2110 .transpose()
2111 }
2112
2113 pub(crate) async fn cleanup_receipts(&self) -> Result<(), AppError> {
2114 let mut after = None;
2115 loop {
2116 let (page, next) = self
2117 .list_page(Some("receipt/"), after.as_deref(), PAGE_SIZE)
2118 .await
2119 .map_err(StorageFailure::into_error)?;
2120 if !page.is_empty() {
2121 let mut transaction = self
2122 .inner
2123 .begin_transaction()
2124 .await
2125 .map_err(AppError::from)?;
2126 for snapshot in page {
2127 transaction
2128 .delete(snapshot.record.key)
2129 .await
2130 .map_err(AppError::from)?;
2131 }
2132 transaction.commit().await.map_err(AppError::from)?;
2133 }
2134 match next {
2135 Some(next) => after = Some(next),
2136 None => return Ok(()),
2137 }
2138 }
2139 }
2140
2141 async fn cleanup_receipt(&self, id: &str) {
2142 let retired = {
2145 let Ok(mut retired) = self.retired_receipts.lock() else {
2146 return;
2147 };
2148 retired.push(id.to_owned());
2149 if retired.len() < PAGE_SIZE {
2150 return;
2151 }
2152 std::mem::take(&mut *retired)
2153 };
2154 let Ok(mut transaction) = self.inner.begin_transaction().await else {
2155 return;
2156 };
2157 for id in retired {
2158 let Ok(key) = RecordKey::Receipt(id.clone()).state_key() else {
2159 continue;
2160 };
2161 let snapshot = match transaction.load(key.clone()).await {
2162 Ok(Some(snapshot)) => snapshot,
2163 Ok(None) => continue,
2164 Err(_) => {
2165 let _ = transaction.rollback().await;
2166 return;
2167 }
2168 };
2169 if RecordCodec::preflight_epoch(&snapshot.record.value).is_err() {
2170 continue;
2171 }
2172 let Ok(receipt) = RecordCodec::decode::<ReceiptRecord>(&snapshot.record.value) else {
2173 continue;
2174 };
2175 if receipt.id != id {
2176 continue;
2177 }
2178 if transaction.delete(key).await.is_err() {
2179 let _ = transaction.rollback().await;
2180 return;
2181 }
2182 }
2183 let _ = transaction.commit().await;
2186 }
2187}
2188
2189impl AppMutationKind {
2190 fn stage<'a, T: StateTransactionBackend, CS: StateStoreBackend>(
2191 self,
2192 transaction: &'a mut T,
2193 store: &'a AppStore<CS>,
2194 ) -> Pin<Box<impl Future<Output = Result<(), StorageFailure>> + 'a>> {
2195 Box::pin(async move {
2196 let mut observed_pages = Vec::new();
2197 match self {
2198 Self::ClaimApp {
2199 fence,
2200 app,
2201 definitions,
2202 } => {
2203 match fence {
2204 AppClaimFence::Missing => {
2205 app.key().expect_missing(transaction).await?;
2206 let intended = RecordCodec::decode::<AppRecord>(app.value())
2207 .map_err(StorageFailure::permanent)?;
2208 if intended.epoch != EPOCH
2209 || intended.generation != 2
2210 || intended.run_count != 0
2211 || !matches!(&intended.shutdown, ShutdownRecord::Active)
2212 {
2213 return Err(StorageFailure::permanent(AppError::Conflict(
2214 "initial application generation claim is invalid".to_owned(),
2215 )));
2216 }
2217 }
2218 AppClaimFence::Revision(expected) => {
2219 let snapshot = transaction
2220 .load(app.key().state_key().map_err(StorageFailure::permanent)?)
2221 .await
2222 .map_err(StorageFailure::from)?
2223 .ok_or_else(|| {
2224 StorageFailure::permanent(AppError::NotFound(
2225 "durable record does not exist".to_owned(),
2226 ))
2227 })?;
2228 if snapshot.revision.get() != expected {
2229 return Err(StorageFailure::permanent(AppError::Conflict(
2230 "durable record revision fence is stale".to_owned(),
2231 )));
2232 }
2233 RecordCodec::preflight_epoch(&snapshot.record.value)
2234 .map_err(StorageFailure::permanent)?;
2235 RecordKey::validate_value(
2236 snapshot.record.key.key(),
2237 &snapshot.record.value,
2238 )
2239 .map_err(StorageFailure::permanent)?;
2240 let current = RecordCodec::decode::<AppRecord>(&snapshot.record.value)
2241 .map_err(StorageFailure::permanent)?;
2242 let intended = RecordCodec::decode::<AppRecord>(app.value())
2243 .map_err(StorageFailure::permanent)?;
2244 let next_generation =
2245 current.generation.checked_add(1).ok_or_else(|| {
2246 StorageFailure::permanent(AppError::Conflict(
2247 "application generation is exhausted".to_owned(),
2248 ))
2249 })?;
2250 let next_shutdown =
2251 if matches!(¤t.shutdown, ShutdownRecord::Closed) {
2252 ShutdownRecord::Active
2253 } else {
2254 current.shutdown.clone()
2255 };
2256 if intended.epoch != current.epoch
2257 || intended.generation != next_generation
2258 || intended.run_count != current.run_count
2259 || intended.shutdown != next_shutdown
2260 {
2261 return Err(StorageFailure::permanent(AppError::Conflict(
2262 "application generation claim changed durable authority"
2263 .to_owned(),
2264 )));
2265 }
2266 }
2267 }
2268 for definition in definitions {
2269 definition.catalog(transaction).await?;
2270 }
2271 app.put(transaction).await
2272 }
2273 Self::AdvanceLifecycle {
2274 expected_app_revision,
2275 generation,
2276 shutdown,
2277 } => {
2278 let key = RecordKey::App
2279 .state_key()
2280 .map_err(StorageFailure::permanent)?;
2281 let snapshot = transaction
2282 .load(key.clone())
2283 .await
2284 .map_err(StorageFailure::from)?
2285 .ok_or_else(|| {
2286 StorageFailure::permanent(AppError::NotFound(
2287 "durable record does not exist".to_owned(),
2288 ))
2289 })?;
2290 if snapshot.revision.get() != expected_app_revision {
2291 return Err(StorageFailure::permanent(AppError::Conflict(
2292 "durable record revision fence is stale".to_owned(),
2293 )));
2294 }
2295 RecordCodec::preflight_epoch(&snapshot.record.value)
2296 .map_err(StorageFailure::permanent)?;
2297 RecordKey::validate_value(key.key(), &snapshot.record.value)
2298 .map_err(StorageFailure::permanent)?;
2299 let mut record = RecordCodec::decode::<AppRecord>(&snapshot.record.value)
2300 .map_err(StorageFailure::permanent)?;
2301 if record.generation != generation {
2302 return Err(StorageFailure::permanent(AppError::Closed(
2303 "application generation has been superseded".to_owned(),
2304 )));
2305 }
2306 let legal = matches!(
2307 (&record.shutdown, &shutdown),
2308 (ShutdownRecord::Active, ShutdownRecord::Draining)
2309 | (ShutdownRecord::Active, ShutdownRecord::Cancelling)
2310 | (ShutdownRecord::Draining, ShutdownRecord::Cancelling)
2311 | (ShutdownRecord::Draining, ShutdownRecord::Closed)
2312 | (ShutdownRecord::Cancelling, ShutdownRecord::Closed)
2313 );
2314 if !legal {
2315 return Err(StorageFailure::permanent(AppError::Conflict(
2316 "application lifecycle transition is invalid".to_owned(),
2317 )));
2318 }
2319 record.shutdown = shutdown;
2320 AppStorageRecord::app(&record)
2321 .map_err(StorageFailure::permanent)?
2322 .put(transaction)
2323 .await
2324 }
2325 Self::AdmitWorkspace {
2326 expected_app_revision,
2327 app,
2328 workspace,
2329 } => {
2330 app.key()
2331 .expect_revision(transaction, expected_app_revision)
2332 .await?;
2333 workspace.key().expect_missing(transaction).await?;
2334 app.put(transaction).await?;
2335 workspace.put(transaction).await
2336 }
2337 Self::AdvanceWorkspace {
2338 expected_app_revision,
2339 expected_workspace_revision,
2340 workspace,
2341 } => {
2342 RecordKey::App
2343 .expect_revision(transaction, expected_app_revision)
2344 .await?;
2345 workspace
2346 .key()
2347 .expect_revision(transaction, expected_workspace_revision)
2348 .await?;
2349 workspace.put(transaction).await
2350 }
2351 Self::AdvanceWorkspaceWithApp {
2352 expected_app_revision,
2353 expected_workspace_revision,
2354 app,
2355 workspace,
2356 } => {
2357 app.key()
2358 .expect_revision(transaction, expected_app_revision)
2359 .await?;
2360 workspace
2361 .key()
2362 .expect_revision(transaction, expected_workspace_revision)
2363 .await?;
2364 app.put(transaction).await?;
2365 workspace.put(transaction).await
2366 }
2367 Self::DeleteWorkspace {
2368 expected_app_revision,
2369 expected_workspace_revision,
2370 workspace,
2371 } => {
2372 RecordKey::App
2373 .expect_revision(transaction, expected_app_revision)
2374 .await?;
2375 workspace
2376 .expect_revision(transaction, expected_workspace_revision)
2377 .await?;
2378 workspace.delete(transaction).await
2379 }
2380 Self::AdmitRun {
2381 expected_app_revision,
2382 expected_workspace_revision,
2383 mut app,
2384 workspace,
2385 run,
2386 head,
2387 source,
2388 } => {
2389 app.rebase_capacity(transaction, expected_app_revision, 1)
2390 .await?;
2391 workspace
2392 .key()
2393 .expect_revision(transaction, expected_workspace_revision)
2394 .await?;
2395 if let Some(source) = source {
2396 source.validate(transaction, store).await?;
2397 }
2398 app.put(transaction).await?;
2399 run.admit(transaction, head).await
2400 }
2401 Self::AdvanceRun {
2402 expected_app_revision,
2403 expected_run_revision,
2404 mut app,
2405 run,
2406 predecessor,
2407 children,
2408 } => {
2409 app.rebase_capacity(transaction, expected_app_revision, 0)
2410 .await?;
2411 let head = run.load_head(transaction, expected_run_revision).await?;
2412 let mut released = 0_usize;
2413 if !children.is_empty() {
2414 let parent: RunRecord =
2415 run.decode_run().map_err(StorageFailure::permanent)?;
2416 let previous = &predecessor;
2417 let deleting = matches!(
2418 parent.checkpoint.phase,
2419 CheckpointPhaseRecord::Routing { .. }
2420 ) && parent.checkpoint.base_sequence
2421 == parent.checkpoint.sequence;
2422 let mut admitted = BTreeMap::new();
2423 if deleting {
2424 for activation in &previous.checkpoint.staged {
2425 if let Some((sequence, members)) = &activation.children {
2426 for (index, member) in members.iter().enumerate() {
2427 admitted.insert(
2428 &member.key,
2429 (
2430 *sequence,
2431 index,
2432 member.revision.as_str(),
2433 member.history,
2434 ),
2435 );
2436 }
2437 }
2438 }
2439 } else {
2440 for activation in &parent.checkpoint.staged {
2443 if !matches!(
2444 activation.outcome,
2445 StagedOutcomeRecord::Complete { .. }
2446 ) {
2447 continue;
2448 }
2449 let Some((sequence, members)) = &activation.children else {
2450 continue;
2451 };
2452 let Some(control) = previous.checkpoint.control(*sequence) else {
2453 continue;
2454 };
2455 let ControlRecordState::Child {
2456 inputs,
2457 admitted: original,
2458 } = &control.state
2459 else {
2460 continue;
2461 };
2462 if activation.node != control.node
2463 || activation.lineage != control.lineage
2464 || members != original
2465 || members.len() != inputs.len()
2466 || parent.checkpoint.control(*sequence).is_some()
2467 || parent.checkpoint.base_sequence
2468 != previous.checkpoint.base_sequence
2469 {
2470 return Err(StorageFailure::permanent(AppError::Conflict(
2471 "child capacity requires its completed original Stage"
2472 .into(),
2473 )));
2474 }
2475 for (index, member) in members.iter().enumerate() {
2476 admitted.insert(
2477 &member.key,
2478 (
2479 *sequence,
2480 index,
2481 member.revision.as_str(),
2482 member.history,
2483 ),
2484 );
2485 }
2486 }
2487 }
2488 for (child, revision) in children {
2489 let key = RecordKey::Run {
2490 workspace: child.workspace().clone(),
2491 workflow: child.workflow().clone(),
2492 run: child.run().clone(),
2493 occurrence: child.occurrence().to_owned(),
2494 }
2495 .state_key()
2496 .map_err(StorageFailure::permanent)?;
2497 let snapshot = transaction
2498 .load(key.clone())
2499 .await
2500 .map_err(StorageFailure::from)?
2501 .ok_or_else(|| {
2502 StorageFailure::permanent(AppError::Integration(
2503 "copied child head is missing".into(),
2504 ))
2505 })?;
2506 RecordKey::validate_value(key.key(), &snapshot.record.value)
2507 .map_err(StorageFailure::permanent)?;
2508 let mut child_head = RunHead::decode(&snapshot.record.value)
2509 .map_err(StorageFailure::permanent)?;
2510 let key = child_head
2511 .checkpoint_key(child_head.sequence)
2512 .state_key()
2513 .map_err(StorageFailure::permanent)?;
2514 let snapshot = transaction
2515 .load(key.clone())
2516 .await
2517 .map_err(StorageFailure::from)?
2518 .ok_or_else(|| {
2519 StorageFailure::permanent(AppError::Integration(
2520 "copied child checkpoint is missing".into(),
2521 ))
2522 })?;
2523 RecordKey::validate_value(key.key(), &snapshot.record.value)
2524 .map_err(StorageFailure::permanent)?;
2525 let record: RunRecord = store
2526 .decode_run(transaction, snapshot, false, &mut observed_pages)
2527 .await?;
2528 child_head
2529 .validate_row(&record)
2530 .map_err(StorageFailure::permanent)?;
2531 let relation = record.parent.as_ref().ok_or_else(|| {
2532 StorageFailure::permanent(AppError::Conflict(
2533 "copied child has no original parent".into(),
2534 ))
2535 })?;
2536 let membership = admitted.remove(&child);
2537 if !matches!(record.checkpoint.phase, CheckpointPhaseRecord::Succeeded)
2538 || record.workspace_id != parent.workspace_id
2539 || relation.workflow_id != parent.workflow_id || relation.run_id != parent.id
2540 || relation.occurrence != parent.occurrence
2541 || relation.base_checkpoint != previous.checkpoint.base_sequence
2542 || previous.parent.as_ref().map_or(0, |parent| parent.depth).checked_add(1) != Some(relation.depth)
2543 || relation.depth > crate::workflow::run::MAX_CHILD_DEPTH
2544 || membership.is_none_or(|(sequence, index, selected, history)| {
2545 sequence != relation.control_sequence || index != relation.index || selected != revision
2546 || deleting && history != crate::workflow::definition::ChildHistory::DeleteAfterReduction
2547 })
2548 || record.definition_revision != revision
2549 || record.child_revisions != previous.child_revisions
2550 || record.agent_revisions.iter().any(|(key, revision)| previous.agent_revisions.get(key) != Some(revision))
2551 {
2552 return Err(StorageFailure::permanent(AppError::Conflict("copied child authority changed".into())));
2553 }
2554 match child_head.retention {
2555 RunRetention::Active {} => {
2556 child_head.active_key().expect_present(transaction).await?;
2557 child_head.active_key().delete(transaction).await?;
2558 released += 1;
2559 }
2560 RunRetention::Retained {} => {
2561 child_head.active_key().expect_missing(transaction).await?;
2562 if !deleting {
2563 continue;
2564 }
2565 }
2566 RunRetention::Deleting { .. } => {
2567 return Err(StorageFailure::permanent(AppError::Conflict(
2568 "copied child is already sealed".into(),
2569 )));
2570 }
2571 }
2572 child_head.retention = if deleting {
2573 RunRetention::Deleting { after: None }
2574 } else {
2575 RunRetention::Retained {}
2576 };
2577 transaction
2578 .put(child_head.record().map_err(StorageFailure::permanent)?)
2579 .await
2580 .map_err(StorageFailure::from)?;
2581 }
2582 }
2583 if released != 0 {
2584 let mut app: AppRecord =
2585 RecordCodec::decode(app.value()).map_err(StorageFailure::permanent)?;
2586 app.run_count = app.run_count.checked_sub(released).ok_or_else(|| {
2587 StorageFailure::permanent(AppError::Integration(
2588 "copied child capacity is inconsistent".into(),
2589 ))
2590 })?;
2591 AppStorageRecord::app(&app)
2592 .map_err(StorageFailure::permanent)?
2593 .put(transaction)
2594 .await?;
2595 }
2596 run.append(transaction, head, predecessor, store, &mut observed_pages)
2597 .await
2598 }
2599 Self::AdmitChildren {
2600 expected_app_revision,
2601 expected_workspace_revision,
2602 expected_parent_revision,
2603 mut app,
2604 workspace,
2605 parent,
2606 predecessor,
2607 children,
2608 } => {
2609 app.rebase_capacity(
2610 transaction,
2611 expected_app_revision,
2612 children.len() as isize,
2613 )
2614 .await?;
2615 workspace
2616 .key()
2617 .expect_revision(transaction, expected_workspace_revision)
2618 .await?;
2619 let head = parent
2620 .load_head(transaction, expected_parent_revision)
2621 .await?;
2622 for (_, _, source) in &children {
2623 if let Some(source) = source {
2624 source.validate(transaction, store).await?;
2625 }
2626 }
2627 app.put(transaction).await?;
2628 parent
2629 .append(transaction, head, predecessor, store, &mut observed_pages)
2630 .await?;
2631 for (child, head, _) in children {
2632 child.admit(transaction, head).await?;
2633 }
2634 Ok(())
2635 }
2636 Self::RetainRun {
2637 expected_app_revision,
2638 expected_run_revision,
2639 mut app,
2640 run,
2641 } => {
2642 app.rebase_capacity(transaction, expected_app_revision, -1)
2643 .await?;
2644 let mut head = run.load_head(transaction, expected_run_revision).await?;
2645 let record: RunRecord = run.decode_run().map_err(StorageFailure::permanent)?;
2646 head.validate_row(&record)
2647 .map_err(StorageFailure::permanent)?;
2648 if !record.checkpoint.is_terminal()
2649 || !matches!(head.retention, RunRetention::Active {})
2650 {
2651 return Err(StorageFailure::permanent(AppError::Conflict(
2652 "run capacity is not eligible for release".into(),
2653 )));
2654 }
2655 run.expect_value(transaction, store, &mut observed_pages)
2656 .await?;
2657 head.active_key().expect_present(transaction).await?;
2658 head.retention = RunRetention::Retained {};
2659 app.put(transaction).await?;
2660 head.active_key().delete(transaction).await?;
2661 transaction
2662 .put(head.record().map_err(StorageFailure::permanent)?)
2663 .await
2664 .map_err(StorageFailure::from)
2665 }
2666 Self::ReadmitRun {
2667 expected_app_revision,
2668 expected_workspace_revision,
2669 expected_run_revision,
2670 mut app,
2671 workspace,
2672 run,
2673 predecessor,
2674 } => {
2675 app.rebase_capacity(transaction, expected_app_revision, 1)
2676 .await?;
2677 workspace
2678 .key()
2679 .expect_revision(transaction, expected_workspace_revision)
2680 .await?;
2681 let mut head = run.load_head(transaction, expected_run_revision).await?;
2682 if !matches!(head.retention, RunRetention::Retained {}) {
2683 return Err(StorageFailure::permanent(AppError::Conflict(
2684 "run capacity is already admitted".into(),
2685 )));
2686 }
2687 let prior = transaction
2688 .load(
2689 head.checkpoint_key(head.sequence)
2690 .state_key()
2691 .map_err(StorageFailure::permanent)?,
2692 )
2693 .await
2694 .map_err(StorageFailure::from)?
2695 .ok_or_else(|| {
2696 StorageFailure::permanent(AppError::Integration(
2697 "retained run checkpoint is missing".into(),
2698 ))
2699 })?;
2700 RecordCodec::preflight_epoch(&prior.record.value)
2701 .map_err(StorageFailure::permanent)?;
2702 RecordKey::validate_value(prior.record.key.key(), &prior.record.value)
2703 .map_err(StorageFailure::permanent)?;
2704 let prior: RunRecord = store
2705 .decode_run(transaction, prior, false, &mut observed_pages)
2706 .await?;
2707 head.validate_row(&prior)
2708 .map_err(StorageFailure::permanent)?;
2709 head.active_key().expect_missing(transaction).await?;
2710 head.retention = RunRetention::Active {};
2711 let active = head
2712 .active_key()
2713 .index_record()
2714 .map_err(StorageFailure::permanent)?;
2715 app.put(transaction).await?;
2716 run.append(transaction, head, predecessor, store, &mut observed_pages)
2717 .await?;
2718 transaction.put(active).await.map_err(StorageFailure::from)
2719 }
2720 Self::SealRun {
2721 expected_app_revision,
2722 expected_run_revision,
2723 run,
2724 parent,
2725 } => {
2726 RecordKey::App
2727 .expect_revision(transaction, expected_app_revision)
2728 .await?;
2729 let mut head = run.load_head(transaction, expected_run_revision).await?;
2730 let record: RunRecord = run.decode_run().map_err(StorageFailure::permanent)?;
2731 head.validate_row(&record)
2732 .map_err(StorageFailure::permanent)?;
2733 if !matches!(head.retention, RunRetention::Retained {})
2734 || !record.checkpoint.is_terminal()
2735 {
2736 return Err(StorageFailure::permanent(AppError::Conflict(
2737 "run history is not eligible for deletion".into(),
2738 )));
2739 }
2740 run.expect_value(transaction, store, &mut observed_pages)
2741 .await?;
2742 match (&record.parent, parent) {
2743 (Some(relation), Some((parent, revision))) => {
2744 let parent_record: RunRecord =
2745 parent.decode_run().map_err(StorageFailure::permanent)?;
2746 if parent_record.workspace_id != record.workspace_id
2747 || parent_record.workflow_id != relation.workflow_id
2748 || parent_record.id != relation.run_id
2749 || parent_record.occurrence != relation.occurrence
2750 {
2751 return Err(StorageFailure::permanent(AppError::Conflict(
2752 "deletion parent fence has foreign identity".into(),
2753 )));
2754 }
2755 let parent_head = parent.load_head(transaction, revision).await?;
2756 parent_head
2757 .ensure_readable()
2758 .map_err(StorageFailure::permanent)?;
2759 parent_head
2760 .validate_row(&parent_record)
2761 .map_err(StorageFailure::permanent)?;
2762 parent
2763 .expect_value(transaction, store, &mut observed_pages)
2764 .await?;
2765 }
2766 (Some(relation), None) => {
2767 RecordKey::Run {
2768 workspace: record.workspace_id.clone(),
2769 workflow: relation.workflow_id.clone(),
2770 run: relation.run_id.clone(),
2771 occurrence: relation.occurrence.clone(),
2772 }
2773 .expect_missing(transaction)
2774 .await?;
2775 }
2776 (None, None) => {}
2777 (None, Some(_)) => {
2778 return Err(StorageFailure::permanent(AppError::Conflict(
2779 "root deletion cannot fence a parent".into(),
2780 )));
2781 }
2782 }
2783 head.active_key().expect_missing(transaction).await?;
2784 head.retention = RunRetention::Deleting { after: None };
2785 transaction
2786 .put(head.record().map_err(StorageFailure::permanent)?)
2787 .await
2788 .map_err(StorageFailure::from)
2789 }
2790 Self::DeleteRunPage {
2791 expected_app_revision,
2792 expected_run_revision,
2793 head,
2794 keys,
2795 complete,
2796 } => {
2797 RecordKey::App
2798 .expect_revision(transaction, expected_app_revision)
2799 .await?;
2800 head.key()
2801 .expect_revision(transaction, expected_run_revision)
2802 .await?;
2803 head.expect_value(transaction, store, &mut observed_pages)
2804 .await?;
2805 let mut decoded =
2806 RunHead::decode(head.value()).map_err(StorageFailure::permanent)?;
2807 let RunRetention::Deleting { after } = &decoded.retention else {
2808 return Err(StorageFailure::permanent(AppError::Conflict(
2809 "run history deletion is not admitted".into(),
2810 )));
2811 };
2812 let mut previous = after.clone();
2813 for key in &keys {
2814 let text = key
2815 .state_key()
2816 .map_err(StorageFailure::permanent)?
2817 .key()
2818 .to_owned();
2819 let valid = match key {
2820 RecordKey::Checkpoint {
2821 workspace,
2822 workflow,
2823 run,
2824 occurrence,
2825 sequence,
2826 }
2827 | RecordKey::Ledger {
2828 workspace,
2829 workflow,
2830 run,
2831 occurrence,
2832 sequence,
2833 ..
2834 }
2835 | RecordKey::ActionObservation {
2836 workspace,
2837 workflow,
2838 run,
2839 occurrence,
2840 sequence,
2841 ..
2842 } => {
2843 workspace == &decoded.workspace_id
2844 && workflow == &decoded.workflow_id
2845 && run == &decoded.id
2846 && occurrence == &decoded.creation
2847 && *sequence <= decoded.sequence
2848 }
2849 RecordKey::ProjectionLink { key, sequence, .. } => {
2850 key.workspace() == &decoded.workspace_id
2851 && key.workflow() == &decoded.workflow_id
2852 && key.run() == &decoded.id
2853 && key.occurrence() == decoded.creation
2854 && *sequence <= decoded.sequence
2855 }
2856 _ => false,
2857 };
2858 if !valid || previous.as_ref().is_some_and(|previous| previous >= &text) {
2859 return Err(StorageFailure::permanent(AppError::Integration(
2860 "run deletion page is invalid".into(),
2861 )));
2862 }
2863 previous = Some(text);
2864 }
2865 for key in keys {
2866 match &key {
2867 RecordKey::ActionObservation {
2868 workspace,
2869 workflow,
2870 run,
2871 occurrence,
2872 ordinal,
2873 sequence,
2874 } => {
2875 RunProjection::Action(*ordinal)
2876 .remove(
2877 transaction,
2878 &RunKey::new(
2879 workspace.clone(),
2880 workflow.clone(),
2881 run.clone(),
2882 occurrence.clone(),
2883 ),
2884 *sequence,
2885 EPOCH,
2886 )
2887 .await?;
2888 }
2889 RecordKey::ProjectionLink {
2890 key,
2891 projection,
2892 sequence,
2893 } => {
2894 projection
2895 .remove(transaction, key, *sequence, EPOCH)
2896 .await?;
2897 }
2898 _ => key.delete(transaction).await?,
2899 }
2900 }
2901 if complete {
2902 decoded.head_key().delete(transaction).await
2903 } else {
2904 decoded.retention = RunRetention::Deleting { after: previous };
2905 transaction
2906 .put(decoded.record().map_err(StorageFailure::permanent)?)
2907 .await
2908 .map_err(StorageFailure::from)
2909 }
2910 }
2911 }
2912 })
2913 }
2914}
2915
2916impl AppStorageRecord {
2917 fn key(&self) -> &RecordKey {
2918 match self {
2919 Self::Value { key, .. } | Self::Checkpoint { key, .. } => key,
2920 }
2921 }
2922
2923 fn value(&self) -> &Value {
2924 match self {
2925 Self::Value { value, .. } | Self::Checkpoint { value, .. } => value,
2926 }
2927 }
2928
2929 fn definitions(
2930 definitions: &BTreeMap<WorkflowId, Vec<Arc<CompiledDefinition>>>,
2931 ) -> Result<Vec<Self>, AppError> {
2932 definitions
2933 .iter()
2934 .flat_map(|(id, revisions)| {
2935 revisions.iter().map(move |definition| {
2936 let record = DefinitionRecord {
2937 epoch: EPOCH.into(),
2938 workflow_id: id.clone(),
2939 revision: definition.revision().to_owned(),
2940 canonical: definition.fingerprint().to_owned(),
2941 };
2942 Self::new(record.key(), &record, 0)
2943 })
2944 })
2945 .collect()
2946 }
2947
2948 async fn catalog<T: StateTransactionBackend>(
2949 self,
2950 transaction: &mut T,
2951 ) -> Result<(), StorageFailure> {
2952 let key = self.key().state_key().map_err(StorageFailure::permanent)?;
2953 if let Some(current) = transaction
2954 .load(key.clone())
2955 .await
2956 .map_err(StorageFailure::from)?
2957 {
2958 RecordKey::validate_value(key.key(), ¤t.record.value)
2959 .map_err(StorageFailure::permanent)?;
2960 let current: DefinitionRecord =
2961 RecordCodec::decode(¤t.record.value).map_err(StorageFailure::permanent)?;
2962 let intended: DefinitionRecord =
2963 RecordCodec::decode(self.value()).map_err(StorageFailure::permanent)?;
2964 if current.canonical != intended.canonical {
2965 return Err(StorageFailure::permanent(AppError::Conflict(
2966 "configured workflow revision differs from its durable definition".into(),
2967 )));
2968 }
2969 Ok(())
2970 } else {
2971 self.put(transaction).await
2972 }
2973 }
2974
2975 async fn rebase_capacity<T: StateTransactionBackend>(
2976 &mut self,
2977 transaction: &mut T,
2978 expected: u64,
2979 change: isize,
2980 ) -> Result<(), StorageFailure> {
2981 let snapshot = transaction
2982 .load(self.key().state_key().map_err(StorageFailure::permanent)?)
2983 .await
2984 .map_err(StorageFailure::from)?
2985 .ok_or_else(|| {
2986 StorageFailure::permanent(AppError::NotFound(
2987 "durable record does not exist".into(),
2988 ))
2989 })?;
2990 RecordCodec::preflight_epoch(&snapshot.record.value).map_err(StorageFailure::permanent)?;
2991 RecordKey::validate_value("app", &snapshot.record.value)
2992 .map_err(StorageFailure::permanent)?;
2993 let mut current: AppRecord =
2994 RecordCodec::decode(&snapshot.record.value).map_err(StorageFailure::permanent)?;
2995 let intended = AppRecord::deserialize(self.value()).map_err(|_| {
2996 StorageFailure::permanent(AppError::Integration(
2997 "app durable record is corrupt".into(),
2998 ))
2999 })?;
3000 let count = current.run_count;
3001 current.run_count = intended.run_count;
3004 if current != intended {
3005 return Err(StorageFailure::permanent(AppError::Conflict(
3006 "run capacity mutation changes unrelated application authority".into(),
3007 )));
3008 }
3009 let count = count.checked_add_signed(change).ok_or_else(|| {
3010 StorageFailure::permanent(AppError::Conflict(
3011 "run capacity transition is invalid".into(),
3012 ))
3013 })?;
3014 if snapshot.revision.get() < expected
3015 || snapshot.revision.get() == expected && count != intended.run_count
3016 {
3017 return Err(StorageFailure::permanent(AppError::Conflict(
3018 "run capacity revision or count fence is stale".into(),
3019 )));
3020 }
3021 if count > MAX_RUNS {
3022 return Err(StorageFailure::permanent(AppError::Busy(
3023 "application run capacity is exhausted".into(),
3024 )));
3025 }
3026 current.run_count = count;
3027 *self = Self::app(¤t).map_err(StorageFailure::permanent)?;
3028 Ok(())
3029 }
3030
3031 fn app(record: &AppRecord) -> Result<Self, AppError> {
3032 Self::new(RecordKey::App, record, 0)
3033 }
3034
3035 fn workspace(record: &WorkspaceRecord) -> Result<Self, AppError> {
3036 Self::new(RecordKey::Workspace(record.id.clone()), record, 0)
3037 }
3038
3039 fn predecessor(record: &RunRecord) -> Result<RunRecord, AppError> {
3040 let predecessor = record.observed_predecessor()?;
3041 let key = RecordKey::checkpoint(&predecessor);
3042 let reserved_bytes = predecessor.checkpoint.prospective_settlement_bytes()?;
3043 let state_key = key.state_key()?;
3044 let size = RecordCodec::logical_run_len(&predecessor)?;
3045 if size.saturating_add(reserved_bytes) > MAX_RECORD_BYTES {
3046 return Err(AppError::InvalidInput(
3047 "app durable record exceeds the private byte bound".into(),
3048 ));
3049 }
3050 if RecordKey::history_key(state_key.key())? != key {
3054 return Err(AppError::Integration(
3055 "durable history value does not match its key".into(),
3056 ));
3057 }
3058 Ok(predecessor)
3059 }
3060
3061 fn head(record: &RunRecord) -> Result<Self, AppError> {
3062 let head = RunHead::admitted(record)?;
3063 Self::new(head.head_key(), &head, 0)
3064 }
3065
3066 async fn load_head<T: StateTransactionBackend>(
3067 &self,
3068 transaction: &mut T,
3069 expected: u64,
3070 ) -> Result<RunHead, StorageFailure> {
3071 let key = self
3072 .key()
3073 .head_key()
3074 .map_err(StorageFailure::permanent)?
3075 .state_key()
3076 .map_err(StorageFailure::permanent)?;
3077 let snapshot = transaction
3078 .load(key.clone())
3079 .await
3080 .map_err(StorageFailure::from)?
3081 .ok_or_else(|| {
3082 StorageFailure::permanent(AppError::NotFound("workflow run does not exist".into()))
3083 })?;
3084 if snapshot.revision.get() != expected {
3085 return Err(StorageFailure::permanent(AppError::Conflict(
3086 "run head revision fence is stale".into(),
3087 )));
3088 }
3089 RecordKey::validate_value(key.key(), &snapshot.record.value)
3090 .map_err(StorageFailure::permanent)?;
3091 RunHead::decode(&snapshot.record.value).map_err(StorageFailure::permanent)
3092 }
3093
3094 async fn expect_value<T: StateTransactionBackend, CS: StateStoreBackend>(
3095 &self,
3096 transaction: &mut T,
3097 store: &AppStore<CS>,
3098 observed_pages: &mut Vec<StateRecordSnapshot>,
3099 ) -> Result<(), StorageFailure> {
3100 let snapshot = transaction
3101 .load(self.key().state_key().map_err(StorageFailure::permanent)?)
3102 .await
3103 .map_err(StorageFailure::from)?;
3104 let matches = match snapshot {
3105 Some(snapshot) if matches!(self.key(), RecordKey::Checkpoint { .. }) => {
3106 let actual: RunRecord = store
3107 .decode_run(transaction, snapshot, false, observed_pages)
3108 .await?;
3109 self.matches_run(&actual)
3110 .map_err(StorageFailure::permanent)?
3111 }
3112 Some(snapshot) => snapshot.record.value.as_ref() == self.value(),
3113 None => false,
3114 };
3115 if !matches {
3116 return Err(StorageFailure::permanent(AppError::Conflict(
3117 "immutable checkpoint evidence changed".into(),
3118 )));
3119 }
3120 Ok(())
3121 }
3122
3123 fn decode_run(&self) -> Result<RunRecord, AppError> {
3124 match self {
3125 Self::Checkpoint { record, .. } => Ok(record.as_ref().clone()),
3126 Self::Value { .. } => Err(AppError::Integration("expected checkpoint evidence".into())),
3127 }
3128 }
3129
3130 fn matches_run(&self, actual: &RunRecord) -> Result<bool, AppError> {
3131 match self {
3132 Self::Checkpoint { record, value, .. } => {
3133 let expected: SettlementPages =
3134 RecordCodec::decode(&value["checkpoint"]["settlements"])?;
3135 Ok(actual.same_persisted_row(record)?
3136 && actual.checkpoint.settlement_references() == expected.pages)
3137 }
3138 Self::Value { .. } => Err(AppError::Integration("expected checkpoint evidence".into())),
3139 }
3140 }
3141
3142 fn append<'a, T: StateTransactionBackend, CS: StateStoreBackend>(
3143 self,
3144 transaction: &'a mut T,
3145 mut head: RunHead,
3146 predecessor: RunRecord,
3147 store: &'a AppStore<CS>,
3148 observed_pages: &'a mut Vec<StateRecordSnapshot>,
3149 ) -> Pin<Box<impl Future<Output = Result<(), StorageFailure>> + 'a>> {
3150 Box::pin(async move {
3151 let record: RunRecord = self.decode_run().map_err(StorageFailure::permanent)?;
3152 let prior = transaction
3153 .load(
3154 head.checkpoint_key(head.sequence)
3155 .state_key()
3156 .map_err(StorageFailure::permanent)?,
3157 )
3158 .await
3159 .map_err(StorageFailure::from)?
3160 .ok_or_else(|| {
3161 StorageFailure::permanent(AppError::Integration(
3162 "current checkpoint is missing".into(),
3163 ))
3164 })?;
3165 RecordCodec::preflight_epoch(&prior.record.value).map_err(StorageFailure::permanent)?;
3166 RecordKey::validate_value(prior.record.key.key(), &prior.record.value)
3167 .map_err(StorageFailure::permanent)?;
3168 let prior: RunRecord = store
3169 .decode_run(transaction, prior, false, observed_pages)
3170 .await?;
3171 if RecordKey::checkpoint(&predecessor) != head.checkpoint_key(head.sequence)
3172 || prior.checkpoint.settlement_references()
3173 != predecessor.checkpoint.settlement_references()
3174 || !prior
3175 .same_persisted_row(&predecessor)
3176 .map_err(StorageFailure::permanent)?
3177 {
3178 return Err(StorageFailure::permanent(AppError::Conflict(
3179 "immutable checkpoint predecessor changed".into(),
3180 )));
3181 }
3182 head.validate_row(&prior)
3183 .map_err(StorageFailure::permanent)?;
3184 if !record.same_run(&prior) {
3185 return Err(StorageFailure::permanent(AppError::Integration(
3186 "checkpoint changes immutable run identity".into(),
3187 )));
3188 }
3189 head.advance(&record, &prior)
3190 .map_err(StorageFailure::permanent)?;
3191 self.key().expect_missing(transaction).await?;
3192 for projection in RunProjection::for_checkpoint(&record, Some(&prior)) {
3193 projection
3194 .append(transaction, &record, record.checkpoint.sequence)
3195 .await?;
3196 }
3197 let before = prior.checkpoint.is_copy_clean();
3198 let after = record.checkpoint.is_copy_clean();
3199 if before && !after {
3200 RunProjection::Unsettled
3201 .append(transaction, &record, 0)
3202 .await?;
3203 } else if !before && after {
3204 RunProjection::Unsettled
3205 .remove(transaction, &record.key(), 0, EPOCH)
3206 .await?;
3207 }
3208 self.put(transaction).await?;
3209 transaction
3210 .put(head.record().map_err(StorageFailure::permanent)?)
3211 .await
3212 .map_err(StorageFailure::from)
3213 })
3214 }
3215
3216 async fn admit<T: StateTransactionBackend>(
3217 self,
3218 transaction: &mut T,
3219 head: Self,
3220 ) -> Result<(), StorageFailure> {
3221 let decoded = RunHead::decode(head.value()).map_err(StorageFailure::permanent)?;
3222 let record: RunRecord = self.decode_run().map_err(StorageFailure::permanent)?;
3223 decoded
3224 .validate_row(&record)
3225 .map_err(StorageFailure::permanent)?;
3226 head.key().expect_missing(transaction).await?;
3227 self.key().expect_missing(transaction).await?;
3228 decoded.active_key().expect_missing(transaction).await?;
3229 for projection in RunProjection::for_checkpoint(&record, None) {
3230 projection
3231 .append(transaction, &record, record.checkpoint.sequence)
3232 .await?;
3233 }
3234 if !record.checkpoint.is_copy_clean() {
3235 RunProjection::Unsettled
3236 .append(transaction, &record, 0)
3237 .await?;
3238 }
3239 self.put(transaction).await?;
3240 head.put(transaction).await?;
3241 transaction
3242 .put(
3243 decoded
3244 .active_key()
3245 .index_record()
3246 .map_err(StorageFailure::permanent)?,
3247 )
3248 .await
3249 .map_err(StorageFailure::from)
3250 }
3251
3252 fn new<T>(key: RecordKey, record: &T, reserved_bytes: usize) -> Result<Self, AppError>
3253 where
3254 T: Serialize,
3255 {
3256 let state_key = key.state_key()?;
3257 let value = RecordCodec::encode(record, reserved_bytes)?;
3258 RecordKey::validate_value(state_key.key(), &value)?;
3259 Ok(Self::Value { key, value })
3260 }
3261
3262 async fn put<T: StateTransactionBackend>(
3263 self,
3264 transaction: &mut T,
3265 ) -> Result<(), StorageFailure> {
3266 let (key, value) = match self {
3267 Self::Value { key, value } => (key, value),
3268 Self::Checkpoint {
3269 key, value, pages, ..
3270 } => {
3271 for page in pages {
3272 if transaction
3273 .load(page.key.clone())
3274 .await
3275 .map_err(StorageFailure::from)?
3276 .is_some()
3277 {
3278 return Err(StorageFailure::permanent(AppError::Conflict(
3279 "immutable settlement page already exists".into(),
3280 )));
3281 }
3282 transaction.put(page).await.map_err(StorageFailure::from)?;
3283 }
3284 (key, value)
3285 }
3286 };
3287 transaction
3288 .put(StateRecord::new(
3289 key.state_key().map_err(StorageFailure::permanent)?,
3290 value,
3291 ))
3292 .await
3293 .map_err(StorageFailure::from)
3294 }
3295}
3296
3297impl AppMutation {
3298 pub(crate) fn claim_new_app(
3299 record: &AppRecord,
3300 definitions: &BTreeMap<WorkflowId, Vec<Arc<CompiledDefinition>>>,
3301 ) -> Result<Self, AppError> {
3302 Ok(Self::new(AppMutationKind::ClaimApp {
3303 fence: AppClaimFence::Missing,
3304 app: AppStorageRecord::app(record)?,
3305 definitions: AppStorageRecord::definitions(definitions)?,
3306 }))
3307 }
3308
3309 pub(crate) fn claim_app_revision(
3310 expected_app_revision: u64,
3311 record: &AppRecord,
3312 definitions: &BTreeMap<WorkflowId, Vec<Arc<CompiledDefinition>>>,
3313 ) -> Result<Self, AppError> {
3314 Ok(Self::new(AppMutationKind::ClaimApp {
3315 fence: AppClaimFence::Revision(expected_app_revision),
3316 app: AppStorageRecord::app(record)?,
3317 definitions: AppStorageRecord::definitions(definitions)?,
3318 }))
3319 }
3320
3321 pub(crate) fn advance_lifecycle(
3322 expected_app_revision: u64,
3323 generation: u64,
3324 shutdown: ShutdownRecord,
3325 ) -> Result<Self, AppError> {
3326 if matches!(&shutdown, ShutdownRecord::Active) {
3327 return Err(AppError::Integration(
3328 "application lifecycle mutation cannot publish active state".to_owned(),
3329 ));
3330 }
3331 Ok(Self::new(AppMutationKind::AdvanceLifecycle {
3332 expected_app_revision,
3333 generation,
3334 shutdown,
3335 }))
3336 }
3337
3338 pub(crate) fn admit_workspace(
3339 expected_app_revision: u64,
3340 app: &AppRecord,
3341 workspace: &WorkspaceRecord,
3342 ) -> Result<Self, AppError> {
3343 Ok(Self::new(AppMutationKind::AdmitWorkspace {
3344 expected_app_revision,
3345 app: AppStorageRecord::app(app)?,
3346 workspace: AppStorageRecord::workspace(workspace)?,
3347 }))
3348 }
3349
3350 pub(crate) fn advance_workspace(
3351 expected_app_revision: u64,
3352 expected_workspace_revision: u64,
3353 workspace: &WorkspaceRecord,
3354 ) -> Result<Self, AppError> {
3355 Ok(Self::new(AppMutationKind::AdvanceWorkspace {
3356 expected_app_revision,
3357 expected_workspace_revision,
3358 workspace: AppStorageRecord::workspace(workspace)?,
3359 }))
3360 }
3361
3362 pub(crate) fn advance_workspace_with_app(
3363 expected_app_revision: u64,
3364 expected_workspace_revision: u64,
3365 app: &AppRecord,
3366 workspace: &WorkspaceRecord,
3367 ) -> Result<Self, AppError> {
3368 Ok(Self::new(AppMutationKind::AdvanceWorkspaceWithApp {
3369 expected_app_revision,
3370 expected_workspace_revision,
3371 app: AppStorageRecord::app(app)?,
3372 workspace: AppStorageRecord::workspace(workspace)?,
3373 }))
3374 }
3375
3376 pub(crate) fn delete_workspace(
3377 expected_app_revision: u64,
3378 expected_workspace_revision: u64,
3379 workspace: &WorkspaceId,
3380 ) -> Result<Self, AppError> {
3381 RecordKey::Workspace(workspace.clone()).state_key()?;
3382 Ok(Self::new(AppMutationKind::DeleteWorkspace {
3383 expected_app_revision,
3384 expected_workspace_revision,
3385 workspace: RecordKey::Workspace(workspace.clone()),
3386 }))
3387 }
3388
3389 pub(crate) fn admit_run(
3390 expected_app_revision: u64,
3391 expected_workspace_revision: u64,
3392 app: &AppRecord,
3393 workspace: &WorkspaceRecord,
3394 run: &RunRecord,
3395 ) -> Result<Self, AppError> {
3396 if workspace.id != run.workspace_id || workspace.revision != run.workspace_revision {
3397 return Err(AppError::Integration(
3398 "run admission workspace identity is inconsistent".to_owned(),
3399 ));
3400 }
3401 Ok(Self::new(AppMutationKind::AdmitRun {
3402 expected_app_revision,
3403 expected_workspace_revision,
3404 app: AppStorageRecord::app(app)?,
3405 workspace: AppStorageRecord::workspace(workspace)?,
3406 run: RecordCodec::run(run)?,
3407 head: AppStorageRecord::head(run)?,
3408 source: None,
3409 }))
3410 }
3411
3412 pub(crate) fn admit_restore(
3413 expected_app_revision: u64,
3414 expected_workspace_revision: u64,
3415 app: &AppRecord,
3416 workspace: &WorkspaceRecord,
3417 run: &RunRecord,
3418 selected: &RunCopy,
3419 ) -> Result<Self, AppError> {
3420 if selected.source.as_ref().is_some_and(|(source, _)| {
3421 source.workspace_id != run.workspace_id || source.workflow_id != run.workflow_id
3422 }) {
3423 return Err(AppError::InvalidInput(
3424 "restore source is outside the workflow scope".into(),
3425 ));
3426 }
3427 let mut mutation = Self::admit_run(
3428 expected_app_revision,
3429 expected_workspace_revision,
3430 app,
3431 workspace,
3432 run,
3433 )?;
3434 let AppMutationKind::AdmitRun { source, .. } = &mut mutation.kind else {
3435 unreachable!()
3436 };
3437 *source = Some(Box::new(selected.clone()));
3438 Ok(mutation)
3439 }
3440
3441 pub(crate) fn advance_run(
3442 expected_app_revision: u64,
3443 expected_run_revision: u64,
3444 app: &AppRecord,
3445 run: &RunRecord,
3446 children: &[(RunKey, String)],
3447 ) -> Result<Self, AppError> {
3448 let mut children = children.to_vec();
3449 if children.is_empty()
3450 && let Some(previous) = &run.previous
3451 {
3452 for activation in &run.checkpoint.staged {
3453 if matches!(activation.outcome, StagedOutcomeRecord::Complete { .. })
3454 && let Some((sequence, members)) = &activation.children
3455 && previous.control(*sequence).is_some()
3456 {
3457 children.extend(
3458 members
3459 .iter()
3460 .map(|member| (member.key.clone(), member.revision.clone())),
3461 );
3462 }
3463 }
3464 }
3465 Ok(Self::new(AppMutationKind::AdvanceRun {
3466 expected_app_revision,
3467 expected_run_revision,
3468 app: AppStorageRecord::app(app)?,
3469 run: RecordCodec::run(run)?,
3470 predecessor: AppStorageRecord::predecessor(run)?,
3471 children,
3472 }))
3473 }
3474
3475 pub(crate) fn admit_children<'a>(
3476 expected_app_revision: u64,
3477 expected_workspace_revision: u64,
3478 expected_parent_revision: u64,
3479 app: &AppRecord,
3480 workspace: &WorkspaceRecord,
3481 parent: &RunRecord,
3482 children: impl ExactSizeIterator<Item = (&'a RunRecord, Option<RunCopy>)>,
3483 ) -> Result<Self, AppError> {
3484 if children.len() == 0 {
3485 return Err(AppError::Integration(
3486 "child admission mutation requires at least one child".to_owned(),
3487 ));
3488 }
3489 if workspace.id != parent.workspace_id || workspace.revision != parent.workspace_revision {
3490 return Err(AppError::Integration(
3491 "child admission workspace identity is inconsistent".to_owned(),
3492 ));
3493 }
3494 let mut identities = BTreeSet::new();
3495 let mut stored_children = Vec::with_capacity(children.len());
3496 for (child, source) in children {
3497 let relation_matches = child.parent.as_ref().is_some_and(|relation| {
3498 relation.workflow_id == parent.workflow_id
3499 && relation.run_id == parent.id
3500 && relation.occurrence == parent.occurrence
3501 && parent
3502 .parent
3503 .as_ref()
3504 .map_or(0, |parent| parent.depth)
3505 .checked_add(1)
3506 == Some(relation.depth)
3507 && relation.depth <= crate::workflow::run::MAX_CHILD_DEPTH
3508 });
3509 if child.workspace_id != parent.workspace_id
3510 || child.workspace_revision != parent.workspace_revision
3511 || !relation_matches
3512 {
3513 return Err(AppError::Integration(
3514 "child admission run identity is inconsistent".to_owned(),
3515 ));
3516 }
3517 let stored = RecordCodec::run(child)?;
3518 if !identities.insert(stored.key().clone()) {
3519 return Err(AppError::Integration(
3520 "child admission contains duplicate run identities".to_owned(),
3521 ));
3522 }
3523 stored_children.push((stored, AppStorageRecord::head(child)?, source));
3524 }
3525 Ok(Self::new(AppMutationKind::AdmitChildren {
3526 expected_app_revision,
3527 expected_workspace_revision,
3528 expected_parent_revision,
3529 app: AppStorageRecord::app(app)?,
3530 workspace: AppStorageRecord::workspace(workspace)?,
3531 parent: RecordCodec::run(parent)?,
3532 predecessor: AppStorageRecord::predecessor(parent)?,
3533 children: stored_children,
3534 }))
3535 }
3536
3537 pub(crate) fn retain_run(
3538 expected_app_revision: u64,
3539 expected_run_revision: u64,
3540 app: &AppRecord,
3541 run: &RunRecord,
3542 ) -> Result<Self, AppError> {
3543 Ok(Self::new(AppMutationKind::RetainRun {
3544 expected_app_revision,
3545 expected_run_revision,
3546 app: AppStorageRecord::app(app)?,
3547 run: RecordCodec::run(run)?,
3548 }))
3549 }
3550
3551 pub(crate) fn readmit_run(
3552 expected_app_revision: u64,
3553 expected_workspace_revision: u64,
3554 expected_run_revision: u64,
3555 app: &AppRecord,
3556 workspace: &WorkspaceRecord,
3557 run: &RunRecord,
3558 ) -> Result<Self, AppError> {
3559 if workspace.id != run.workspace_id || workspace.revision != run.workspace_revision {
3560 return Err(AppError::Conflict(
3561 "run readmission workspace identity is stale".into(),
3562 ));
3563 }
3564 Ok(Self::new(AppMutationKind::ReadmitRun {
3565 expected_app_revision,
3566 expected_workspace_revision,
3567 expected_run_revision,
3568 app: AppStorageRecord::app(app)?,
3569 workspace: AppStorageRecord::workspace(workspace)?,
3570 run: RecordCodec::run(run)?,
3571 predecessor: AppStorageRecord::predecessor(run)?,
3572 }))
3573 }
3574
3575 pub(crate) fn seal_run(
3576 expected_app_revision: u64,
3577 expected_run_revision: u64,
3578 run: &RunRecord,
3579 parent: Option<(&RunRecord, u64)>,
3580 ) -> Result<Self, AppError> {
3581 Ok(Self::new(AppMutationKind::SealRun {
3582 expected_app_revision,
3583 expected_run_revision,
3584 run: RecordCodec::run(run)?,
3585 parent: parent
3586 .map(|(record, revision)| RecordCodec::run(record).map(|record| (record, revision)))
3587 .transpose()?,
3588 }))
3589 }
3590
3591 fn new(kind: AppMutationKind) -> Self {
3592 Self {
3593 id: Uuid::new_v4().simple().to_string(),
3594 kind,
3595 }
3596 }
3597}
3598
3599impl RecordKey {
3600 fn ledger(record: &RunRecord, index: usize, sequence: u64) -> Self {
3601 Self::Ledger {
3602 workspace: record.workspace_id.clone(),
3603 workflow: record.workflow_id.clone(),
3604 run: record.id.clone(),
3605 occurrence: record.occurrence.clone(),
3606 index,
3607 sequence,
3608 }
3609 }
3610
3611 fn exact_run(key: &RunKey) -> Self {
3612 Self::Run {
3613 workspace: key.workspace().clone(),
3614 workflow: key.workflow().clone(),
3615 run: key.run().clone(),
3616 occurrence: key.occurrence().to_owned(),
3617 }
3618 }
3619
3620 async fn expect_present<T: StateTransactionBackend>(
3621 &self,
3622 transaction: &mut T,
3623 ) -> Result<(), StorageFailure> {
3624 let key = self.state_key().map_err(StorageFailure::permanent)?;
3625 let snapshot = transaction
3626 .load(key.clone())
3627 .await
3628 .map_err(StorageFailure::from)?
3629 .ok_or_else(|| {
3630 StorageFailure::permanent(AppError::Integration("run index is missing".into()))
3631 })?;
3632 RecordCodec::preflight_epoch(&snapshot.record.value).map_err(StorageFailure::permanent)?;
3633 Self::validate_value(key.key(), &snapshot.record.value).map_err(StorageFailure::permanent)
3634 }
3635
3636 fn run(record: &RunRecord) -> Self {
3637 Self::Run {
3638 workspace: record.workspace_id.clone(),
3639 workflow: record.workflow_id.clone(),
3640 run: record.id.clone(),
3641 occurrence: record.occurrence.clone(),
3642 }
3643 }
3644
3645 fn checkpoint(record: &RunRecord) -> Self {
3646 Self::Checkpoint {
3647 workspace: record.workspace_id.clone(),
3648 workflow: record.workflow_id.clone(),
3649 run: record.id.clone(),
3650 occurrence: record.occurrence.clone(),
3651 sequence: record.checkpoint.sequence,
3652 }
3653 }
3654
3655 fn head_key(&self) -> Result<Self, AppError> {
3656 match self {
3657 Self::Run {
3658 workspace,
3659 workflow,
3660 run,
3661 occurrence,
3662 }
3663 | Self::Checkpoint {
3664 workspace,
3665 workflow,
3666 run,
3667 occurrence,
3668 ..
3669 }
3670 | Self::Ledger {
3671 workspace,
3672 workflow,
3673 run,
3674 occurrence,
3675 ..
3676 }
3677 | Self::ActionObservation {
3678 workspace,
3679 workflow,
3680 run,
3681 occurrence,
3682 ..
3683 }
3684 | Self::ActiveRun {
3685 workspace,
3686 workflow,
3687 run,
3688 occurrence,
3689 } => Ok(Self::Run {
3690 workspace: workspace.clone(),
3691 workflow: workflow.clone(),
3692 run: run.clone(),
3693 occurrence: occurrence.clone(),
3694 }),
3695 Self::ProjectionLink { key, .. } => Ok(Self::exact_run(key)),
3696 Self::App
3697 | Self::Definition { .. }
3698 | Self::Workspace(_)
3699 | Self::Receipt(_)
3700 | Self::ProjectionTail { .. } => {
3701 Err(AppError::Integration("record has no run scope".into()))
3702 }
3703 }
3704 }
3705
3706 fn history_key(key: &str) -> Result<Self, AppError> {
3707 let parts = key.split('/').collect::<Vec<_>>();
3708 let parsed = (|| match parts.as_slice() {
3709 [
3710 "history",
3711 workspace,
3712 workflow,
3713 run,
3714 occurrence,
3715 "checkpoint",
3716 sequence,
3717 ] => Some(Self::Checkpoint {
3718 workspace: workspace.parse().ok()?,
3719 workflow: workflow.parse().ok()?,
3720 run: run.parse().ok()?,
3721 occurrence: (*occurrence).to_owned(),
3722 sequence: Self::sequence(sequence)?,
3723 }),
3724 [
3725 "history",
3726 workspace,
3727 workflow,
3728 run,
3729 occurrence,
3730 "ledger",
3731 index,
3732 sequence,
3733 ] if index.len() == 2 && index.bytes().all(|byte| byte.is_ascii_digit()) => {
3734 let index = index.parse::<usize>().ok()?;
3735 if index >= MAX_INTERACTION_SETTLEMENTS.div_ceil(64) {
3736 return None;
3737 }
3738 Some(Self::Ledger {
3739 workspace: workspace.parse().ok()?,
3740 workflow: workflow.parse().ok()?,
3741 run: run.parse().ok()?,
3742 occurrence: (*occurrence).to_owned(),
3743 index,
3744 sequence: Self::sequence(sequence)?,
3745 })
3746 }
3747 [
3748 "history",
3749 workspace,
3750 workflow,
3751 run,
3752 occurrence,
3753 "action",
3754 ordinal,
3755 reverse,
3756 ] => Some(Self::ActionObservation {
3757 workspace: workspace.parse().ok()?,
3758 workflow: workflow.parse().ok()?,
3759 run: run.parse().ok()?,
3760 occurrence: (*occurrence).to_owned(),
3761 ordinal: Self::sequence(ordinal)?,
3762 sequence: u64::MAX - Self::sequence(reverse)?,
3763 }),
3764 ["active", workspace, workflow, run, occurrence] => Some(Self::ActiveRun {
3765 workspace: workspace.parse().ok()?,
3766 workflow: workflow.parse().ok()?,
3767 run: run.parse().ok()?,
3768 occurrence: (*occurrence).to_owned(),
3769 }),
3770 [
3771 "history",
3772 workspace,
3773 workflow,
3774 run,
3775 occurrence,
3776 "projection",
3777 rest @ ..,
3778 ] if rest.len() >= 2 => {
3779 let (sequence, projection) = rest.split_last()?;
3780 let projection = RunProjection::parse(projection)?;
3781 if matches!(projection, RunProjection::Action(_)) {
3782 return None;
3783 }
3784 RunId::validate_occurrence(occurrence).ok()?;
3785 Some(Self::ProjectionLink {
3786 key: RunKey::new(
3787 workspace.parse().ok()?,
3788 workflow.parse().ok()?,
3789 run.parse().ok()?,
3790 (*occurrence).into(),
3791 ),
3792 projection,
3793 sequence: Self::sequence(sequence)?,
3794 })
3795 }
3796 _ => None,
3797 })();
3798 let parsed =
3799 parsed.ok_or_else(|| AppError::Integration("durable history key is invalid".into()))?;
3800 match &parsed {
3801 Self::Checkpoint { occurrence, .. }
3802 | Self::Ledger { occurrence, .. }
3803 | Self::ActionObservation { occurrence, .. }
3804 | Self::ActiveRun { occurrence, .. } => RunId::validate_occurrence(occurrence)
3805 .map_err(|_| {
3806 AppError::Integration("durable history occurrence is invalid".into())
3807 })?,
3808 Self::ProjectionLink { .. } => {}
3809 _ => {
3810 return Err(AppError::Integration(
3811 "durable history key is invalid".into(),
3812 ));
3813 }
3814 }
3815 Ok(parsed)
3816 }
3817
3818 fn sequence(value: &str) -> Option<u64> {
3819 (value.len() == 20 && value.bytes().all(|byte| byte.is_ascii_digit()))
3820 .then(|| value.parse().ok())
3821 .flatten()
3822 }
3823
3824 fn index_record(&self) -> Result<StateRecord, AppError> {
3825 if !matches!(self, Self::ActiveRun { .. }) {
3826 return Err(AppError::Integration(
3827 "record is not a history index".into(),
3828 ));
3829 }
3830 self.record(&serde_json::json!({ "epoch": EPOCH }))
3831 }
3832
3833 async fn expect_missing<T: StateTransactionBackend>(
3834 &self,
3835 transaction: &mut T,
3836 ) -> Result<(), StorageFailure> {
3837 let key = self.state_key().map_err(StorageFailure::permanent)?;
3838 if transaction
3839 .load(key)
3840 .await
3841 .map_err(StorageFailure::from)?
3842 .is_some()
3843 {
3844 Err(StorageFailure::permanent(AppError::Conflict(
3845 "durable record already exists".to_owned(),
3846 )))
3847 } else {
3848 Ok(())
3849 }
3850 }
3851
3852 async fn expect_revision<T: StateTransactionBackend>(
3853 &self,
3854 transaction: &mut T,
3855 expected: u64,
3856 ) -> Result<(), StorageFailure> {
3857 let key = self.state_key().map_err(StorageFailure::permanent)?;
3858 let Some(snapshot) = transaction.load(key).await.map_err(StorageFailure::from)? else {
3859 return Err(StorageFailure::permanent(AppError::NotFound(
3860 "durable record does not exist".to_owned(),
3861 )));
3862 };
3863 if matches!(self, Self::App) {
3864 RecordCodec::preflight_epoch(&snapshot.record.value)
3865 .map_err(StorageFailure::permanent)?;
3866 Self::validate_value("app", &snapshot.record.value)
3867 .map_err(StorageFailure::permanent)?;
3868 }
3869 if snapshot.revision.get() != expected {
3870 return Err(StorageFailure::permanent(AppError::Conflict(
3871 "durable record revision fence is stale".to_owned(),
3872 )));
3873 }
3874 Ok(())
3875 }
3876
3877 async fn delete<T: StateTransactionBackend>(
3878 self,
3879 transaction: &mut T,
3880 ) -> Result<(), StorageFailure> {
3881 let key = self.state_key().map_err(StorageFailure::permanent)?;
3882 transaction.delete(key).await.map_err(StorageFailure::from)
3883 }
3884
3885 fn record<T>(&self, record: &T) -> Result<StateRecord, AppError>
3886 where
3887 T: Serialize,
3888 {
3889 let key = self.state_key()?;
3890 let value = RecordCodec::encode(record, 0)?;
3891 Self::validate_value(key.key(), &value)?;
3892 Ok(StateRecord::new(key, value))
3893 }
3894
3895 fn validate_value(key: &str, value: &Value) -> Result<(), AppError> {
3896 if key.starts_with("definition/") {
3897 let record: DefinitionRecord = RecordCodec::decode(value)?;
3898 record.validate()?;
3899 return if record.key().state_key()?.key() == key {
3900 Ok(())
3901 } else {
3902 Err(AppError::Integration(
3903 "durable workflow definition key is inconsistent".into(),
3904 ))
3905 };
3906 }
3907 if key.starts_with("projection/") {
3908 let parts = key.split('/').collect::<Vec<_>>();
3909 let ["projection", workspace, workflow, run, projection @ ..] = parts.as_slice() else {
3910 return Err(AppError::Integration(
3911 "saved projection key is invalid".into(),
3912 ));
3913 };
3914 let parsed = Self::ProjectionTail {
3915 workspace: workspace.parse()?,
3916 workflow: workflow.parse()?,
3917 run: run.parse()?,
3918 projection: RunProjection::parse(projection)
3919 .ok_or_else(|| AppError::Integration("saved projection is invalid".into()))?,
3920 };
3921 let index: RunIndex = RecordCodec::decode(value)?;
3922 index.validate()?;
3923 let valid = match (&parsed, &index) {
3924 (Self::ProjectionTail { projection, .. }, RunIndex::Tail { last, .. }) => {
3925 match projection {
3926 RunProjection::Sequence(sequence) => last.sequence == *sequence,
3927 RunProjection::Unsettled => last.sequence == 0,
3928 RunProjection::Checkpoint
3929 | RunProjection::Saved
3930 | RunProjection::Action(_) => true,
3931 }
3932 }
3933 _ => false,
3934 };
3935 if parsed.state_key()?.key() != key || !valid {
3936 return Err(AppError::Integration(
3937 "saved projection tail is invalid".into(),
3938 ));
3939 }
3940 return Ok(());
3941 }
3942 if key.starts_with("history/") || key.starts_with("active/") {
3943 let matches = match Self::history_key(key)? {
3944 Self::Checkpoint {
3945 workspace,
3946 workflow,
3947 run,
3948 occurrence,
3949 sequence,
3950 } => {
3951 value.get("workspace_id").and_then(Value::as_str) == Some(workspace.as_str())
3952 && value.get("previous").is_none()
3953 && value.get("workflow_id").and_then(Value::as_str)
3954 == Some(workflow.as_str())
3955 && value.get("id").and_then(Value::as_str) == Some(run.as_str())
3956 && value.get("occurrence").and_then(Value::as_str)
3957 == Some(occurrence.as_str())
3958 && value
3959 .get("checkpoint")
3960 .and_then(|checkpoint| checkpoint.get("sequence"))
3961 .and_then(Value::as_u64)
3962 == Some(sequence)
3963 }
3964 ledger @ Self::Ledger { .. } => SettlementPage::matches(&ledger, value),
3965 Self::ActionObservation {
3966 occurrence,
3967 sequence,
3968 ..
3969 } => {
3970 let index: RunIndex = RecordCodec::decode(value)?;
3971 index.validate()?;
3972 matches!(index, RunIndex::Link { previous, next, .. }
3973 if previous.iter().chain(&next).all(|neighbor|
3974 neighbor.occurrence != occurrence || neighbor.sequence != sequence))
3975 }
3976 Self::ProjectionLink {
3977 key,
3978 projection,
3979 sequence,
3980 } => {
3981 let index: RunIndex = RecordCodec::decode(value)?;
3982 index.validate()?;
3983 let scope = match projection {
3984 RunProjection::Sequence(expected) => sequence == expected,
3985 RunProjection::Unsettled => sequence == 0,
3986 RunProjection::Checkpoint | RunProjection::Saved => true,
3987 RunProjection::Action(_) => false,
3988 };
3989 scope
3990 && matches!(index, RunIndex::Link { previous, next, .. }
3991 if previous.iter().chain(&next).all(|neighbor|
3992 (neighbor.occurrence != key.occurrence() || neighbor.sequence != sequence)
3993 && match projection {
3994 RunProjection::Sequence(expected) => neighbor.sequence == expected,
3995 RunProjection::Unsettled => neighbor.sequence == 0,
3996 _ => true,
3997 }))
3998 }
3999 Self::ActiveRun { .. } => {
4000 value.as_object().is_some_and(|object| object.len() == 1)
4001 && value.get("epoch").and_then(Value::as_str) == Some(EPOCH)
4002 }
4003 _ => false,
4004 };
4005 return if matches {
4006 Ok(())
4007 } else {
4008 Err(AppError::Integration(
4009 "durable history value does not match its key".into(),
4010 ))
4011 };
4012 }
4013 let mut parts = key.split('/');
4014 let matches = match (
4015 parts.next(),
4016 parts.next(),
4017 parts.next(),
4018 parts.next(),
4019 parts.next(),
4020 parts.next(),
4021 ) {
4022 (Some("app"), None, None, None, None, None) => {
4023 let migration = value.get("migration").ok_or_else(|| {
4024 AppError::Integration("app migration state is missing".into())
4025 })?;
4026 matches!(
4027 RecordCodec::decode::<AppMigration>(migration)?,
4028 AppMigration::Ready
4029 )
4030 }
4031 (Some("workspace"), Some(id), None, None, None, None) => {
4032 value.get("id").and_then(Value::as_str) == Some(id)
4033 }
4034 (Some("run"), Some(workspace), Some(workflow), Some(run), Some(occurrence), None) => {
4035 value.get("workspace_id").and_then(Value::as_str) == Some(workspace)
4036 && value.get("workflow_id").and_then(Value::as_str) == Some(workflow)
4037 && value.get("id").and_then(Value::as_str) == Some(run)
4038 && value.get("creation").and_then(Value::as_str) == Some(occurrence)
4039 }
4040 (Some("receipt"), Some(id), None, None, None, None) => {
4041 value.get("id").and_then(Value::as_str) == Some(id)
4042 }
4043 _ => false,
4044 };
4045 if matches {
4046 Ok(())
4047 } else {
4048 Err(AppError::Integration(
4049 "durable record identity does not match its storage key".to_owned(),
4050 ))
4051 }
4052 }
4053
4054 fn state_key(&self) -> Result<StateKey, AppError> {
4055 let key = match self {
4056 Self::App => "app".to_owned(),
4057 Self::Definition { workflow, revision } => {
4058 let mut encoded = String::with_capacity(revision.len() * 2);
4059 for byte in revision.bytes() {
4060 encoded.push(char::from(b"0123456789abcdef"[usize::from(byte >> 4)]));
4061 encoded.push(char::from(b"0123456789abcdef"[usize::from(byte & 15)]));
4062 }
4063 format!("definition/{workflow}/{encoded}")
4064 }
4065 Self::Workspace(id) => format!("workspace/{id}"),
4066 Self::Run {
4067 workspace,
4068 workflow,
4069 run,
4070 occurrence,
4071 } => format!("run/{workspace}/{workflow}/{run}/{occurrence}"),
4072 Self::Checkpoint {
4073 workspace,
4074 workflow,
4075 run,
4076 occurrence,
4077 sequence,
4078 } => {
4079 format!(
4080 "history/{workspace}/{workflow}/{run}/{occurrence}/checkpoint/{sequence:020}"
4081 )
4082 }
4083 Self::Ledger {
4084 workspace,
4085 workflow,
4086 run,
4087 occurrence,
4088 index,
4089 sequence,
4090 } => {
4091 format!(
4092 "history/{workspace}/{workflow}/{run}/{occurrence}/ledger/{index:02}/{sequence:020}"
4093 )
4094 }
4095 Self::ActionObservation {
4096 workspace,
4097 workflow,
4098 run,
4099 occurrence,
4100 ordinal,
4101 sequence,
4102 } => {
4103 format!(
4104 "history/{workspace}/{workflow}/{run}/{occurrence}/action/{ordinal:020}/{:020}",
4105 u64::MAX - sequence
4106 )
4107 }
4108 Self::ActiveRun {
4109 workspace,
4110 workflow,
4111 run,
4112 occurrence,
4113 } => {
4114 format!("active/{workspace}/{workflow}/{run}/{occurrence}")
4115 }
4116 Self::ProjectionTail {
4117 workspace,
4118 workflow,
4119 run,
4120 projection,
4121 } => {
4122 format!(
4123 "projection/{workspace}/{workflow}/{run}/{}",
4124 projection.text()
4125 )
4126 }
4127 Self::ProjectionLink {
4128 key,
4129 projection,
4130 sequence,
4131 } => {
4132 format!(
4133 "history/{}/{}/{}/{}/projection/{}/{sequence:020}",
4134 key.workspace(),
4135 key.workflow(),
4136 key.run(),
4137 key.occurrence(),
4138 projection.text()
4139 )
4140 }
4141 Self::Receipt(id) => format!("receipt/{id}"),
4142 };
4143 StateKey::new(NAMESPACE, key).map_err(AppError::from)
4144 }
4145}
4146
4147impl std::io::Write for RecordCodec {
4148 fn write(&mut self, bytes: &[u8]) -> Result<usize, std::io::Error> {
4149 self.bytes = self
4150 .bytes
4151 .checked_add(bytes.len())
4152 .ok_or_else(|| std::io::Error::other("app durable record encoded length overflowed"))?;
4153 Ok(bytes.len())
4154 }
4155
4156 fn flush(&mut self) -> Result<(), std::io::Error> {
4157 Ok(())
4158 }
4159}
4160
4161impl RecordCodec {
4162 fn run(record: &RunRecord) -> Result<AppStorageRecord, AppError> {
4163 let reserved = record.checkpoint.prospective_settlement_bytes()?;
4164 let key = RecordKey::checkpoint(record);
4165 let state_key = key.state_key()?;
4166 let size = RecordCodec::logical_run_len(record)?;
4167 if size.saturating_add(reserved) > MAX_RECORD_BYTES {
4168 return Err(AppError::InvalidInput(
4169 "app durable record exceeds the private byte bound".into(),
4170 ));
4171 }
4172 let count = record.checkpoint.settlement_count();
4173 if count > MAX_INTERACTION_SETTLEMENTS {
4174 return Err(AppError::InvalidInput(
4175 "durable interaction settlement capacity is insufficient".into(),
4176 ));
4177 }
4178 let mut normalized = record.clone();
4179 if normalized.checkpoint.settlement_references().len() != count.div_ceil(64) {
4180 normalized
4181 .checkpoint
4182 .prepare_settlement_pages(record.previous.as_ref(), record.checkpoint.sequence)?;
4183 }
4184 normalized.previous = None;
4187 let references = normalized.checkpoint.settlement_references().to_vec();
4188 let mut pages = Vec::new();
4189 for (index, sequence) in references.iter().copied().enumerate() {
4190 if sequence == record.checkpoint.sequence {
4191 let entries = record.checkpoint.settlement_page(index)?;
4192 let page = SettlementPage {
4193 epoch: EPOCH.into(),
4194 workspace_id: record.workspace_id.clone(),
4195 workflow_id: record.workflow_id.clone(),
4196 id: record.id.clone(),
4197 occurrence: record.occurrence.clone(),
4198 index,
4199 sequence,
4200 entries,
4201 };
4202 let key = RecordKey::ledger(record, index, sequence);
4203 let value = serde_json::to_value(&page).map_err(|_| {
4204 AppError::Integration("app durable record encoding failed".into())
4205 })?;
4206 let state_key = key.state_key()?;
4207 RecordKey::validate_value(state_key.key(), &value)?;
4208 pages.push(StateRecord::new(state_key, value));
4209 }
4210 }
4211 let descriptor = SettlementPages {
4212 count,
4213 pages: references,
4214 };
4215 descriptor.validate(record.checkpoint.sequence)?;
4216 let mut value = serde_json::to_value(record)
4217 .map_err(|_| AppError::Integration("app durable record encoding failed".into()))?;
4218 value
4219 .get_mut("checkpoint")
4220 .and_then(Value::as_object_mut)
4221 .ok_or_else(|| AppError::Integration("checkpoint body encoding is invalid".into()))?
4222 .insert(
4223 "settlements".into(),
4224 serde_json::to_value(&descriptor).map_err(|_| {
4225 AppError::Integration("app durable record encoding failed".into())
4226 })?,
4227 );
4228 RecordKey::validate_value(state_key.key(), &value)?;
4229 Ok(AppStorageRecord::Checkpoint {
4230 key,
4231 value,
4232 record: Box::new(normalized),
4233 pages,
4234 })
4235 }
4236
4237 pub(crate) fn encoded_len<T: Serialize>(record: &T) -> Result<usize, AppError> {
4238 let mut encoded = Self { bytes: 0 };
4239 serde_json::to_writer(&mut encoded, record)
4240 .map_err(|_| AppError::Integration("app durable record encoding failed".into()))?;
4241 Ok(encoded.bytes)
4242 }
4243
4244 fn value_len(value: &Value) -> Result<usize, AppError> {
4245 let checked = |size: Option<usize>| {
4246 size.ok_or_else(|| {
4247 AppError::Integration("app durable record encoding failed".to_owned())
4248 })
4249 };
4250 Ok(match value {
4251 Value::Null => 4,
4252 Value::Bool(true) => 4,
4253 Value::Bool(false) => 5,
4254 Value::Number(number) => {
4255 if let Some(value) = number.as_u64() {
4256 value.checked_ilog10().unwrap_or(0) as usize + 1
4257 } else if let Some(value) = number.as_i64() {
4258 value.unsigned_abs().checked_ilog10().unwrap_or(0) as usize + 2
4259 } else {
4260 Self::encoded_len(number)?
4261 }
4262 }
4263 Value::String(text) => checked(Self::string_len(text))?,
4264 Value::Array(values) => {
4265 let mut size = 2usize;
4266 for (index, value) in values.iter().enumerate() {
4267 size = checked(size.checked_add(usize::from(index != 0)))?;
4268 size = checked(size.checked_add(Self::value_len(value)?))?;
4269 }
4270 size
4271 }
4272 Value::Object(values) => {
4273 let mut size = 2usize;
4274 for (index, (key, value)) in values.iter().enumerate() {
4275 size = checked(size.checked_add(if index == 0 { 1 } else { 2 }))?;
4276 size = checked(size.checked_add(checked(Self::string_len(key))?))?;
4277 size = checked(size.checked_add(Self::value_len(value)?))?;
4278 }
4279 size
4280 }
4281 })
4282 }
4283
4284 fn string_len(text: &str) -> Option<usize> {
4285 let mut size = text.len().checked_add(2)?;
4286 for byte in text.bytes() {
4287 let extra = match byte {
4288 b'"' | b'\\' | b'\x08' | b'\t' | b'\n' | b'\x0c' | b'\r' => 1,
4289 0..=0x1f => 5,
4290 _ => continue,
4291 };
4292 size = size.checked_add(extra)?;
4293 }
4294 Some(size)
4295 }
4296
4297 fn parse_run(value: &Value) -> Result<RunRecord, AppError> {
4298 RunRecord::deserialize(value)
4299 .map_err(|_| AppError::Integration("app durable record is corrupt".into()))
4300 }
4301
4302 fn parse_legacy_run(value: &Value) -> Result<RunRecord, AppError> {
4303 if value.get("epoch").and_then(Value::as_str) != Some(LEGACY_EPOCH) {
4304 return Err(AppError::Integration(
4305 "unsupported app record schema version".into(),
4306 ));
4307 }
4308 let size = Self::value_len(value)?;
4309 if size > MAX_RECORD_BYTES {
4310 return Err(AppError::Integration(
4311 "app durable record exceeds the private byte bound".into(),
4312 ));
4313 }
4314 let mut record = Self::parse_run(value)?;
4315 let entries = value
4316 .get("checkpoint")
4317 .and_then(|checkpoint| checkpoint.get("settlements"))
4318 .ok_or_else(|| AppError::Integration("app durable record is corrupt".into()))?;
4319 record.checkpoint.hydrate_legacy_settlements(entries)?;
4320 Self::validate_run(&record, size)?;
4321 record.checkpoint.validate_settlements()?;
4322 Ok(record)
4323 }
4324
4325 pub(crate) fn logical_run_len(record: &RunRecord) -> Result<usize, AppError> {
4326 let body = Self::encoded_len(record)?;
4330 let entries = record.checkpoint.settlement_bytes()?;
4331 body.checked_add(
4332 Self::string_len("settlements").ok_or_else(|| {
4333 AppError::Integration("app durable record encoding failed".into())
4334 })?,
4335 )
4336 .and_then(|size| size.checked_add(2))
4337 .and_then(|size| size.checked_add(entries))
4338 .ok_or_else(|| AppError::Integration("app durable record encoding failed".into()))
4339 }
4340
4341 pub(crate) fn same_run(left: &RunRecord, right: &RunRecord) -> Result<bool, AppError> {
4342 let encode = |record: &RunRecord| {
4343 serde_json::to_vec(record).map_err(|_| {
4344 AppError::Integration("workflow checkpoint evidence encoding failed".into())
4345 })
4346 };
4347 let left_body = encode(left)?;
4350 let left_entries = left.checkpoint.settlement_evidence()?;
4351 let right_body = encode(right)?;
4352 let right_entries = right.checkpoint.settlement_evidence()?;
4353 Ok(left_body == right_body && left_entries == right_entries)
4354 }
4355
4356 fn validate_run(record: &RunRecord, size: usize) -> Result<(), AppError> {
4357 RunId::validate_occurrence(&record.occurrence)
4358 .map_err(|_| AppError::Integration("durable run occurrence is invalid".into()))?;
4359 let reserve = record
4360 .checkpoint
4361 .prospective_settlement_bytes()
4362 .map_err(|_| {
4363 AppError::Integration(
4364 "durable interaction settlement capacity is insufficient".into(),
4365 )
4366 })?;
4367 if size.saturating_add(reserve) > MAX_RECORD_BYTES {
4368 return Err(AppError::Integration(
4369 "durable interaction settlement capacity is insufficient".into(),
4370 ));
4371 }
4372 Ok(())
4373 }
4374
4375 fn preflight_epoch(value: &Value) -> Result<(), AppError> {
4376 if value.get("epoch").and_then(Value::as_str) == Some(EPOCH) {
4377 Ok(())
4378 } else {
4379 Err(AppError::Integration(
4380 "unsupported app record schema version".to_owned(),
4381 ))
4382 }
4383 }
4384
4385 fn encode<T>(record: &T, reserved_bytes: usize) -> Result<Value, AppError>
4386 where
4387 T: Serialize,
4388 {
4389 let value = serde_json::to_value(record)
4390 .map_err(|_| AppError::Integration("app durable record encoding failed".to_owned()))?;
4391 let size = Self::value_len(&value)?;
4392 if size.saturating_add(reserved_bytes) > MAX_RECORD_BYTES {
4393 return Err(AppError::InvalidInput(
4394 "app durable record exceeds the private byte bound".to_owned(),
4395 ));
4396 }
4397 Ok(value)
4398 }
4399
4400 fn decode<T>(value: &Value) -> Result<T, AppError>
4401 where
4402 T: DeserializeOwned,
4403 {
4404 let size = Self::value_len(value)
4405 .map_err(|_| AppError::Integration("app durable record decoding failed".to_owned()))?;
4406 if size > MAX_RECORD_BYTES {
4407 return Err(AppError::Integration(
4408 "app durable record exceeds the private byte bound".to_owned(),
4409 ));
4410 }
4411 T::deserialize(value)
4412 .map_err(|_| AppError::Integration("app durable record is corrupt".to_owned()))
4413 }
4414}
4415
4416impl From<AppError> for StorageFailure {
4417 fn from(error: AppError) -> Self {
4418 Self::from_app(error)
4419 }
4420}
4421
4422impl From<StateStoreError> for StorageFailure {
4423 fn from(error: StateStoreError) -> Self {
4424 let recoverable = matches!(
4425 error,
4426 StateStoreError::TimedOut { .. }
4427 | StateStoreError::Unavailable { .. }
4428 | StateStoreError::BackendOperation { .. }
4429 );
4430 let error = AppError::from(error);
4431 if recoverable {
4432 Self::Recoverable(error)
4433 } else {
4434 Self::Permanent(error)
4435 }
4436 }
4437}
4438
4439impl From<StateStoreError> for AppError {
4440 fn from(error: StateStoreError) -> Self {
4441 match error {
4442 StateStoreError::InvalidConfig(_) | StateStoreError::InvalidRequest(_) => {
4443 Self::InvalidInput("app state-store input is invalid".to_owned())
4444 }
4445 StateStoreError::TransactionConflict { .. } => {
4446 Self::Conflict("app state transaction conflicted".to_owned())
4447 }
4448 StateStoreError::TimedOut { .. } => {
4449 Self::Timeout("app state-store operation timed out".to_owned())
4450 }
4451 StateStoreError::Unavailable { .. }
4452 | StateStoreError::BackendOperation { .. }
4453 | StateStoreError::Serialization(_) => {
4454 Self::Integration("app state-store operation failed".to_owned())
4455 }
4456 }
4457 }
4458}