1use std::collections::{BTreeMap, VecDeque};
16use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
17use std::sync::{Arc, Mutex};
18use std::time::{Duration, Instant};
19
20use serde_json::Value;
21
22use super::event::{EventPayload, ExecutionEvent};
23use super::model::{
24 Actor, Execution, ExecutionError, ExecutionId, ExecutionState, ProgressView, RepositoryRef,
25 StepState, StepView,
26};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct Limits {
35 pub max_executions: usize,
38 pub max_events: usize,
41 pub max_log_chars: usize,
43 pub max_subscriber_queue: usize,
46 pub max_running: usize,
48}
49
50impl Default for Limits {
51 fn default() -> Self {
52 Limits {
53 max_executions: 200,
54 max_events: 2_000,
55 max_log_chars: 2_000,
56 max_subscriber_queue: 512,
57 max_running: 4,
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum Delivery {
65 Event(Arc<ExecutionEvent>),
67 Lagged {
70 dropped: u64,
72 },
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct EventPage {
78 pub events: Vec<Arc<ExecutionEvent>>,
80 pub last_sequence: u64,
82 pub more: bool,
84 pub truncated: bool,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub enum Filter {
91 All,
93 Only(Vec<ExecutionId>),
95}
96
97impl Filter {
98 pub fn wants(&self, id: &ExecutionId) -> bool {
100 match self {
101 Filter::All => true,
102 Filter::Only(ids) => ids.contains(id),
103 }
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum CancelOutcome {
110 Requested,
112 AlreadyRequested,
114 AlreadyFinished(ExecutionState),
116 Unknown,
118}
119
120#[derive(Debug, Clone)]
141pub struct Seed<'a> {
142 pub capability: &'a str,
144 pub title: &'a str,
146 pub input: Value,
148 pub cancellable: bool,
150 pub actor: Actor,
152 pub repository: RepositoryRef,
154}
155
156struct Subscriber {
157 id: u64,
158 filter: Filter,
159 tx: std::sync::mpsc::SyncSender<Delivery>,
160 dropped: u64,
161}
162
163pub const REPORT_HEARTBEAT: Duration = Duration::from_secs(2);
166
167pub const REPORT_SILENCE: Duration = Duration::from_secs(30);
171
172#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct ReportOutcome {
195 pub applied: usize,
197 pub through: u64,
200 pub cancel_requested: bool,
203}
204
205#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
230pub enum AdoptError {
231 #[error("{0} is an execution this process runs itself, not one another process reports")]
233 Own(ExecutionId),
234}
235
236struct Reported {
238 through: u64,
240 heard: Instant,
242 pid: u32,
244}
245
246struct Record {
247 snapshot: Execution,
248 events: VecDeque<Arc<ExecutionEvent>>,
249 first_retained: u64,
251 cancel: Arc<AtomicBool>,
252 cancel_requested: bool,
253 started: Option<Instant>,
254 reported: Option<Reported>,
256}
257
258struct Inner {
259 records: BTreeMap<ExecutionId, Record>,
260 order: VecDeque<ExecutionId>,
261 subscribers: Vec<Subscriber>,
262}
263
264pub struct ExecutionStore {
266 limits: Limits,
267 inner: Mutex<Inner>,
268 next_subscriber: AtomicU64,
269}
270
271impl ExecutionStore {
272 pub fn new(limits: Limits) -> Self {
274 ExecutionStore {
275 limits,
276 inner: Mutex::new(Inner {
277 records: BTreeMap::new(),
278 order: VecDeque::new(),
279 subscribers: Vec::new(),
280 }),
281 next_subscriber: AtomicU64::new(1),
282 }
283 }
284
285 pub fn limits(&self) -> Limits {
287 self.limits
288 }
289
290 fn lock(&self) -> std::sync::MutexGuard<'_, Inner> {
291 self.inner.lock().unwrap_or_else(|e| e.into_inner())
296 }
297
298 #[allow(clippy::too_many_arguments)]
302 pub fn create(
303 &self,
304 id: ExecutionId,
305 capability: &str,
306 title: &str,
307 input: Value,
308 cancellable: bool,
309 actor: Actor,
310 repository: RepositoryRef,
311 ) -> Arc<AtomicBool> {
312 let created = self.insert(
313 id,
314 Seed {
315 capability,
316 title,
317 input,
318 cancellable,
319 actor,
320 repository,
321 },
322 None,
323 );
324 self.publish_created(created)
325 }
326
327 pub fn adopt(&self, id: ExecutionId, seed: Seed<'_>, pid: u32) -> Result<(), AdoptError> {
354 {
355 let inner = self.lock();
356 if let Some(record) = inner.records.get(&id) {
357 return match record.reported {
358 Some(_) => Ok(()),
359 None => Err(AdoptError::Own(id)),
360 };
361 }
362 }
363 let created = self.insert(
364 id,
365 seed,
366 Some(Reported {
367 through: 1,
370 heard: Instant::now(),
371 pid,
372 }),
373 );
374 self.publish_created(created);
375 Ok(())
376 }
377
378 pub fn report(
411 &self,
412 id: &ExecutionId,
413 events: impl IntoIterator<Item = (u64, EventPayload)>,
414 ) -> Option<ReportOutcome> {
415 let mut published = Vec::new();
416 let outcome = {
417 let mut inner = self.lock();
418 let mut through = inner.records.get(id)?.reported.as_ref()?.through;
419 let mut applied = 0usize;
420 for (sequence, payload) in events {
421 if sequence <= through {
422 continue;
423 }
424 if sequence != through + 1 {
425 break;
426 }
427 through = sequence;
428 let state = inner.records.get(id).map(|r| r.snapshot.state);
429 let said_already = match &payload {
430 EventPayload::Created { .. } => true,
432 EventPayload::Cancelling { .. } => state == Some(ExecutionState::Cancelling),
434 _ => false,
435 };
436 if said_already {
437 continue;
438 }
439 if let Some(event) = self.publish_locked(&mut inner, id, payload) {
440 applied += 1;
441 published.push(event);
442 }
443 }
444 inner.records.get_mut(id).map(|record| {
446 record.reported.iter_mut().for_each(|reported| {
447 reported.through = through;
448 reported.heard = Instant::now();
449 });
450 ReportOutcome {
451 applied,
452 through,
453 cancel_requested: record.cancel_requested,
454 }
455 })
456 };
457 for (event, deliveries) in published {
458 traced(&event, deliveries);
459 }
460 outcome
461 }
462
463 pub fn abandon_silent(&self, silence: Duration) -> Vec<ExecutionId> {
491 let mut published = Vec::new();
492 let abandoned = {
493 let mut inner = self.lock();
494 let silent: Vec<(ExecutionId, u32, u64)> = inner
495 .records
496 .iter()
497 .filter(|(_, r)| r.snapshot.state.is_active())
498 .filter_map(|(id, r)| {
499 let reported = r.reported.as_ref()?;
500 let quiet = reported.heard.elapsed();
501 (quiet > silence).then(|| (id.clone(), reported.pid, quiet.as_secs()))
502 })
503 .collect();
504 let mut abandoned = Vec::new();
505 for (id, pid, quiet) in silent {
506 let payload = EventPayload::Failed {
507 error: ExecutionError {
508 code: "unavailable".into(),
509 message: format!(
510 "process {pid}, which ran this execution and reported it here, has not reported for {quiet} s; what it did afterwards is unknown to this server"
511 ),
512 suggestion: Some(
513 "the terminal that ran it has the outcome; `majordomus run` reports to the server only while it is running".into(),
514 ),
515 correlation_id: id.to_string(),
516 },
517 };
518 let event = self.publish_locked(&mut inner, &id, payload);
521 abandoned.extend(event.is_some().then_some(id));
522 published.extend(event);
523 }
524 abandoned
525 };
526 for (event, deliveries) in published {
527 traced(&event, deliveries);
528 }
529 abandoned
530 }
531
532 pub fn is_reported(&self, id: &ExecutionId) -> bool {
552 self.lock()
553 .records
554 .get(id)
555 .is_some_and(|r| r.reported.is_some())
556 }
557
558 fn insert(
561 &self,
562 id: ExecutionId,
563 seed: Seed<'_>,
564 reported: Option<Reported>,
565 ) -> (ExecutionId, Arc<AtomicBool>, EventPayload) {
566 let Seed {
567 capability,
568 title,
569 input,
570 cancellable,
571 actor,
572 repository,
573 } = seed;
574 let cancel = Arc::new(AtomicBool::new(false));
575 let now = crate::peers::rfc3339(std::time::SystemTime::now());
576 let snapshot = Execution {
577 id: id.clone(),
578 capability: capability.to_string(),
579 title: title.to_string(),
580 state: ExecutionState::Queued,
581 created_at: now,
582 started_at: None,
583 finished_at: None,
584 duration_ms: None,
585 input: input.clone(),
586 output: None,
587 error: None,
588 progress: None,
589 steps: Vec::new(),
590 diagnostics: Vec::new(),
591 last_sequence: 0,
592 event_count: 0,
593 events_truncated: false,
594 cancellable,
595 actor,
596 repository,
597 correlation_id: id.to_string(),
598 };
599 {
600 let mut inner = self.lock();
601 inner.records.insert(
602 id.clone(),
603 Record {
604 snapshot,
605 events: VecDeque::new(),
606 first_retained: 1,
607 cancel: Arc::clone(&cancel),
608 cancel_requested: false,
609 started: None,
610 reported,
611 },
612 );
613 inner.order.push_back(id.clone());
614 self.evict(&mut inner);
615 }
616 let created = EventPayload::Created {
617 capability: capability.to_string(),
618 title: title.to_string(),
619 input,
620 };
621 (id, cancel, created)
622 }
623
624 fn publish_created(
625 &self,
626 (id, cancel, created): (ExecutionId, Arc<AtomicBool>, EventPayload),
627 ) -> Arc<AtomicBool> {
628 self.publish(&id, created);
629 cancel
630 }
631
632 fn evict(&self, inner: &mut Inner) {
636 while inner.records.len() > self.limits.max_executions {
637 let Some(position) = inner.order.iter().position(|id| {
638 inner
639 .records
640 .get(id)
641 .is_some_and(|r| r.snapshot.state.is_final())
642 }) else {
643 break;
644 };
645 if let Some(id) = inner.order.remove(position) {
646 inner.records.remove(&id);
647 }
648 }
649 }
650
651 pub fn publish(&self, id: &ExecutionId, payload: EventPayload) -> Option<u64> {
658 let (event, deliveries) = {
659 let mut inner = self.lock();
660 self.publish_locked(&mut inner, id, payload)?
661 };
662 traced(&event, deliveries);
663 Some(event.sequence)
664 }
665
666 fn publish_locked(
669 &self,
670 inner: &mut Inner,
671 id: &ExecutionId,
672 payload: EventPayload,
673 ) -> Option<(Arc<ExecutionEvent>, usize)> {
674 let limits = self.limits;
675 let record = inner.records.get_mut(id)?;
676 if let Some(next) = payload.implies_state() {
677 let current = record.snapshot.state;
678 if current == next {
679 } else if !current.may_move_to(next) {
681 tracing::debug!(
682 execution_id = %id,
683 from = current.as_str(),
684 to = next.as_str(),
685 "an execution event was refused: the state machine does not allow the move"
686 );
687 return None;
688 }
689 }
690 let sequence = record.snapshot.last_sequence + 1;
691 apply(record, &payload, sequence, limits);
692 let event = Arc::new(ExecutionEvent::new(id.clone(), sequence, payload));
693 record.events.push_back(Arc::clone(&event));
694 while record.events.len() > limits.max_events {
695 record.events.pop_front();
696 record.first_retained += 1;
697 record.snapshot.events_truncated = true;
698 }
699 let deliveries = inner.fan_out(id, &event);
700 Some((event, deliveries))
701 }
702
703 pub fn get(&self, id: &ExecutionId) -> Option<Execution> {
705 self.lock().records.get(id).map(|r| r.snapshot.clone())
706 }
707
708 pub fn list(
710 &self,
711 state: Option<ExecutionState>,
712 capability: Option<&str>,
713 limit: usize,
714 ) -> Vec<Execution> {
715 let inner = self.lock();
716 inner
717 .order
718 .iter()
719 .rev()
720 .filter_map(|id| inner.records.get(id))
721 .map(|r| &r.snapshot)
722 .filter(|s| state.is_none_or(|want| s.state == want))
723 .filter(|s| capability.is_none_or(|want| s.capability == want))
724 .take(limit)
725 .cloned()
726 .collect()
727 }
728
729 pub fn len(&self) -> usize {
731 self.lock().records.len()
732 }
733
734 pub fn is_empty(&self) -> bool {
736 self.len() == 0
737 }
738
739 pub fn count(&self, state: ExecutionState) -> usize {
741 self.lock()
742 .records
743 .values()
744 .filter(|r| r.snapshot.state == state)
745 .count()
746 }
747
748 pub fn active(&self) -> usize {
750 self.lock()
751 .records
752 .values()
753 .filter(|r| r.snapshot.state.is_active())
754 .count()
755 }
756
757 pub fn events(&self, id: &ExecutionId, after: u64, limit: usize) -> Option<EventPage> {
759 let inner = self.lock();
760 let record = inner.records.get(id)?;
761 let all: Vec<Arc<ExecutionEvent>> = record
762 .events
763 .iter()
764 .filter(|e| e.sequence > after)
765 .cloned()
766 .collect();
767 let more = all.len() > limit;
768 let events: Vec<Arc<ExecutionEvent>> = all.into_iter().take(limit).collect();
769 Some(EventPage {
770 last_sequence: events
771 .last()
772 .map(|e| e.sequence)
773 .unwrap_or_else(|| after.max(record.snapshot.last_sequence)),
774 more,
775 truncated: after + 1 < record.first_retained,
777 events,
778 })
779 }
780
781 pub fn token(&self, id: &ExecutionId) -> Option<Arc<AtomicBool>> {
783 self.lock().records.get(id).map(|r| Arc::clone(&r.cancel))
784 }
785
786 pub fn request_cancel(&self, id: &ExecutionId, by: &str) -> CancelOutcome {
800 let published = {
801 let mut inner = self.lock();
802 let Some(record) = inner.records.get_mut(id) else {
803 return CancelOutcome::Unknown;
804 };
805 let state = record.snapshot.state;
806 if state.is_final() {
807 return CancelOutcome::AlreadyFinished(state);
808 }
809 if record.cancel_requested {
810 return CancelOutcome::AlreadyRequested;
811 }
812 record.cancel_requested = true;
813 if record.reported.is_some() && state == ExecutionState::Queued {
817 return CancelOutcome::Requested;
818 }
819 let token = Arc::clone(&record.cancel);
820 let payload = if state == ExecutionState::Queued {
822 EventPayload::Cancelled
823 } else {
824 EventPayload::Cancelling { by: by.to_string() }
825 };
826 let published = self.publish_locked(&mut inner, id, payload);
827 token.store(true, Ordering::SeqCst);
828 published
829 };
830 if let Some((event, deliveries)) = published {
831 traced(&event, deliveries);
832 }
833 CancelOutcome::Requested
834 }
835
836 pub fn subscribe(&self, filter: Filter) -> (u64, std::sync::mpsc::Receiver<Delivery>) {
839 let (tx, rx) = std::sync::mpsc::sync_channel(self.limits.max_subscriber_queue);
840 let id = self.next_subscriber.fetch_add(1, Ordering::Relaxed);
841 self.lock().subscribers.push(Subscriber {
842 id,
843 filter,
844 tx,
845 dropped: 0,
846 });
847 (id, rx)
848 }
849
850 pub fn resubscribe(&self, subscriber: u64, filter: Filter) -> bool {
852 let mut inner = self.lock();
853 match inner.subscribers.iter_mut().find(|s| s.id == subscriber) {
854 Some(s) => {
855 s.filter = filter;
856 true
857 }
858 None => false,
859 }
860 }
861
862 pub fn unsubscribe(&self, subscriber: u64) {
864 self.lock().subscribers.retain(|s| s.id != subscriber);
865 }
866
867 pub fn subscribers(&self) -> usize {
869 self.lock().subscribers.len()
870 }
871}
872
873impl Inner {
874 fn fan_out(&mut self, id: &ExecutionId, event: &Arc<ExecutionEvent>) -> usize {
877 let mut delivered = 0usize;
878 let mut gone = Vec::new();
879 for subscriber in self.subscribers.iter_mut() {
880 if !subscriber.filter.wants(id) {
881 continue;
882 }
883 if subscriber.dropped > 0 {
884 match subscriber.tx.try_send(Delivery::Lagged {
886 dropped: subscriber.dropped,
887 }) {
888 Ok(()) => subscriber.dropped = 0,
889 Err(std::sync::mpsc::TrySendError::Full(_)) => {
890 subscriber.dropped += 1;
891 continue;
892 }
893 Err(std::sync::mpsc::TrySendError::Disconnected(_)) => {
894 gone.push(subscriber.id);
895 continue;
896 }
897 }
898 }
899 match subscriber.tx.try_send(Delivery::Event(Arc::clone(event))) {
900 Ok(()) => delivered += 1,
901 Err(std::sync::mpsc::TrySendError::Full(_)) => subscriber.dropped += 1,
904 Err(std::sync::mpsc::TrySendError::Disconnected(_)) => gone.push(subscriber.id),
905 }
906 }
907 if !gone.is_empty() {
908 self.subscribers.retain(|s| !gone.contains(&s.id));
909 }
910 delivered
911 }
912}
913
914fn traced(event: &ExecutionEvent, deliveries: usize) {
916 tracing::debug!(
917 execution_id = %event.execution_id,
918 sequence = event.sequence,
919 event = event.type_name(),
920 subscribers = deliveries,
921 "execution event"
922 );
923}
924
925fn apply(record: &mut Record, payload: &EventPayload, sequence: u64, limits: Limits) {
927 let now = crate::peers::rfc3339(std::time::SystemTime::now());
928 let s = &mut record.snapshot;
929 s.last_sequence = sequence;
930 s.event_count += 1;
931 if let Some(next) = payload.implies_state() {
932 s.state = next;
933 }
934 match payload {
935 EventPayload::Started => {
936 s.started_at = Some(now.clone());
937 record.started = Some(Instant::now());
938 }
939 EventPayload::Progress(p) => {
940 s.progress = Some(ProgressView {
941 current: p.current,
942 total: p.total,
943 message: p.message.clone(),
944 });
945 }
946 EventPayload::StepStarted { name, title } => {
947 s.steps.push(StepView {
948 name: name.clone(),
949 title: title.clone(),
950 state: StepState::Running,
951 started_at: now.clone(),
952 finished_at: None,
953 detail: None,
954 });
955 }
956 EventPayload::StepCompleted { name, ok, detail } => {
957 let state = if *ok {
958 StepState::Completed
959 } else {
960 StepState::Failed
961 };
962 match s.steps.iter_mut().rev().find(|s| &s.name == name) {
963 Some(step) => {
964 step.state = state;
965 step.finished_at = Some(now.clone());
966 step.detail.clone_from(detail);
967 }
968 None => s.steps.push(StepView {
971 name: name.clone(),
972 title: name.clone(),
973 state,
974 started_at: now.clone(),
975 finished_at: Some(now.clone()),
976 detail: detail.clone(),
977 }),
978 }
979 }
980 EventPayload::Diagnostic(d) => {
981 if s.diagnostics.len() < limits.max_events.min(200) {
984 s.diagnostics.push(d.clone());
985 }
986 }
987 EventPayload::Completed { output } => {
988 s.output = Some(output.clone());
989 s.error = None;
990 }
991 EventPayload::Failed { error } => {
992 s.error = Some(error.clone());
993 s.output = None;
994 }
995 EventPayload::Cancelled if s.error.is_none() => {
998 s.error = Some(ExecutionError {
999 code: "cancelled".into(),
1000 message: "the execution was cancelled".into(),
1001 suggestion: None,
1002 correlation_id: s.correlation_id.clone(),
1003 });
1004 }
1005 _ => {}
1006 }
1007 if s.state.is_final() && s.finished_at.is_none() {
1008 s.finished_at = Some(now);
1009 s.duration_ms = record.started.map(|t| t.elapsed().as_millis() as u64);
1010 }
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015 use super::*;
1016 use crate::execution::model::{ActorKind, LogStream};
1017
1018 fn store() -> ExecutionStore {
1019 ExecutionStore::new(Limits::default())
1020 }
1021
1022 fn repository() -> RepositoryRef {
1023 RepositoryRef {
1024 name: "r".into(),
1025 id: "id".into(),
1026 branch: None,
1027 }
1028 }
1029
1030 fn start(store: &ExecutionStore) -> ExecutionId {
1031 let id = ExecutionId::fresh();
1032 store.create(
1033 id.clone(),
1034 "demo.x",
1035 "Demo",
1036 serde_json::json!({}),
1037 true,
1038 Actor::of(ActorKind::Internal),
1039 repository(),
1040 );
1041 id
1042 }
1043
1044 #[test]
1045 fn a_snapshot_and_the_stream_agree_at_every_step() {
1046 let store = store();
1047 let (_, rx) = store.subscribe(Filter::All);
1048 let id = start(&store);
1049 store.publish(&id, EventPayload::Started);
1050 store.publish(
1051 &id,
1052 EventPayload::Progress(ProgressView {
1053 current: 1,
1054 total: Some(2),
1055 message: Some("half".into()),
1056 }),
1057 );
1058 store.publish(
1059 &id,
1060 EventPayload::Completed {
1061 output: serde_json::json!({ "ok": true }),
1062 },
1063 );
1064 let snapshot = store.get(&id).expect("a snapshot");
1065 assert_eq!(snapshot.state, ExecutionState::Succeeded);
1066 assert_eq!(snapshot.output, Some(serde_json::json!({ "ok": true })));
1067 assert_eq!(snapshot.percent(), Some(50));
1068 assert!(snapshot.finished_at.is_some());
1069 assert_eq!(snapshot.last_sequence, 4);
1070 assert_eq!(snapshot.event_count, 4);
1071
1072 let seen: Vec<Arc<ExecutionEvent>> = rx
1073 .try_iter()
1074 .map(|d| match d {
1075 Delivery::Event(e) => e,
1076 Delivery::Lagged { .. } => panic!("a reader that kept up was told it lagged"),
1077 })
1078 .collect();
1079 assert_eq!(seen.len(), 4);
1080 assert_eq!(
1081 seen.iter().map(|e| e.sequence).collect::<Vec<_>>(),
1082 [1, 2, 3, 4],
1083 "sequences are dense and ordered"
1084 );
1085 assert_eq!(seen.last().unwrap().type_name(), "execution.completed");
1086 }
1087
1088 #[test]
1089 fn a_refused_transition_changes_nothing() {
1090 let store = store();
1091 let id = start(&store);
1092 store.publish(&id, EventPayload::Started);
1093 store.publish(
1094 &id,
1095 EventPayload::Completed {
1096 output: Value::Null,
1097 },
1098 );
1099 let before = store.get(&id).unwrap();
1100 assert_eq!(
1101 store.publish(&id, EventPayload::Started),
1102 None,
1103 "a finished execution does not start again"
1104 );
1105 assert_eq!(
1106 store.publish(
1107 &id,
1108 EventPayload::Failed {
1109 error: ExecutionError {
1110 code: "internal".into(),
1111 message: "no".into(),
1112 suggestion: None,
1113 correlation_id: id.to_string()
1114 }
1115 }
1116 ),
1117 None
1118 );
1119 assert_eq!(store.get(&id).unwrap(), before);
1120 assert_eq!(
1121 store.publish(&ExecutionId::fresh(), EventPayload::Started),
1122 None
1123 );
1124 }
1125
1126 #[test]
1127 fn history_replays_from_a_cursor_and_says_when_it_lost_the_start() {
1128 let limits = Limits {
1129 max_events: 4,
1130 ..Limits::default()
1131 };
1132 let store = ExecutionStore::new(limits);
1133 let id = start(&store);
1134 store.publish(&id, EventPayload::Started);
1135 for n in 0..6 {
1136 store.publish(
1137 &id,
1138 EventPayload::Log {
1139 stream: LogStream::Handler,
1140 message: format!("line {n}"),
1141 },
1142 );
1143 }
1144 let page = store.events(&id, 0, 100).expect("a page");
1145 assert_eq!(page.events.len(), 4, "the bound holds");
1146 assert!(page.truncated, "and says the start is gone");
1147 let snapshot = store.get(&id).unwrap();
1148 assert!(snapshot.events_truncated);
1149 assert_eq!(snapshot.event_count, 8, "counted, not retained");
1150
1151 let last = page.last_sequence;
1152 let after = store.events(&id, last, 100).unwrap();
1153 assert!(after.events.is_empty());
1154 assert!(!after.more);
1155 assert_eq!(after.last_sequence, last);
1156
1157 let paged = store.events(&id, 4, 2).unwrap();
1158 assert_eq!(paged.events.len(), 2);
1159 assert!(paged.more);
1160 }
1161
1162 #[test]
1163 fn a_slow_subscriber_is_told_to_resynchronise_and_never_stalls_the_execution() {
1164 let store = ExecutionStore::new(Limits {
1165 max_subscriber_queue: 2,
1166 ..Limits::default()
1167 });
1168 let (_, rx) = store.subscribe(Filter::All);
1169 let id = start(&store); for n in 0..20 {
1171 store.publish(
1172 &id,
1173 EventPayload::Log {
1174 stream: LogStream::Handler,
1175 message: n.to_string(),
1176 },
1177 );
1178 }
1179 assert_eq!(store.get(&id).unwrap().event_count, 21);
1181 let deliveries: Vec<Delivery> = rx.try_iter().collect();
1182 assert!(deliveries.len() <= 3, "the queue stayed bounded");
1183 store.publish(&id, EventPayload::Started);
1185 let mut told = false;
1186 for d in rx.try_iter() {
1187 if let Delivery::Lagged { dropped } = d {
1188 assert!(dropped > 0);
1189 told = true;
1190 }
1191 }
1192 assert!(told, "a lagging subscriber is told, not silently starved");
1193 }
1194
1195 #[test]
1196 fn a_dropped_receiver_is_forgotten_on_the_next_event() {
1197 let store = store();
1198 let (_, rx) = store.subscribe(Filter::All);
1199 assert_eq!(store.subscribers(), 1);
1200 drop(rx);
1201 let id = start(&store);
1202 store.publish(&id, EventPayload::Started);
1203 assert_eq!(store.subscribers(), 0);
1204 }
1205
1206 #[test]
1207 fn a_filter_narrows_and_can_be_changed_without_reconnecting() {
1208 let store = store();
1209 let a = start(&store);
1210 let (sub, rx) = store.subscribe(Filter::Only(vec![a.clone()]));
1211 let b = start(&store);
1212 store.publish(&b, EventPayload::Started);
1213 assert!(rx.try_iter().next().is_none(), "b was not wanted");
1214 assert!(store.resubscribe(sub, Filter::Only(vec![b.clone()])));
1215 store.publish(
1216 &b,
1217 EventPayload::Progress(ProgressView {
1218 current: 1,
1219 total: None,
1220 message: None,
1221 }),
1222 );
1223 assert!(matches!(rx.try_recv(), Ok(Delivery::Event(_))));
1224 store.unsubscribe(sub);
1225 assert_eq!(store.subscribers(), 0);
1226 assert!(!store.resubscribe(sub, Filter::All));
1227 }
1228
1229 #[test]
1230 fn cancelling_a_queued_execution_finishes_it_and_cancelling_twice_says_so() {
1231 let store = store();
1232 let id = start(&store);
1233 assert_eq!(store.request_cancel(&id, "test"), CancelOutcome::Requested);
1234 assert_eq!(store.get(&id).unwrap().state, ExecutionState::Cancelled);
1235 assert_eq!(
1236 store.request_cancel(&id, "test"),
1237 CancelOutcome::AlreadyFinished(ExecutionState::Cancelled)
1238 );
1239 assert_eq!(
1240 store.request_cancel(&ExecutionId::fresh(), "test"),
1241 CancelOutcome::Unknown
1242 );
1243
1244 let running = start(&store);
1245 store.publish(&running, EventPayload::Started);
1246 assert_eq!(
1247 store.request_cancel(&running, "test"),
1248 CancelOutcome::Requested
1249 );
1250 assert!(store.token(&running).unwrap().load(Ordering::SeqCst));
1251 assert_eq!(
1252 store.get(&running).unwrap().state,
1253 ExecutionState::Cancelling
1254 );
1255 assert_eq!(
1256 store.request_cancel(&running, "test"),
1257 CancelOutcome::AlreadyRequested
1258 );
1259 store.publish(&running, EventPayload::Cancelled);
1260 let snapshot = store.get(&running).unwrap();
1261 assert_eq!(snapshot.state, ExecutionState::Cancelled);
1262 assert_eq!(
1263 snapshot.error.map(|e| e.code),
1264 Some("cancelled".to_string())
1265 );
1266 }
1267
1268 #[test]
1269 fn a_handler_that_stops_the_instant_it_sees_the_token_still_ends_cancelled() {
1270 for round in 0..300 {
1278 let store = Arc::new(store());
1279 let id = start(&store);
1280 store.publish(&id, EventPayload::Started);
1281 let token = store.token(&id).unwrap();
1282 let ready = Arc::new(AtomicBool::new(false));
1283 let worker = {
1284 let (store, id, ready) = (Arc::clone(&store), id.clone(), Arc::clone(&ready));
1285 std::thread::spawn(move || {
1286 ready.store(true, Ordering::SeqCst);
1287 let deadline = Instant::now() + std::time::Duration::from_secs(10);
1289 while !token.load(Ordering::SeqCst) {
1290 if Instant::now() >= deadline {
1291 return None;
1292 }
1293 std::hint::spin_loop();
1294 }
1295 store.publish(&id, EventPayload::Cancelled)
1296 })
1297 };
1298 while !ready.load(Ordering::SeqCst) {
1299 std::hint::spin_loop();
1300 }
1301 assert_eq!(store.request_cancel(&id, "test"), CancelOutcome::Requested);
1302 let published = worker.join().unwrap();
1303 let state = store.get(&id).unwrap().state;
1304 assert!(
1305 published.is_some() && state == ExecutionState::Cancelled,
1306 "round {round}: the handler stopped and the execution says {state:?}"
1307 );
1308 }
1309
1310 let store = store();
1313 let id = start(&store);
1314 assert_eq!(store.request_cancel(&id, "test"), CancelOutcome::Requested);
1315 assert_eq!(store.publish(&id, EventPayload::Started), None);
1316 assert_eq!(store.get(&id).unwrap().state, ExecutionState::Cancelled);
1317 }
1318
1319 #[test]
1320 fn the_oldest_finished_execution_is_forgotten_and_a_running_one_never_is() {
1321 let store = ExecutionStore::new(Limits {
1322 max_executions: 2,
1323 ..Limits::default()
1324 });
1325 let running = start(&store);
1326 store.publish(&running, EventPayload::Started);
1327 let mut finished = Vec::new();
1328 for _ in 0..4 {
1329 let id = start(&store);
1330 store.publish(&id, EventPayload::Started);
1331 store.publish(
1332 &id,
1333 EventPayload::Completed {
1334 output: Value::Null,
1335 },
1336 );
1337 finished.push(id);
1338 }
1339 assert!(
1340 store.get(&running).is_some(),
1341 "work in flight is not evicted"
1342 );
1343 assert!(
1344 store.get(finished.last().unwrap()).is_some(),
1345 "the newest is kept"
1346 );
1347 assert!(
1348 store.get(&finished[0]).is_none(),
1349 "the oldest finished is gone"
1350 );
1351 assert_eq!(store.active(), 1);
1352 assert_eq!(
1353 store.count(ExecutionState::Succeeded),
1354 store.list(Some(ExecutionState::Succeeded), None, 100).len()
1355 );
1356 }
1357
1358 fn seed() -> Seed<'static> {
1359 Seed {
1360 capability: "demo.x",
1361 title: "Demo",
1362 input: serde_json::json!({}),
1363 cancellable: true,
1364 actor: Actor::reported(4242),
1365 repository: repository(),
1366 }
1367 }
1368
1369 fn log(n: u32) -> EventPayload {
1370 EventPayload::Log {
1371 stream: LogStream::Handler,
1372 message: format!("line {n}"),
1373 }
1374 }
1375
1376 #[test]
1377 fn a_reported_execution_is_applied_once_in_order_and_resumes_after_a_gap() {
1378 let store = store();
1379 let (_, rx) = store.subscribe(Filter::All);
1380 let id = ExecutionId::fresh();
1381 store.adopt(id.clone(), seed(), 4242).expect("adopted");
1382 assert!(store.is_reported(&id));
1383 store
1385 .adopt(id.clone(), seed(), 4242)
1386 .expect("adopted twice");
1387 assert_eq!(store.get(&id).unwrap().event_count, 1, "one created event");
1388
1389 let first = vec![
1391 (
1392 1,
1393 EventPayload::Created {
1394 capability: "demo.x".into(),
1395 title: "Demo".into(),
1396 input: serde_json::json!({}),
1397 },
1398 ),
1399 (2, EventPayload::Queued { ahead: 0 }),
1400 (3, EventPayload::Started),
1401 (4, log(1)),
1402 ];
1403 let outcome = store
1404 .report(&id, first.clone())
1405 .expect("a reported execution");
1406 assert_eq!((outcome.applied, outcome.through), (3, 4));
1407 assert!(!outcome.cancel_requested);
1408 assert_eq!(store.get(&id).unwrap().state, ExecutionState::Running);
1409
1410 let again = store.report(&id, first).unwrap();
1412 assert_eq!((again.applied, again.through), (0, 4));
1413
1414 let gapped = store.report(&id, vec![(6, log(3))]).unwrap();
1416 assert_eq!((gapped.applied, gapped.through), (0, 4));
1417 let resumed = store
1418 .report(
1419 &id,
1420 vec![
1421 (5, log(2)),
1422 (6, log(3)),
1423 (
1424 7,
1425 EventPayload::Completed {
1426 output: serde_json::json!({ "ok": true }),
1427 },
1428 ),
1429 ],
1430 )
1431 .unwrap();
1432 assert_eq!((resumed.applied, resumed.through), (3, 7));
1433 let snapshot = store.get(&id).unwrap();
1434 assert_eq!(snapshot.state, ExecutionState::Succeeded);
1435 assert_eq!(snapshot.output, Some(serde_json::json!({ "ok": true })));
1436 assert_eq!(snapshot.actor.pid, Some(4242));
1437 let types: Vec<&str> = rx
1439 .try_iter()
1440 .filter_map(|d| match d {
1441 Delivery::Event(e) => Some(e.type_name()),
1442 Delivery::Lagged { .. } => None,
1443 })
1444 .collect();
1445 assert_eq!(
1446 types,
1447 [
1448 "execution.created",
1449 "execution.queued",
1450 "execution.started",
1451 "execution.log",
1452 "execution.log",
1453 "execution.log",
1454 "execution.completed"
1455 ]
1456 );
1457 }
1458
1459 #[test]
1462 fn a_report_about_nothing_held_answers_nothing_and_a_repeated_created_is_passed_over() {
1463 let store = store();
1464 assert_eq!(
1465 store.report(&ExecutionId::fresh(), vec![(2, EventPayload::Started)]),
1466 None
1467 );
1468 let id = ExecutionId::fresh();
1469 store.adopt(id.clone(), seed(), 4242).expect("adopted");
1470 let outcome = store
1471 .report(
1472 &id,
1473 vec![(
1474 2,
1475 EventPayload::Created {
1476 capability: "demo.x".into(),
1477 title: "Demo".into(),
1478 input: serde_json::json!({}),
1479 },
1480 )],
1481 )
1482 .expect("a reported execution");
1483 assert_eq!((outcome.applied, outcome.through), (0, 2));
1484 assert_eq!(store.get(&id).unwrap().event_count, 1, "one created event");
1485 }
1486
1487 #[test]
1488 fn a_report_cannot_write_into_an_execution_this_process_runs() {
1489 let store = store();
1490 let own = start(&store);
1491 assert_eq!(
1492 store.adopt(own.clone(), seed(), 1),
1493 Err(AdoptError::Own(own.clone()))
1494 );
1495 assert!(!store.is_reported(&own));
1496 assert_eq!(
1497 store.report(&own, vec![(2, EventPayload::Started)]),
1498 None,
1499 "a report names a reported execution or nothing"
1500 );
1501 assert_eq!(store.get(&own).unwrap().state, ExecutionState::Queued);
1502 }
1503
1504 #[test]
1505 fn a_stop_asked_here_is_handed_to_the_process_that_runs_it() {
1506 let store = store();
1507 let queued = ExecutionId::fresh();
1508 store.adopt(queued.clone(), seed(), 7).unwrap();
1509 store
1510 .report(&queued, vec![(2, EventPayload::Queued { ahead: 1 })])
1511 .unwrap();
1512 assert_eq!(
1514 store.request_cancel(&queued, "client"),
1515 CancelOutcome::Requested
1516 );
1517 assert_eq!(store.get(&queued).unwrap().state, ExecutionState::Queued);
1518 let told = store.report(&queued, Vec::new()).unwrap();
1519 assert!(told.cancel_requested, "the reporter is told to stop it");
1520 store
1521 .report(&queued, vec![(3, EventPayload::Cancelled)])
1522 .unwrap();
1523 assert_eq!(store.get(&queued).unwrap().state, ExecutionState::Cancelled);
1524
1525 let running = ExecutionId::fresh();
1526 store.adopt(running.clone(), seed(), 7).unwrap();
1527 store
1528 .report(
1529 &running,
1530 vec![
1531 (2, EventPayload::Queued { ahead: 0 }),
1532 (3, EventPayload::Started),
1533 ],
1534 )
1535 .unwrap();
1536 assert_eq!(
1537 store.request_cancel(&running, "client"),
1538 CancelOutcome::Requested
1539 );
1540 assert_eq!(
1541 store.get(&running).unwrap().state,
1542 ExecutionState::Cancelling
1543 );
1544 let before = store.get(&running).unwrap().event_count;
1545 let outcome = store
1547 .report(
1548 &running,
1549 vec![
1550 (
1551 4,
1552 EventPayload::Cancelling {
1553 by: "server".into(),
1554 },
1555 ),
1556 (5, EventPayload::Cancelled),
1557 ],
1558 )
1559 .unwrap();
1560 assert_eq!((outcome.applied, outcome.through), (1, 5));
1561 let snapshot = store.get(&running).unwrap();
1562 assert_eq!(snapshot.state, ExecutionState::Cancelled);
1563 assert_eq!(snapshot.event_count, before + 1);
1564 }
1565
1566 #[test]
1567 fn a_reporter_that_goes_silent_is_failed_rather_than_shown_running_for_ever() {
1568 let store = store();
1569 let id = ExecutionId::fresh();
1570 store.adopt(id.clone(), seed(), 99).unwrap();
1571 store
1572 .report(
1573 &id,
1574 vec![
1575 (2, EventPayload::Queued { ahead: 0 }),
1576 (3, EventPayload::Started),
1577 ],
1578 )
1579 .unwrap();
1580 let own = start(&store);
1581 store.publish(&own, EventPayload::Started);
1582 assert!(
1583 store.abandon_silent(REPORT_SILENCE).is_empty(),
1584 "heard just now"
1585 );
1586 std::thread::sleep(Duration::from_millis(20));
1587 assert_eq!(
1588 store.abandon_silent(Duration::from_millis(5)),
1589 vec![id.clone()]
1590 );
1591 let snapshot = store.get(&id).unwrap();
1592 assert_eq!(snapshot.state, ExecutionState::Failed);
1593 let error = snapshot.error.expect("an error");
1594 assert_eq!(error.code, "unavailable");
1595 assert!(error.message.contains("process 99"), "{}", error.message);
1596 assert_eq!(
1597 store.get(&own).unwrap().state,
1598 ExecutionState::Running,
1599 "this process's own execution is never abandoned"
1600 );
1601 assert!(store.abandon_silent(Duration::ZERO).is_empty(), "once");
1602 store.report(
1604 &id,
1605 vec![(
1606 4,
1607 EventPayload::Completed {
1608 output: Value::Null,
1609 },
1610 )],
1611 );
1612 assert_eq!(store.get(&id).unwrap().state, ExecutionState::Failed);
1613 }
1614
1615 #[test]
1616 fn listing_is_newest_first_and_narrows_by_state_and_capability() {
1617 let store = store();
1618 let a = start(&store);
1619 let b = start(&store);
1620 store.publish(&b, EventPayload::Started);
1621 let all = store.list(None, None, 10);
1622 assert_eq!(all.first().map(|e| e.id.clone()), Some(b.clone()));
1623 assert_eq!(all.len(), 2);
1624 assert_eq!(store.list(Some(ExecutionState::Queued), None, 10)[0].id, a);
1625 assert_eq!(store.list(None, Some("demo.x"), 10).len(), 2);
1626 assert!(store.list(None, Some("nothing.here"), 10).is_empty());
1627 assert_eq!(store.list(None, None, 1).len(), 1);
1628 }
1629}