154 lines
6.4 KiB
Rust
154 lines
6.4 KiB
Rust
//! Per-automation run-mode machinery (ADR-162, completes ADR-161 §A5).
|
|
//!
|
|
//! ADR-161 implemented `RunMode::Single` (a per-automation `AtomicBool`
|
|
//! re-entrancy guard) and `Parallel`, but honestly left `Restart`, `Queued`
|
|
//! and `max: N` as "ACCEPTED-FUTURE / unbounded parallel" — every non-Single
|
|
//! mode spawned an unbounded task. This module makes them real:
|
|
//!
|
|
//! | Mode | Semantics implemented |
|
|
//! |------|-----------------------|
|
|
//! | `Single` / `IgnoreFirst` | re-entrancy guard: skip while a run is in flight (ADR-161). |
|
|
//! | `Restart` | **cancel** the in-flight run (`tokio::task::AbortHandle`) and start a fresh one. |
|
|
//! | `Queued` | **serialize**: runs execute sequentially in arrival order via a per-automation async mutex — nothing is dropped. |
|
|
//! | `Parallel` | spawn on every trigger (optionally capped, see below). |
|
|
//! | `max: N` | cap concurrency at **N** via a per-automation semaphore; triggers beyond N **queue** (await a permit) rather than running concurrently — matching HA's bounded `parallel`/`queued`. |
|
|
//!
|
|
//! Each registered automation owns one [`RunState`]; the engine calls
|
|
//! [`RunState::dispatch`] on every (trigger + conditions-passed) event.
|
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use tokio::sync::{Mutex as AsyncMutex, Semaphore};
|
|
|
|
use homecore::HomeCore;
|
|
|
|
use crate::action::ExecutionContext;
|
|
use crate::automation::{Automation, RunMode};
|
|
|
|
/// Per-automation runtime state backing the run-mode dispatch.
|
|
///
|
|
/// Cheap to clone (all fields are `Arc`); the engine clones it into each
|
|
/// spawned run so the machinery (abort handle, queue mutex, semaphore) is
|
|
/// shared across all triggers of the same automation.
|
|
#[derive(Clone)]
|
|
pub struct RunState {
|
|
/// `Single`/`IgnoreFirst` re-entrancy guard (ADR-161 §A5).
|
|
running: Arc<AtomicBool>,
|
|
/// `Restart`: handle to the currently-running action task, so a new
|
|
/// trigger can abort it before starting a fresh one.
|
|
current: Arc<Mutex<Option<tokio::task::AbortHandle>>>,
|
|
/// `Queued`: serializes runs in arrival order (one at a time, FIFO via
|
|
/// fair async mutex acquisition).
|
|
queue_lock: Arc<AsyncMutex<()>>,
|
|
/// `max: N` (and bounded `Parallel`): caps concurrent runs at N.
|
|
/// `None` when no cap applies.
|
|
semaphore: Option<Arc<Semaphore>>,
|
|
}
|
|
|
|
impl RunState {
|
|
/// Build run-state for an automation, sizing the concurrency semaphore
|
|
/// from its `max:` field (only meaningful for `Queued`/`Parallel`).
|
|
pub fn new(automation: &Automation) -> Self {
|
|
let semaphore = automation
|
|
.max
|
|
.filter(|n| *n > 0)
|
|
.map(|n| Arc::new(Semaphore::new(n)));
|
|
Self {
|
|
running: Arc::new(AtomicBool::new(false)),
|
|
current: Arc::new(Mutex::new(None)),
|
|
queue_lock: Arc::new(AsyncMutex::new(())),
|
|
semaphore,
|
|
}
|
|
}
|
|
|
|
/// Dispatch one trigger for `automation` according to its `RunMode`.
|
|
/// Honors Single re-entrancy, Restart cancel-and-replace, Queued
|
|
/// serialization, and `max:` concurrency capping.
|
|
pub fn dispatch(&self, hc: &HomeCore, automation: Arc<Automation>) {
|
|
match automation.mode {
|
|
RunMode::Single | RunMode::IgnoreFirst => self.dispatch_single(hc, automation),
|
|
RunMode::Restart => self.dispatch_restart(hc, automation),
|
|
RunMode::Queued => self.dispatch_queued(hc, automation),
|
|
RunMode::Parallel => self.dispatch_parallel(hc, automation),
|
|
}
|
|
}
|
|
|
|
/// `Single`: skip if a run is already in flight; clear the flag on done.
|
|
fn dispatch_single(&self, hc: &HomeCore, automation: Arc<Automation>) {
|
|
if self
|
|
.running
|
|
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
|
.is_err()
|
|
{
|
|
return; // already running — skip re-entrant trigger.
|
|
}
|
|
let hc = hc.clone();
|
|
let running = Arc::clone(&self.running);
|
|
tokio::spawn(async move {
|
|
run_actions(&hc, &automation).await;
|
|
running.store(false, Ordering::SeqCst);
|
|
});
|
|
}
|
|
|
|
/// `Restart`: abort the in-flight run (if any), then start a fresh one
|
|
/// and record its abort handle.
|
|
fn dispatch_restart(&self, hc: &HomeCore, automation: Arc<Automation>) {
|
|
// Abort any prior run before starting the new one.
|
|
if let Some(prev) = self.current.lock().unwrap().take() {
|
|
prev.abort();
|
|
}
|
|
let hc = hc.clone();
|
|
let slot = Arc::clone(&self.current);
|
|
let handle = tokio::spawn(async move {
|
|
run_actions(&hc, &automation).await;
|
|
});
|
|
*slot.lock().unwrap() = Some(handle.abort_handle());
|
|
}
|
|
|
|
/// `Queued`: serialize via the per-automation async mutex. Each trigger
|
|
/// spawns a task that waits its turn, so all triggers run in arrival
|
|
/// order, one at a time — nothing is dropped.
|
|
fn dispatch_queued(&self, hc: &HomeCore, automation: Arc<Automation>) {
|
|
let hc = hc.clone();
|
|
let lock = Arc::clone(&self.queue_lock);
|
|
let sem = self.semaphore.clone();
|
|
tokio::spawn(async move {
|
|
// Optional `max:` cap still applies on top of serialization.
|
|
let _permit = match &sem {
|
|
Some(s) => Some(s.acquire().await.expect("semaphore not closed")),
|
|
None => None,
|
|
};
|
|
let _guard = lock.lock().await; // FIFO turn — sequential execution.
|
|
run_actions(&hc, &automation).await;
|
|
});
|
|
}
|
|
|
|
/// `Parallel`: spawn on every trigger, capped at `max:` if set.
|
|
fn dispatch_parallel(&self, hc: &HomeCore, automation: Arc<Automation>) {
|
|
let hc = hc.clone();
|
|
let sem = self.semaphore.clone();
|
|
tokio::spawn(async move {
|
|
let _permit = match &sem {
|
|
Some(s) => Some(s.acquire().await.expect("semaphore not closed")),
|
|
None => None,
|
|
};
|
|
run_actions(&hc, &automation).await;
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Execute an automation's action sequence once.
|
|
async fn run_actions(hc: &HomeCore, automation: &Automation) {
|
|
let mut exec_ctx = ExecutionContext::new(hc.clone(), automation.id.clone());
|
|
for action in &automation.action {
|
|
if let Err(e) = action.execute(&mut exec_ctx).await {
|
|
eprintln!(
|
|
"[homecore-automation] action error in {}: {e}",
|
|
automation.id
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
}
|