1use serde::{Deserialize, Serialize};
4
5use std::future::Future;
6use std::pin::Pin;
7
8use crate::comms::{PeerAddress, PeerId, TrustedPeerDescriptor};
9use crate::lifecycle::{RunId, WaitRequestId};
10pub use crate::ops::{OperationId, OperationResult};
11use crate::runtime_epoch::EpochCursorState;
12use crate::types::SessionId;
13
14pub const DEFAULT_MAX_COMPLETED: usize = 256;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum OperationKind {
21 MobMemberChild,
22 BackgroundToolOp,
23 BackgroundToolCapacitySlot,
24}
25
26impl OperationKind {
27 pub const ALL: [Self; 3] = [
29 Self::MobMemberChild,
30 Self::BackgroundToolOp,
31 Self::BackgroundToolCapacitySlot,
32 ];
33
34 pub const fn generated_variant(self) -> &'static str {
36 match self {
37 Self::MobMemberChild => "MobMemberChild",
38 Self::BackgroundToolOp => "BackgroundToolOp",
39 Self::BackgroundToolCapacitySlot => "BackgroundToolCapacitySlot",
40 }
41 }
42
43 pub fn expects_peer_channel(self) -> bool {
45 matches!(self, Self::MobMemberChild)
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(tag = "kind", rename_all = "snake_case")]
52pub enum OperationSource {
53 SessionChild {
54 session_id: SessionId,
55 },
56 BackendPeer {
57 peer_id: PeerId,
58 address: PeerAddress,
59 },
60}
61
62impl OperationSource {
63 pub fn session_child(session_id: SessionId) -> Self {
64 Self::SessionChild { session_id }
65 }
66
67 pub fn backend_peer(peer_id: PeerId, address: PeerAddress) -> Self {
68 Self::BackendPeer { peer_id, address }
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct OperationSpec {
75 pub id: OperationId,
76 pub kind: OperationKind,
77 pub owner_session_id: SessionId,
82 pub display_name: String,
83 pub source_label: String,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub operation_source: Option<OperationSource>,
86 pub child_session_id: Option<SessionId>,
87 pub expect_peer_channel: bool,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct OperationPeerHandle {
93 pub peer_name: crate::comms::PeerName,
94 pub trusted_peer: TrustedPeerDescriptor,
95}
96
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
99pub struct OperationProgressUpdate {
100 pub message: String,
101 pub percent: Option<f32>,
102}
103
104#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(tag = "outcome_type", rename_all = "snake_case")]
113pub enum OperationTerminalOutcome {
114 Completed(OperationResult),
115 Failed {
116 error: String,
117 },
118 Aborted {
119 reason: Option<String>,
120 },
121 Cancelled {
122 reason: Option<String>,
123 },
124 #[default]
125 Retired,
126 Terminated {
127 reason: String,
128 },
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
133#[serde(rename_all = "snake_case")]
134pub enum OperationStatus {
135 Absent,
136 Provisioning,
137 Running,
138 Retiring,
139 Completed,
140 Failed,
141 Aborted,
142 Cancelled,
143 Retired,
144 Terminated,
145}
146
147impl OperationStatus {
148 pub fn as_str(self) -> &'static str {
153 match self {
154 Self::Absent => "absent",
155 Self::Provisioning => "provisioning",
156 Self::Running => "running",
157 Self::Retiring => "retiring",
158 Self::Completed => "completed",
159 Self::Failed => "failed",
160 Self::Aborted => "aborted",
161 Self::Cancelled => "cancelled",
162 Self::Retired => "retired",
163 Self::Terminated => "terminated",
164 }
165 }
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
174#[serde(rename_all = "snake_case")]
175pub enum OperationPublicResultClass {
176 MissingAuthority,
177 Running,
178 Completed,
179 Failed,
180 Cancelled,
181}
182
183impl OperationPublicResultClass {
184 pub const ALL: [Self; 5] = [
186 Self::MissingAuthority,
187 Self::Running,
188 Self::Completed,
189 Self::Failed,
190 Self::Cancelled,
191 ];
192
193 pub const fn generated_variant(self) -> &'static str {
195 match self {
196 Self::MissingAuthority => "MissingAuthority",
197 Self::Running => "Running",
198 Self::Completed => "Completed",
199 Self::Failed => "Failed",
200 Self::Cancelled => "Cancelled",
201 }
202 }
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
208#[serde(rename_all = "snake_case")]
209pub enum OperationCompletionWakeClass {
210 Wake,
211 Ignore,
212}
213
214impl OperationCompletionWakeClass {
215 pub const ALL: [Self; 2] = [Self::Wake, Self::Ignore];
217
218 pub const fn generated_variant(self) -> &'static str {
220 match self {
221 Self::Wake => "Wake",
222 Self::Ignore => "Ignore",
223 }
224 }
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
229#[serde(rename_all = "snake_case")]
230pub enum OperationLifecycleAction {
231 Start,
232 Fail,
233 PeerReady,
234 ProgressReported,
235 Complete,
236 Abort,
237 Cancel,
238 RetireRequested,
239 RetireCompleted,
240 Terminate,
241}
242
243impl OperationLifecycleAction {
244 pub const ALL: [Self; 10] = [
246 Self::Start,
247 Self::Fail,
248 Self::PeerReady,
249 Self::ProgressReported,
250 Self::Complete,
251 Self::Abort,
252 Self::Cancel,
253 Self::RetireRequested,
254 Self::RetireCompleted,
255 Self::Terminate,
256 ];
257
258 pub const fn generated_variant(self) -> &'static str {
260 match self {
261 Self::Start => "Start",
262 Self::Fail => "Fail",
263 Self::PeerReady => "PeerReady",
264 Self::ProgressReported => "ProgressReported",
265 Self::Complete => "Complete",
266 Self::Abort => "Abort",
267 Self::Cancel => "Cancel",
268 Self::RetireRequested => "RetireRequested",
269 Self::RetireCompleted => "RetireCompleted",
270 Self::Terminate => "Terminate",
271 }
272 }
273}
274
275#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
277pub struct OperationLifecycleSnapshot {
278 pub id: OperationId,
279 pub kind: OperationKind,
280 pub owner_session_id: SessionId,
282 pub display_name: String,
283 pub source_label: String,
284 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub operation_source: Option<OperationSource>,
286 pub status: OperationStatus,
287 pub terminal: bool,
289 pub public_result_class: OperationPublicResultClass,
291 pub peer_ready: bool,
292 pub progress_count: u32,
293 pub watcher_count: u32,
294 pub terminal_outcome: Option<OperationTerminalOutcome>,
295 pub child_session_id: Option<SessionId>,
296 pub expect_peer_channel: bool,
297 #[serde(skip_serializing_if = "Option::is_none", default)]
299 pub peer_handle: Option<OperationPeerHandle>,
300 #[serde(default)]
302 pub created_at_ms: u64,
303 #[serde(skip_serializing_if = "Option::is_none", default)]
305 pub started_at_ms: Option<u64>,
306 #[serde(skip_serializing_if = "Option::is_none", default)]
308 pub completed_at_ms: Option<u64>,
309 #[serde(skip_serializing_if = "Option::is_none", default)]
311 pub elapsed_ms: Option<u64>,
312}
313
314pub type OperationCompletionWatch = Pin<
321 Box<
322 dyn Future<Output = Result<OperationTerminalOutcome, OperationCompletionWatchError>>
323 + Send
324 + 'static,
325 >,
326>;
327
328#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
333pub enum OperationCompletionWatchError {
334 #[error("operation completion channel closed without an authorized terminal outcome")]
335 ChannelClosed,
336}
337
338#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
340pub enum OpsLifecycleError {
341 #[error("operation already registered: {0}")]
342 AlreadyRegistered(OperationId),
343 #[error("operation not found: {0}")]
344 NotFound(OperationId),
345 #[error("invalid lifecycle transition for {id}: {status:?} -> {action}")]
346 InvalidTransition {
347 id: OperationId,
348 status: OperationStatus,
349 action: &'static str,
350 },
351 #[error("operation does not expect a peer handoff: {0}")]
352 PeerNotExpected(OperationId),
353 #[error("operation is already peer-ready: {0}")]
354 AlreadyPeerReady(OperationId),
355 #[error("max concurrent operations exceeded (limit: {limit}, active: {active})")]
356 MaxConcurrentExceeded { limit: usize, active: usize },
357 #[error("operation not supported: {0}")]
358 Unsupported(String),
359 #[error("wait_all already active")]
360 WaitAlreadyActive,
361 #[error("wait_all not active for request: {0}")]
362 WaitNotActive(WaitRequestId),
363 #[error("wait_all contains duplicate operation id: {0}")]
364 DuplicateWaitOperation(OperationId),
365 #[error("operation owner is retired")]
366 OwnerRetired,
367 #[error("internal lifecycle registry error: {0}")]
368 Internal(String),
369}
370
371#[derive(Debug)]
378pub struct WaitAllResult {
379 pub outcomes: Vec<(OperationId, OperationTerminalOutcome)>,
381 pub satisfied: WaitAllSatisfied,
383}
384
385#[derive(Debug)]
392pub struct WaitAllSatisfied {
393 pub wait_request_id: WaitRequestId,
395 pub run_id: RunId,
397 pub operation_ids: Vec<OperationId>,
399}
400
401#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
403pub enum CompletionCursorConsumer {
404 AgentApplied,
406 RuntimeObserved,
408 RuntimeInjected,
410}
411
412pub trait OpsLifecycleRegistry: Send + Sync {
414 fn register_operation(&self, spec: OperationSpec) -> Result<(), OpsLifecycleError>;
415 fn provisioning_succeeded(&self, id: &OperationId) -> Result<(), OpsLifecycleError>;
416 fn provisioning_failed(&self, id: &OperationId, error: String)
417 -> Result<(), OpsLifecycleError>;
418 fn peer_ready(
419 &self,
420 id: &OperationId,
421 peer: OperationPeerHandle,
422 ) -> Result<(), OpsLifecycleError>;
423 fn register_watcher(
424 &self,
425 id: &OperationId,
426 ) -> Result<OperationCompletionWatch, OpsLifecycleError>;
427 fn report_progress(
428 &self,
429 id: &OperationId,
430 update: OperationProgressUpdate,
431 ) -> Result<(), OpsLifecycleError>;
432 fn complete_operation(
433 &self,
434 id: &OperationId,
435 result: OperationResult,
436 ) -> Result<(), OpsLifecycleError>;
437 fn fail_operation(&self, id: &OperationId, error: String) -> Result<(), OpsLifecycleError>;
438 fn abort_provisioning(
439 &self,
440 id: &OperationId,
441 reason: Option<String>,
442 ) -> Result<(), OpsLifecycleError>;
443 fn cancel_operation(
444 &self,
445 id: &OperationId,
446 reason: Option<String>,
447 ) -> Result<(), OpsLifecycleError>;
448 fn request_retire(&self, id: &OperationId) -> Result<(), OpsLifecycleError>;
449 fn mark_retired(&self, id: &OperationId) -> Result<(), OpsLifecycleError>;
450 fn snapshot(
451 &self,
452 id: &OperationId,
453 ) -> Result<Option<OperationLifecycleSnapshot>, OpsLifecycleError>;
454 fn list_operations(&self) -> Result<Vec<OperationLifecycleSnapshot>, OpsLifecycleError>;
455 fn terminate_owner(&self, reason: String) -> Result<(), OpsLifecycleError>;
456
457 fn classify_operation_terminality(
460 &self,
461 _id: &OperationId,
462 _status: OperationStatus,
463 ) -> Result<bool, OpsLifecycleError> {
464 Err(OpsLifecycleError::Unsupported(
465 "classify_operation_terminality".into(),
466 ))
467 }
468
469 fn classify_operation_public_result(
472 &self,
473 _id: &OperationId,
474 ) -> Result<OperationPublicResultClass, OpsLifecycleError> {
475 Err(OpsLifecycleError::Unsupported(
476 "classify_operation_public_result".into(),
477 ))
478 }
479
480 fn classify_operation_completion_wake(
483 &self,
484 _id: &OperationId,
485 _kind: OperationKind,
486 ) -> Result<OperationCompletionWakeClass, OpsLifecycleError> {
487 Err(OpsLifecycleError::Unsupported(
488 "classify_operation_completion_wake".into(),
489 ))
490 }
491
492 fn classify_operation_transition_idempotence(
495 &self,
496 _id: &OperationId,
497 _action: OperationLifecycleAction,
498 ) -> Result<bool, OpsLifecycleError> {
499 Err(OpsLifecycleError::Unsupported(
500 "classify_operation_transition_idempotence".into(),
501 ))
502 }
503
504 fn register_operation_with_admission_limit(
507 &self,
508 _spec: OperationSpec,
509 _max_concurrent: Option<usize>,
510 ) -> Result<(), OpsLifecycleError> {
511 Err(OpsLifecycleError::Unsupported(
512 "register_operation_with_admission_limit".into(),
513 ))
514 }
515
516 fn collect_completed(
518 &self,
519 ) -> Result<Vec<(OperationId, OperationTerminalOutcome)>, OpsLifecycleError> {
520 Err(OpsLifecycleError::Unsupported("collect_completed".into()))
521 }
522
523 fn completion_feed(
529 &self,
530 ) -> Option<std::sync::Arc<dyn crate::completion_feed::CompletionFeed>> {
531 None
532 }
533
534 fn completion_cursor(
541 &self,
542 _consumer: CompletionCursorConsumer,
543 ) -> Result<Option<crate::completion_feed::CompletionSeq>, OpsLifecycleError> {
544 Ok(None)
545 }
546
547 fn advance_completion_cursor(
553 &self,
554 _consumer: CompletionCursorConsumer,
555 _cursor: crate::completion_feed::CompletionSeq,
556 _projection: Option<&EpochCursorState>,
557 ) -> Result<crate::completion_feed::CompletionSeq, OpsLifecycleError> {
558 Err(OpsLifecycleError::Unsupported(
559 "advance_completion_cursor".into(),
560 ))
561 }
562
563 fn wait_all(
570 &self,
571 _run_id: &RunId,
572 _ids: &[OperationId],
573 ) -> std::pin::Pin<
574 Box<dyn std::future::Future<Output = Result<WaitAllResult, OpsLifecycleError>> + Send + '_>,
575 > {
576 Box::pin(std::future::ready(Err(OpsLifecycleError::Unsupported(
577 "wait_all".into(),
578 ))))
579 }
580}
581
582#[cfg(test)]
583#[allow(clippy::unwrap_used, clippy::panic)]
584mod tests {
585 use super::*;
586
587 #[test]
588 fn operation_kind_peer_expectation_matches_contract() {
589 assert!(OperationKind::MobMemberChild.expects_peer_channel());
590 assert!(!OperationKind::BackgroundToolOp.expects_peer_channel());
591 assert!(!OperationKind::BackgroundToolCapacitySlot.expects_peer_channel());
592 }
593}