pub struct BasicCpmPlanner { /* private fields */ }Expand description
Open-source CPM planner with file-aware locking.
Implementations§
Source§impl BasicCpmPlanner
impl BasicCpmPlanner
Sourcepub fn new() -> Self
pub fn new() -> Self
Construct a planner with a real-clock and the
DEFAULT_TTL. Audit events are dropped on the floor (use
Self::with_audit if you need them retained).
Sourcepub fn with_audit(audit: Arc<dyn AuditSink>) -> Self
pub fn with_audit(audit: Arc<dyn AuditSink>) -> Self
Construct a planner with the supplied audit sink and the default
TTL. The real Utc::now is used as the clock.
Examples found in repository?
examples/plan_basic.rs (line 71)
68async fn main() -> anyhow::Result<()> {
69 // ── 1. Build the planner with a capturing audit sink ────────────
70 let audit = Arc::new(BufferingAudit::default());
71 let planner = BasicCpmPlanner::with_audit(audit.clone());
72
73 // ── 2. Submit a small graph ─────────────────────────────────────
74 //
75 // d1 (1h) ─┐
76 // ├─► d3 (2h) ─► d4 (1h)
77 // d2 (3h) ─┘ ▲
78 // │
79 // (critical
80 // path: d2→d3→d4 = 6h)
81 let graph = PlanGraph {
82 deliverables: vec![
83 deliverable("d1", &["src/a.rs"], &[], 1.0),
84 deliverable("d2", &["src/b.rs"], &[], 3.0),
85 deliverable("d3", &["src/c.rs"], &["d1", "d2"], 2.0),
86 deliverable("d4", &["src/d.rs"], &["d3"], 1.0),
87 ],
88 max_chained_dispatch: None,
89 };
90 let plan_id = planner.submit_plan(graph).await?;
91 println!("submitted plan: {plan_id}");
92
93 // ── 3. Print the critical path ──────────────────────────────────
94 let status = planner.status(&plan_id).await?;
95 println!(
96 "critical path: {:?} ({:.1}h total)",
97 status.critical_path, status.critical_path_hours
98 );
99
100 // ── 4. Drive the plan to completion, cohort by cohort ───────────
101 let caller = CallerId("demo-orchestrator".to_string());
102 let mut round = 0;
103 loop {
104 round += 1;
105 // Acquire up to 4 deliverables. The planner returns only those
106 // whose prerequisites are Complete AND whose files don't overlap
107 // with anything currently locked.
108 let cohort = planner.acquire_cohort(&plan_id, &caller, 4).await?;
109 if cohort.rows.is_empty() {
110 // Two cases for empty: terminal (everything Complete) or
111 // blocked (locks held by someone else, or no Ready work).
112 // For this single-caller example, empty means terminal.
113 println!("round {round}: no work remaining; plan is terminal.");
114 break;
115 }
116 let ids: Vec<&str> = cohort
117 .rows
118 .iter()
119 .map(|r| r.deliverable.id.as_str())
120 .collect();
121 println!("round {round}: acquired cohort {ids:?}");
122
123 for row in &cohort.rows {
124 planner
125 .mark_status(
126 &plan_id,
127 &row.deliverable.id,
128 &caller,
129 DeliverableStatus::Complete,
130 )
131 .await?;
132 println!(" marked {} complete", row.deliverable.id);
133 }
134 }
135
136 // ── 5. Final status ─────────────────────────────────────────────
137 let status = planner.status(&plan_id).await?;
138 let complete = status
139 .deliverables
140 .iter()
141 .filter(|(_, s)| matches!(s, DeliverableStatus::Complete))
142 .count();
143 println!(
144 "final state: {complete}/{total} deliverables complete; {locks} locks held",
145 total = status.deliverables.len(),
146 locks = status.locks_held.len()
147 );
148
149 // ── 6. Audit trail ──────────────────────────────────────────────
150 let events = audit
151 .events
152 .lock()
153 .map_err(|e| anyhow::anyhow!("audit buffer poisoned: {e}"))?;
154 println!("\naudit events ({} total):", events.len());
155 for event in events.iter() {
156 println!(" - {}", event.event_type);
157 }
158
159 Ok(())
160}Sourcepub fn with_ttl(self, ttl: Duration) -> Self
pub fn with_ttl(self, ttl: Duration) -> Self
Override the lock TTL. Useful for short-lived integration tests.
Sourcepub fn with_clock(self, clock: ClockFn) -> Self
pub fn with_clock(self, clock: ClockFn) -> Self
Override the clock. Intended for deterministic TTL tests; production code should not call this.
Trait Implementations§
Source§impl Default for BasicCpmPlanner
impl Default for BasicCpmPlanner
Source§impl Planner for BasicCpmPlanner
impl Planner for BasicCpmPlanner
Source§fn submit_plan<'life0, 'async_trait>(
&'life0 self,
graph: PlanGraph,
) -> Pin<Box<dyn Future<Output = Result<PlanId, PlannerError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn submit_plan<'life0, 'async_trait>(
&'life0 self,
graph: PlanGraph,
) -> Pin<Box<dyn Future<Output = Result<PlanId, PlannerError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Source§fn acquire_cohort<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
plan_id: &'life1 PlanId,
caller_id: &'life2 CallerId,
max_count: usize,
) -> Pin<Box<dyn Future<Output = Result<Cohort, PlannerError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
fn acquire_cohort<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
plan_id: &'life1 PlanId,
caller_id: &'life2 CallerId,
max_count: usize,
) -> Pin<Box<dyn Future<Output = Result<Cohort, PlannerError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Acquire up to
max_count deliverables that are ready to run and have
mutually disjoint owned_files (within the cohort and against all
currently held locks). The returned Cohort carries one
crate::plan::LockInfo per acquired deliverable, in the same order.Source§fn mark_status<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
plan_id: &'life1 PlanId,
deliverable_id: &'life2 str,
caller_id: &'life3 CallerId,
status: DeliverableStatus,
) -> Pin<Box<dyn Future<Output = Result<(), PlannerError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
fn mark_status<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
plan_id: &'life1 PlanId,
deliverable_id: &'life2 str,
caller_id: &'life3 CallerId,
status: DeliverableStatus,
) -> Pin<Box<dyn Future<Output = Result<(), PlannerError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
Update the lifecycle state of a deliverable. Setting
Complete or
Failed releases the lock; caller_id MUST be the lock holder or the
call is rejected with PlannerError::LockNotHeld.Source§fn heartbeat<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
plan_id: &'life1 PlanId,
deliverable_id: &'life2 str,
caller_id: &'life3 CallerId,
) -> Pin<Box<dyn Future<Output = Result<(), PlannerError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
fn heartbeat<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
plan_id: &'life1 PlanId,
deliverable_id: &'life2 str,
caller_id: &'life3 CallerId,
) -> Pin<Box<dyn Future<Output = Result<(), PlannerError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
Refresh the TTL on a held lock. Rejected with
PlannerError::LockNotHeld if caller_id is not the holder, or with
PlannerError::LockExpired if the lock already lapsed.Source§fn status<'life0, 'life1, 'async_trait>(
&'life0 self,
plan_id: &'life1 PlanId,
) -> Pin<Box<dyn Future<Output = Result<PlanStatus, PlannerError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn status<'life0, 'life1, 'async_trait>(
&'life0 self,
plan_id: &'life1 PlanId,
) -> Pin<Box<dyn Future<Output = Result<PlanStatus, PlannerError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Cheap read-only snapshot. Safe to poll on a timer.
Source§fn force_release<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
plan_id: &'life1 PlanId,
deliverable_id: &'life2 str,
reason: &'life3 str,
) -> Pin<Box<dyn Future<Output = Result<(), PlannerError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
fn force_release<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
plan_id: &'life1 PlanId,
deliverable_id: &'life2 str,
reason: &'life3 str,
) -> Pin<Box<dyn Future<Output = Result<(), PlannerError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
Operator escape hatch: forcibly release a lock regardless of holder or
TTL. Implementations MUST emit an audit event carrying
reason.Auto Trait Implementations§
impl !RefUnwindSafe for BasicCpmPlanner
impl !UnwindSafe for BasicCpmPlanner
impl Freeze for BasicCpmPlanner
impl Send for BasicCpmPlanner
impl Sync for BasicCpmPlanner
impl Unpin for BasicCpmPlanner
impl UnsafeUnpin for BasicCpmPlanner
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more