Skip to main content

BasicCpmPlanner

Struct BasicCpmPlanner 

Source
pub struct BasicCpmPlanner { /* private fields */ }
Expand description

Open-source CPM planner with file-aware locking.

Implementations§

Source§

impl BasicCpmPlanner

Source

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).

Source

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}
Source

pub fn with_ttl(self, ttl: Duration) -> Self

Override the lock TTL. Useful for short-lived integration tests.

Source

pub fn with_clock(self, clock: ClockFn) -> Self

Override the clock. Intended for deterministic TTL tests; production code should not call this.

Source

pub fn with_parts( audit: Arc<dyn AuditSink>, ttl: Duration, clock: ClockFn, ) -> Self

Full-parts constructor. Public for clients that want explicit control over every field at once.

Trait Implementations§

Source§

impl Default for BasicCpmPlanner

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

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,

Submit a PlanGraph. Idempotent on (graph, caller_id); an identical resubmission returns the existing PlanId.
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,

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,

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,

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,

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,

Operator escape hatch: forcibly release a lock regardless of holder or TTL. Implementations MUST emit an audit event carrying reason.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more