Skip to main content

wora/
lib.rs

1//! Write Once Run Anywhere (WORA)
2//!
3//! A framework for building applications (daemons, etc.) that run in different environments (Linux, Kubernetes, etc.).
4//!
5//! Just like Java's claim, it really doesn't run everywhere. no_std or embedded environments are not supported.
6
7use std::fmt::Debug;
8use std::future::Future;
9use std::path::{Path, PathBuf};
10use std::pin::Pin;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
13
14use async_trait::async_trait;
15use chrono::Utc;
16use nix::unistd::getpid;
17use serde::Serialize;
18use tokio::sync::mpsc::channel;
19use tokio::sync::mpsc::{Receiver, Sender};
20use tokio::sync::watch;
21use tokio::task::JoinHandle;
22use tokio_util::sync::CancellationToken;
23use tracing::Instrument;
24use tracing::{debug, error, info, trace, warn};
25
26mod boot;
27pub mod dirs;
28pub mod errors;
29pub mod events;
30pub mod exec;
31pub mod exec_env;
32pub mod exec_unix;
33pub mod lock;
34pub mod o11y;
35pub mod prelude;
36pub mod profiles;
37pub mod resources;
38pub mod restart_policy;
39pub mod vfs;
40
41use crate::boot::{default_boot_root, resolve_boot_state};
42use crate::dirs::Dirs;
43use crate::errors::{MainEarlyReturn, ReloadError, SetupFailure, VfsError, WoraSetupError};
44use crate::events::*;
45use crate::exec::*;
46use crate::lock::{LockBackend, LockError, ProcLockBackend};
47use crate::o11y::*;
48pub use crate::resources::{ResourceError, RuntimeResourceLimits, RuntimeResources};
49use crate::restart_policy::*;
50use crate::vfs::*;
51
52const EVENT_BUFFER_SIZE: usize = 1024;
53#[cfg(not(test))]
54const TASK_SHUTDOWN_GRACE_PERIOD: tokio::time::Duration = tokio::time::Duration::from_secs(1);
55#[cfg(test)]
56const TASK_SHUTDOWN_GRACE_PERIOD: tokio::time::Duration = tokio::time::Duration::from_millis(50);
57static CONCURRENT_LOCK_NONCE: AtomicU64 = AtomicU64::new(0);
58
59struct RuntimeTaskRegistry {
60    cancellation: CancellationToken,
61    tasks: Vec<(&'static str, JoinHandle<Result<(), String>>)>,
62}
63
64impl RuntimeTaskRegistry {
65    fn new() -> Self {
66        Self {
67            cancellation: CancellationToken::new(),
68            tasks: Vec::new(),
69        }
70    }
71
72    fn cancellation_token(&self) -> CancellationToken {
73        self.cancellation.clone()
74    }
75
76    fn spawn<F>(&mut self, name: &'static str, future: F)
77    where
78        F: Future<Output = Result<(), String>> + Send + 'static,
79    {
80        self.tasks.push((name, tokio::spawn(future)));
81    }
82
83    fn adopt(&mut self, name: &'static str, mut task: JoinHandle<()>) {
84        let cancelled = self.cancellation_token();
85        self.spawn(name, async move {
86            tokio::select! {
87                result = &mut task => result.map_err(|err| err.to_string()),
88                _ = cancelled.cancelled() => {
89                    task.abort();
90                    match task.await {
91                        Ok(()) => Ok(()),
92                        Err(err) if err.is_cancelled() => Ok(()),
93                        Err(err) => Err(err.to_string()),
94                    }
95                }
96            }
97        });
98    }
99
100    async fn shutdown(&mut self) -> Result<(), MainEarlyReturn> {
101        self.cancellation.cancel();
102        let mut tasks = std::mem::take(&mut self.tasks);
103        let joined = tokio::time::timeout(TASK_SHUTDOWN_GRACE_PERIOD, async {
104            let mut first_failure = None;
105            for (name, task) in &mut tasks {
106                match task.await {
107                    Ok(Ok(())) => {}
108                    Ok(Err(err)) => {
109                        first_failure.get_or_insert_with(|| format!("{name}: {err}"));
110                    }
111                    Err(err) if err.is_cancelled() => {}
112                    Err(err) => {
113                        first_failure.get_or_insert_with(|| format!("{name}: {err}"));
114                    }
115                };
116            }
117            first_failure
118        })
119        .await;
120
121        let failure = match joined {
122            Ok(failure) => failure,
123            Err(_) => {
124                for (_, task) in &tasks {
125                    task.abort();
126                }
127                for (_, task) in &mut tasks {
128                    let _ = task.await;
129                }
130                None
131            }
132        };
133
134        failure.map_or(Ok(()), |failure| Err(MainEarlyReturn::InfrastructureTask(failure)))
135    }
136}
137
138impl Drop for RuntimeTaskRegistry {
139    fn drop(&mut self) {
140        self.cancellation.cancel();
141        for (_, task) in &self.tasks {
142            task.abort();
143        }
144    }
145}
146
147/// Runtime context passed to applications.
148///
149/// `Wora` gives app lifecycle hooks access to executor directories, host
150/// information, event channels, leadership state, and observability settings.
151pub struct Wora<AppEv, AppMetric> {
152    /// application name
153    pub app_name: String,
154    /// common directories for all executors
155    pub dirs: Dirs,
156    /// current directory where the process was invoked (an executor will likely override this)
157    pub initial_working_dir: PathBuf,
158    /// host info/stats
159    pub host: Host,
160    /// process id
161    pid: nix::unistd::Pid,
162    /// `Event` producer
163    pub sender: Sender<Event<AppEv>>,
164    /// `Event` receiver
165    pub receiver: Receiver<Event<AppEv>>,
166    /// leadership state
167    pub leadership: Leadership,
168    /// Observability channel and scheduling options.
169    pub(crate) o11y: O11yProcessorOptions<AppMetric>,
170    /// Shared runtime status for health and readiness supervision.
171    status: RuntimeStatusHandle,
172    memory_stats_tx: watch::Sender<Arc<MemorySnapshot>>,
173    resources: Option<RuntimeResources>,
174}
175
176/// Current leadership role for a workload.
177#[derive(Clone, Debug, Serialize)]
178pub enum Leadership {
179    /// This process is currently the active leader.
180    Leader,
181    /// This process is currently a follower.
182    Follower,
183    /// Leadership has not been established.
184    Unknown,
185}
186
187/// Health state reported by an application.
188#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
189pub enum HealthState {
190    /// The application is healthy.
191    Ok,
192    /// The application is intentionally suspended.
193    Suspended,
194    /// Health could not be determined yet and should be retried.
195    TryAgain,
196    /// The application is unhealthy.
197    Failed,
198    /// Health has not been reported.
199    Unknown,
200}
201
202/// Readiness state reported by an application.
203#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
204pub enum ReadinessState {
205    /// The application is ready to serve.
206    Ready,
207    /// The application is still warming up or otherwise not ready.
208    NotReady,
209    /// The application is draining existing work and should stop receiving new work.
210    Draining,
211    /// The application is stopping and should no longer be considered ready.
212    Stopping,
213    /// Readiness has not been reported.
214    Unknown,
215}
216
217/// Cloneable handle used by apps and background tasks to report health and readiness.
218#[derive(Clone, Debug)]
219pub struct RuntimeStatusHandle {
220    health: watch::Sender<HealthState>,
221    readiness: watch::Sender<ReadinessState>,
222}
223
224impl RuntimeStatusHandle {
225    fn new() -> Self {
226        let (health, _) = watch::channel(HealthState::Unknown);
227        let (readiness, _) = watch::channel(ReadinessState::Ready);
228        Self { health, readiness }
229    }
230
231    /// Report a new health state.
232    pub fn report_health(&self, state: HealthState) {
233        let _ = self.health.send(state);
234    }
235
236    /// Report a new readiness state.
237    pub fn report_readiness(&self, state: ReadinessState) {
238        let _ = self.readiness.send(state);
239    }
240
241    /// Transition the runtime into a draining state.
242    pub fn begin_draining(&self) {
243        self.report_readiness(ReadinessState::Draining);
244    }
245
246    /// Transition the runtime into a stopping state.
247    pub fn begin_stopping(&self) {
248        self.report_readiness(ReadinessState::Stopping);
249    }
250
251    /// Subscribe to health state changes.
252    pub fn subscribe_health(&self) -> watch::Receiver<HealthState> {
253        self.health.subscribe()
254    }
255
256    /// Subscribe to readiness state changes.
257    pub fn subscribe_readiness(&self) -> watch::Receiver<ReadinessState> {
258        self.readiness.subscribe()
259    }
260
261    /// Return the latest reported health state.
262    pub fn health_state(&self) -> HealthState {
263        self.health.borrow().clone()
264    }
265
266    /// Return the latest reported readiness state.
267    pub fn readiness_state(&self) -> ReadinessState {
268        self.readiness.borrow().clone()
269    }
270}
271
272/// WORA API
273impl<AppEv: Send + Sync + 'static, AppMetric: Send + Sync + 'static> Wora<AppEv, AppMetric> {
274    /// Create a new runtime context.
275    pub fn new(dirs: &Dirs, app_name: String, ev_buf_size: usize, o11y: O11yProcessorOptions<AppMetric>) -> Result<Wora<AppEv, AppMetric>, WoraSetupError> {
276        Self::new_with_runtime_environment(dirs, app_name, ev_buf_size, o11y, SystemRuntimeEnvironment::default())
277    }
278
279    /// Create a new runtime context with an explicit runtime environment.
280    pub fn new_with_runtime_environment<R: RuntimeEnvironment>(
281        dirs: &Dirs,
282        app_name: String,
283        ev_buf_size: usize,
284        o11y: O11yProcessorOptions<AppMetric>,
285        runtime_environment: R,
286    ) -> Result<Wora<AppEv, AppMetric>, WoraSetupError> {
287        let pid = getpid();
288
289        let (tx, rx) = channel(ev_buf_size);
290
291        let host = runtime_environment.initial_host()?;
292        let (memory_stats_tx, _) = watch::channel(Arc::new(MemorySnapshot::from_host_stats(&host.stats)));
293        let status = RuntimeStatusHandle::new();
294
295        let current_dir = std::env::current_dir()?;
296
297        Ok(Wora {
298            app_name,
299            initial_working_dir: current_dir,
300            dirs: dirs.clone(),
301            host,
302            pid,
303            sender: tx,
304            receiver: rx,
305            leadership: Leadership::Unknown,
306            o11y,
307            status,
308            memory_stats_tx,
309            resources: None,
310        })
311    }
312
313    /// Return the host statistics captured when the runtime started.
314    pub fn stats_from_start(&self) -> &HostStats {
315        &self.host.stats
316    }
317
318    /// Inspect shared runner admission state, if explicitly configured.
319    pub fn resources(&self) -> Option<&RuntimeResources> {
320        self.resources.as_ref()
321    }
322
323    /// Return the latest stored host memory sample without performing I/O.
324    ///
325    /// Available before app setup and refreshed at the pipeline's
326    /// [`O11yPipelineBuilder::host_stats_interval`]. Reading this value does not
327    /// depend on consuming application events. A failed host refresh retains the
328    /// previous sample. Each unavailable Linux procfs source is `None`.
329    pub fn memory_stats(&self) -> Arc<MemorySnapshot> {
330        self.memory_stats_tx.borrow().clone()
331    }
332
333    /// Subscribe to stored memory samples from app or background tasks.
334    ///
335    /// The receiver starts with the current sample. Await `changed()` for later
336    /// samples and use `borrow_and_update()` to read them. Slow listeners observe
337    /// the latest sample without blocking collection. This API also works on
338    /// macOS, where snapshots contain portable memory and swap totals.
339    pub fn subscribe_memory_stats(&self) -> watch::Receiver<Arc<MemorySnapshot>> {
340        self.memory_stats_tx.subscribe()
341    }
342
343    /// Return the executor directory layout.
344    pub fn dirs(&self) -> &Dirs {
345        &self.dirs
346    }
347
348    /// Emit an event onto the application event channel.
349    pub async fn emit_event(&self, ev: Event<AppEv>) {
350        match self.sender.send(ev).await {
351            Ok(_) => {
352                debug!("event:sent");
353            }
354            Err(send_err) => {
355                error!("event:send:error: {:?}", send_err);
356            }
357        }
358    }
359
360    /// Return the host operating system identifier.
361    pub fn host_os_name(&self) -> &str {
362        &self.host.info.os_name
363    }
364    /// Return the host operating system version, when available.
365    pub fn host_os_version(&self) -> Option<String> {
366        self.host.info.os_version.clone()
367    }
368    /// Return the host architecture, when available.
369    pub fn host_architecture(&self) -> Option<String> {
370        self.host.info.architecture.clone()
371    }
372    /// Return the host name, when available.
373    pub fn host_hostname(&self) -> Option<String> {
374        self.host.info.hostname.clone()
375    }
376    /// Return the number of physical CPU cores detected at startup.
377    pub fn host_cpu_count(&self) -> usize {
378        self.host.info.ncpus
379    }
380    /// Return the number of logical CPUs detected at startup.
381    pub fn host_cpu_max(&self) -> usize {
382        self.host.info.maxcpus
383    }
384
385    /// Return the shared runtime status handle.
386    ///
387    /// Applications and background tasks should use this handle, or the
388    /// convenience methods on `Wora`, to report health and readiness changes to
389    /// the runtime supervisor.
390    pub fn status_handle(&self) -> RuntimeStatusHandle {
391        self.status.clone()
392    }
393
394    /// Report application health to the runtime supervisor.
395    pub fn report_health(&self, state: HealthState) {
396        self.status.report_health(state);
397    }
398
399    /// Report application readiness to the runtime supervisor.
400    pub fn report_readiness(&self, state: ReadinessState) {
401        self.status.report_readiness(state);
402    }
403
404    /// Mark the runtime as draining and no longer willing to accept new work.
405    pub fn begin_draining(&self) {
406        self.status.begin_draining();
407    }
408
409    /// Mark the runtime as stopping.
410    pub fn begin_stopping(&self) {
411        self.status.begin_stopping();
412    }
413
414    /// Return the latest application health state known to the runtime.
415    pub fn health_state(&self) -> HealthState {
416        self.status.health_state()
417    }
418
419    /// Return the latest application readiness state known to the runtime.
420    pub fn readiness_state(&self) -> ReadinessState {
421        self.status.readiness_state()
422    }
423
424    /// Apply a typed config or secret reload event to `app`.
425    ///
426    /// This helper is intended to be called from `App::main` when the app
427    /// receives `Event::ConfigChanged` or `Event::SecretChanged`.
428    pub async fn apply_reload_event<A>(&self, app: &mut A, fs: impl WFS + 'static, event: &Event<AppEv>) -> Result<ReloadHandling, ReloadError>
429    where
430        A: App<AppEv, AppMetric> + Send,
431    {
432        match event {
433            Event::ConfigChanged(change) => {
434                let changed_paths = (!change.paths.is_empty()).then_some(change.paths.as_slice());
435                let reload = load_config_reload::<A::AppConfig>(&self.dirs.metadata_root_dir, app.name(), fs, changed_paths).await?;
436                app.reload_config(reload)
437                    .await
438                    .map_err(|err| ReloadError::Message(format!("failed to apply config reload for {}: {}", app.name(), err)))?;
439                Ok(ReloadHandling::ConfigApplied)
440            }
441            Event::SecretChanged(change) => {
442                let changed_paths = (!change.paths.is_empty()).then_some(change.paths.as_slice());
443                let reload = load_secret_reload::<A::AppSecrets>(&self.dirs.secrets_root_dir, fs, changed_paths).await?;
444                app.reload_secrets(reload)
445                    .await
446                    .map_err(|err| ReloadError::Message(format!("failed to apply secret reload for {}: {}", app.name(), err)))?;
447                Ok(ReloadHandling::SecretsApplied)
448            }
449            _ => Ok(ReloadHandling::NotHandled),
450        }
451    }
452
453    /// Run a reload-aware event loop for event-driven applications.
454    ///
455    /// Typed config and secret reloads are applied before `handler` receives
456    /// the event.
457    pub async fn run_event_loop<A, F>(&mut self, app: &mut A, fs: impl WFS + 'static, mut handler: F) -> Result<MainRetryAction, ReloadError>
458    where
459        A: App<AppEv, AppMetric> + Send,
460        F: FnMut(&mut A, &mut Wora<AppEv, AppMetric>, Event<AppEv>) -> EventLoopAction + Send,
461    {
462        while let Some(event) = self.receiver.recv().await {
463            self.apply_reload_event(app, fs.clone(), &event).await?;
464            match handler(app, self, event) {
465                EventLoopAction::Continue => {}
466                EventLoopAction::Exit(action) => return Ok(action),
467            }
468        }
469
470        Ok(MainRetryAction::Success)
471    }
472
473    /// Sleep for `duration`, then emit `ev`.
474    pub async fn schedule_event(&self, duration: tokio::time::Duration, ev: Event<AppEv>) {
475        tokio::time::sleep(duration).await;
476        self.emit_event(ev).await
477    }
478
479    /// Spawn a periodic background task.
480    ///
481    /// The task receives a clone of the observability sender. Returning
482    /// `TaskOp::Requeue` schedules the next run; returning `TaskOp::Abort`
483    /// stops the loop.
484    /// With resource admission configured, waits for a task slot before spawning.
485    /// The caller owns this waiting future; use [`Self::try_schedule_task`] for
486    /// immediate refusal without adding a waiting producer.
487    pub async fn schedule_task<F, Fut>(&self, duration: tokio::time::Duration, future: F) -> JoinHandle<TaskOp>
488    where
489        F: Fn(Sender<O11yEvent<AppMetric>>) -> Fut + Send + 'static,
490        Fut: Future<Output = TaskOp> + Send,
491    {
492        let admission = match &self.resources {
493            Some(resources) => match resources.task().await {
494                Ok(lease) => Some(lease),
495                Err(_) => return tokio::spawn(async { TaskOp::Abort }),
496            },
497            None => None,
498        };
499        self.spawn_scheduled_task(duration, future, admission)
500    }
501
502    /// Spawn a scheduled loop only if a task slot is immediately available.
503    /// Refusal happens before spawning; admitted work holds its lease until drop.
504    pub fn try_schedule_task<F, Fut>(&self, duration: tokio::time::Duration, future: F) -> Result<JoinHandle<TaskOp>, ResourceError>
505    where
506        F: Fn(Sender<O11yEvent<AppMetric>>) -> Fut + Send + 'static,
507        Fut: Future<Output = TaskOp> + Send,
508    {
509        let admission = self.resources.as_ref().map(RuntimeResources::try_task).transpose()?;
510        Ok(self.spawn_scheduled_task(duration, future, admission))
511    }
512
513    fn spawn_scheduled_task<F, Fut>(
514        &self,
515        duration: tokio::time::Duration,
516        future: F,
517        admission: Option<tokio::sync::OwnedSemaphorePermit>,
518    ) -> JoinHandle<TaskOp>
519    where
520        F: Fn(Sender<O11yEvent<AppMetric>>) -> Fut + Send + 'static,
521        Fut: Future<Output = TaskOp> + Send,
522    {
523        let tx = self.o11y.sender().clone();
524        tokio::spawn(async move {
525            let _admission = admission;
526            loop {
527                tokio::time::sleep(duration).await;
528                match future(tx.clone()).await {
529                    TaskOp::Requeue => {
530                        trace!("wora:task action:requeue");
531                    }
532                    TaskOp::Abort => {
533                        info!("wora:task action:abort");
534                        return TaskOp::Abort;
535                    }
536                }
537            }
538        })
539    }
540}
541
542/// Result from a scheduled background task.
543#[derive(Debug, Clone)]
544pub enum TaskOp {
545    /// Continue scheduling the task.
546    Requeue,
547    /// Stop scheduling the task.
548    Abort,
549}
550
551/// Application configuration parser.
552pub trait Config {
553    /// Parsed configuration type used by the application.
554    type ConfigT: Default + Send + 'static;
555    /// Parse the main configuration file.
556    fn parse_main_config_file(data: String) -> Result<Self::ConfigT, Box<dyn std::error::Error>>;
557
558    /// Parse a supplemental configuration file.
559    fn parse_supplemental_config_file(_file_path: PathBuf, _data: String) -> Result<Self::ConfigT, Box<dyn std::error::Error>> {
560        Ok(Self::ConfigT::default())
561    }
562}
563
564/// Typed configuration file payload.
565#[derive(Clone, Debug)]
566pub struct ConfigFile<T> {
567    /// Source file path.
568    pub path: PathBuf,
569    /// Parsed configuration value.
570    pub value: T,
571}
572
573/// Typed configuration reload payload.
574#[derive(Clone, Debug)]
575pub struct ConfigReload<T> {
576    /// Parsed main config, if present in the reload set.
577    pub main: Option<T>,
578    /// Parsed supplemental config files.
579    pub supplemental: Vec<ConfigFile<T>>,
580}
581
582impl<T> Default for ConfigReload<T> {
583    fn default() -> Self {
584        Self {
585            main: None,
586            supplemental: Vec::new(),
587        }
588    }
589}
590
591/// Typed secret file payload.
592#[derive(Clone, Debug)]
593pub struct SecretFile<T> {
594    /// Source file path.
595    pub path: PathBuf,
596    /// Secret key, usually derived from the filename.
597    pub key: String,
598    /// Parsed secret value.
599    pub value: T,
600}
601
602/// Typed secret reload payload.
603#[derive(Clone, Debug)]
604pub struct SecretReload<T> {
605    /// Parsed secret files included in the reload set.
606    pub files: Vec<SecretFile<T>>,
607}
608
609impl<T> Default for SecretReload<T> {
610    fn default() -> Self {
611        Self { files: Vec::new() }
612    }
613}
614
615/// Secret parser used for initial secret load and runtime reload handling.
616pub trait Secrets {
617    /// Parsed secret value type used by the application.
618    type SecretT: Send + 'static;
619
620    /// Parse a secret file.
621    fn parse_secret_file(file_path: PathBuf, data: Vec<u8>) -> Result<Self::SecretT, Box<dyn std::error::Error>>;
622}
623
624/// Empty secret implementation for apps that do not use secrets.
625pub struct NoSecrets;
626impl Secrets for NoSecrets {
627    type SecretT = ();
628
629    fn parse_secret_file(_file_path: PathBuf, _data: Vec<u8>) -> Result<Self::SecretT, Box<dyn std::error::Error>> {
630        Ok(())
631    }
632}
633
634/// Result from applying a typed runtime reload helper.
635#[derive(Clone, Debug, PartialEq, Eq)]
636pub enum ReloadHandling {
637    /// The event was not a typed reload event.
638    NotHandled,
639    /// A typed config reload was applied.
640    ConfigApplied,
641    /// A typed secret reload was applied.
642    SecretsApplied,
643}
644
645/// Control flow returned by `Wora::run_event_loop`.
646#[derive(Clone, Debug)]
647pub enum EventLoopAction {
648    /// Keep processing events.
649    Continue,
650    /// Exit the loop with the given runner action.
651    Exit(MainRetryAction),
652}
653
654/// Empty configuration implementation for apps that do not use config files.
655pub struct NoConfig;
656impl Config for NoConfig {
657    type ConfigT = ();
658    fn parse_main_config_file(_data: String) -> Result<Self::ConfigT, Box<dyn std::error::Error>> {
659        Ok(())
660    }
661}
662#[async_trait]
663/// Application lifecycle implemented by WORA workloads.
664///
665/// Runtime health and readiness supervision is driven through
666/// [`Wora::report_health`], [`Wora::report_readiness`], or a cloned
667/// [`RuntimeStatusHandle`], not by polling an app callback.
668pub trait App<AppEv: Send + Sync + 'static, AppMetric: Send + Sync + 'static> {
669    /// Configuration parser for the app.
670    type AppConfig: Config;
671    /// Secret parser for the app.
672    type AppSecrets: Secrets;
673    /// Setup artifact type returned by `setup`.
674    type Setup;
675    /// Stable application name used for directories, locks, and logging.
676    fn name(&self) -> &'static str;
677
678    /// Return whether multiple instances may run concurrently.
679    fn allow_concurrent_executions(&self) -> bool {
680        false
681    }
682
683    /// Apply an initially loaded application configuration.
684    ///
685    /// For event-driven apps, runtime config reloads usually flow through
686    /// `reload_config` instead of this hook directly.
687    async fn configure(&mut self, _config: <Self::AppConfig as Config>::ConfigT) -> Result<(), Box<dyn std::error::Error>> {
688        Ok(())
689    }
690
691    /// Apply a typed configuration reload.
692    async fn reload_config(&mut self, reload: ConfigReload<<Self::AppConfig as Config>::ConfigT>) -> Result<(), Box<dyn std::error::Error>> {
693        if let Some(config) = reload.main {
694            self.configure(config).await?;
695        }
696        Ok(())
697    }
698
699    /// Apply a typed secret reload.
700    async fn reload_secrets(&mut self, _reload: SecretReload<<Self::AppSecrets as Secrets>::SecretT>) -> Result<(), Box<dyn std::error::Error>> {
701        Ok(())
702    }
703
704    /// Initialize application state after executor setup and before `main`.
705    async fn setup(
706        &mut self,
707        wora: &Wora<AppEv, AppMetric>,
708        exec: impl AsyncExecutor<AppEv, AppMetric>,
709        fs: impl WFS + 'static,
710        metrics: Sender<O11yEvent<AppMetric>>,
711        is_first_boot: bool,
712    ) -> Result<Self::Setup, Box<dyn std::error::Error>>;
713
714    /// Run the primary application loop.
715    async fn main(
716        &mut self,
717        wora: &mut Wora<AppEv, AppMetric>,
718        exec: impl AsyncExecutor<AppEv, AppMetric>,
719        fs: impl WFS + 'static,
720        metrics: Sender<O11yEvent<AppMetric>>,
721    ) -> MainRetryAction;
722
723    /// Clean up application state after `main` returns.
724    async fn end(
725        &mut self,
726        wora: &Wora<AppEv, AppMetric>,
727        exec: impl AsyncExecutor<AppEv, AppMetric>,
728        fs: impl WFS + 'static,
729        metrics: Sender<O11yEvent<AppMetric>>,
730    );
731}
732
733async fn configure_app_from_metadata<AppEv, AppMetric, A>(app: &mut A, wora: &Wora<AppEv, AppMetric>, fs: impl WFS + 'static) -> Result<(), MainEarlyReturn>
734where
735    AppEv: Send + Sync + 'static,
736    AppMetric: Send + Sync + 'static,
737    A: App<AppEv, AppMetric> + Send,
738{
739    match load_config_reload::<A::AppConfig>(&wora.dirs.metadata_root_dir, app.name(), fs.clone(), None).await {
740        Ok(reload) => {
741            app.reload_config(reload)
742                .instrument(tracing::info_span!("app:run:configure"))
743                .await
744                .map_err(|err| WoraSetupError::Str(format!("failed to apply config {}: {}", wora.dirs.metadata_root_dir.display(), err)))?;
745            Ok(())
746        }
747        Err(ReloadError::Vfs(VfsError::Io(err))) if err.kind() == std::io::ErrorKind::NotFound => {
748            debug!("config:init:skip path:{} error:{}", wora.dirs.metadata_root_dir.display(), err);
749            Ok(())
750        }
751        Err(ReloadError::Vfs(err)) => Err(MainEarlyReturn::Vfs(err)),
752        Err(err) => Err(WoraSetupError::Str(err.to_string()).into()),
753    }
754}
755
756async fn load_initial_secrets<AppEv, AppMetric, A>(app: &mut A, wora: &Wora<AppEv, AppMetric>, fs: impl WFS + 'static) -> Result<(), MainEarlyReturn>
757where
758    AppEv: Send + Sync + 'static,
759    AppMetric: Send + Sync + 'static,
760    A: App<AppEv, AppMetric> + Send,
761{
762    match load_secret_reload::<A::AppSecrets>(&wora.dirs.secrets_root_dir, fs.clone(), None).await {
763        Ok(reload) => {
764            if !reload.files.is_empty() {
765                app.reload_secrets(reload)
766                    .instrument(tracing::info_span!("app:run:secrets"))
767                    .await
768                    .map_err(|err| WoraSetupError::Str(format!("failed to apply secrets {}: {}", wora.dirs.secrets_root_dir.display(), err)))?;
769            }
770            Ok(())
771        }
772        Err(ReloadError::Vfs(VfsError::Io(err))) if err.kind() == std::io::ErrorKind::NotFound => {
773            debug!("secrets:init:skip path:{} error:{}", wora.dirs.secrets_root_dir.display(), err);
774            Ok(())
775        }
776        Err(ReloadError::Vfs(err)) => Err(MainEarlyReturn::Vfs(err)),
777        Err(err) => Err(WoraSetupError::Str(err.to_string()).into()),
778    }
779}
780
781fn config_change_event(app_name: &str, metadata_root: &Path, notify_event: notify::Event) -> ConfigChange {
782    let main_config_path = metadata_root.join(format!("{app_name}.toml"));
783    ConfigChange::new(ChangeKind::from_notify_kind(&notify_event.kind), main_config_path, notify_event.paths)
784}
785
786fn secret_change_event(notify_event: notify::Event) -> SecretChange {
787    SecretChange::new(ChangeKind::from_notify_kind(&notify_event.kind), notify_event.paths)
788}
789
790fn is_main_config_path(app_name: &str, path: &Path) -> bool {
791    path.file_name()
792        .and_then(|name| name.to_str())
793        .is_some_and(|name| name == format!("{app_name}.toml"))
794}
795
796async fn parse_config_file<C: Config>(path: PathBuf, fs: impl WFS + 'static, is_main: bool) -> Result<Option<ConfigFile<C::ConfigT>>, ReloadError> {
797    match fs.read_to_string(&path).await {
798        Ok(data) => {
799            let value = if is_main {
800                C::parse_main_config_file(data)
801            } else {
802                C::parse_supplemental_config_file(path.clone(), data)
803            }
804            .map_err(|err| ReloadError::Message(format!("failed to parse config {}: {}", path.display(), err)))?;
805
806            Ok(Some(ConfigFile { path, value }))
807        }
808        Err(VfsError::Io(err))
809            if matches!(
810                err.kind(),
811                std::io::ErrorKind::NotFound | std::io::ErrorKind::IsADirectory | std::io::ErrorKind::InvalidInput
812            ) =>
813        {
814            Ok(None)
815        }
816        Err(err) => Err(err.into()),
817    }
818}
819
820async fn metadata_paths(fs: impl WFS + 'static, metadata_root: &PathBuf) -> Result<Vec<PathBuf>, ReloadError> {
821    fs.list_dir(metadata_root).await.map_err(ReloadError::from)
822}
823
824async fn secret_paths(fs: impl WFS + 'static, secrets_root: &PathBuf) -> Result<Vec<PathBuf>, ReloadError> {
825    fs.list_dir(secrets_root).await.map_err(ReloadError::from)
826}
827
828async fn load_config_reload<C>(
829    metadata_root: &PathBuf,
830    app_name: &str,
831    fs: impl WFS + 'static,
832    changed_paths: Option<&[PathBuf]>,
833) -> Result<ConfigReload<C::ConfigT>, ReloadError>
834where
835    C: Config,
836{
837    let mut reload = ConfigReload::default();
838    let paths = match changed_paths {
839        Some(paths) => paths.to_vec(),
840        None => metadata_paths(fs.clone(), metadata_root).await?,
841    };
842
843    for path in paths {
844        let is_main = is_main_config_path(app_name, &path);
845        if let Some(parsed) = parse_config_file::<C>(path.clone(), fs.clone(), is_main).await? {
846            if is_main {
847                reload.main = Some(parsed.value);
848            } else {
849                reload.supplemental.push(parsed);
850            }
851        }
852    }
853
854    Ok(reload)
855}
856
857async fn load_secret_reload<S>(
858    secrets_root: &PathBuf,
859    fs: impl WFS + 'static,
860    changed_paths: Option<&[PathBuf]>,
861) -> Result<SecretReload<S::SecretT>, ReloadError>
862where
863    S: Secrets,
864{
865    let mut reload = SecretReload::default();
866    let paths = match changed_paths {
867        Some(paths) => paths.to_vec(),
868        None => secret_paths(fs.clone(), secrets_root).await?,
869    };
870
871    for path in paths {
872        let key = match path.file_name().and_then(|value| value.to_str()) {
873            Some(key) => key.to_string(),
874            None => continue,
875        };
876
877        match fs.read(&path).await {
878            Ok(data) => {
879                let value = S::parse_secret_file(path.clone(), data)
880                    .map_err(|err| ReloadError::Message(format!("failed to parse secret {}: {}", path.display(), err)))?;
881                reload.files.push(SecretFile { path, key, value });
882            }
883            Err(VfsError::Io(err))
884                if matches!(
885                    err.kind(),
886                    std::io::ErrorKind::NotFound | std::io::ErrorKind::IsADirectory | std::io::ErrorKind::InvalidInput
887                ) => {}
888            Err(err) => return Err(err.into()),
889        }
890    }
891
892    Ok(reload)
893}
894
895enum ShutdownReason {
896    External,
897    Unhealthy,
898}
899
900enum RuntimeSupervisionEvent {
901    ShutdownRequested(ShutdownReason, Option<chrono::NaiveDateTime>),
902}
903
904async fn wait_for_ready_notification<AppEv, AppMetric, E, F>(
905    exec: E,
906    app_name: String,
907    dirs: Dirs,
908    fs: F,
909    mut readiness_rx: watch::Receiver<ReadinessState>,
910) -> Result<(), SetupFailure>
911where
912    AppEv: Send + Sync + 'static,
913    AppMetric: Send + Sync + 'static,
914    E: AsyncExecutor<AppEv, AppMetric>,
915    F: WFS + 'static,
916{
917    if *readiness_rx.borrow() == ReadinessState::Ready {
918        return exec.on_runtime_ready(&app_name, &dirs, fs).await;
919    }
920
921    loop {
922        if readiness_rx.changed().await.is_err() {
923            return Ok(());
924        }
925        if *readiness_rx.borrow() == ReadinessState::Ready {
926            return exec.on_runtime_ready(&app_name, &dirs, fs).await;
927        }
928    }
929}
930
931fn runtime_metrics_snapshot<AppEv, AppMetric>(wora: &Wora<AppEv, AppMetric>, restart_count: u32) -> RuntimeMetrics
932where
933    AppEv: Send + Sync + 'static,
934    AppMetric: Send + Sync + 'static,
935{
936    RuntimeMetrics {
937        app_name: wora.app_name.clone(),
938        pid: std::process::id(),
939        leadership: wora.leadership.clone(),
940        health: wora.health_state(),
941        readiness: wora.readiness_state(),
942        restart_count,
943        event_backlog_capacity: wora.sender.capacity(),
944        event_backlog_max_capacity: wora.sender.max_capacity(),
945    }
946}
947
948#[allow(clippy::too_many_arguments)]
949async fn run_app_main_with_restart_policy<AppEv, AppMetric, A, E, F>(
950    app: &mut A,
951    wora: &mut Wora<AppEv, AppMetric>,
952    exec: E,
953    fs: F,
954    metrics_sender: Sender<O11yEvent<AppMetric>>,
955    restart_options: RestartPolicyOptions,
956    supervision_rx: &mut Receiver<RuntimeSupervisionEvent>,
957    restart_counter: Arc<AtomicU32>,
958) -> Result<(), MainEarlyReturn>
959where
960    AppEv: Send + Sync + 'static,
961    AppMetric: Send + Sync + 'static,
962    A: App<AppEv, AppMetric>,
963    E: AsyncExecutor<AppEv, AppMetric> + Clone,
964    F: WFS + 'static,
965{
966    let mut retry_count = 0u32;
967
968    loop {
969        let status_handle = wora.status_handle();
970        let app_event_sender = wora.sender.clone();
971        let app_name = wora.app_name.clone();
972        let runtime_dirs = wora.dirs.clone();
973        status_handle.report_health(HealthState::Unknown);
974        restart_counter.store(retry_count, Ordering::Relaxed);
975        let _ = metrics_sender
976            .send(o11y_new_ev_runtime_metrics(&runtime_metrics_snapshot(wora, retry_count)))
977            .await;
978
979        let main_future = app
980            .main(wora, exec.clone(), fs.clone(), metrics_sender.clone())
981            .instrument(tracing::info_span!("app:run:main", retry = retry_count));
982        tokio::pin!(main_future);
983
984        let mut shutdown_deadline: Option<Pin<Box<tokio::time::Sleep>>> = None;
985        let mut drain_deadline: Option<Pin<Box<tokio::time::Sleep>>> = None;
986        let mut shutdown_reason = ShutdownReason::External;
987        let mut shutdown_timestamp = None;
988        let mut draining_started = false;
989        let mut stopping_started = false;
990
991        let main_result = loop {
992            tokio::select! {
993                result = &mut main_future => break result,
994                Some(event) = supervision_rx.recv() => {
995                    let RuntimeSupervisionEvent::ShutdownRequested(reason, timestamp) = event;
996                    if !draining_started {
997                        draining_started = true;
998                        shutdown_reason = reason;
999                        shutdown_timestamp = timestamp;
1000                        status_handle.begin_draining();
1001                        exec.on_runtime_draining(&app_name, &runtime_dirs, fs.clone()).await?;
1002                        if restart_options.supervision.drain_grace_period.is_zero() {
1003                            if !stopping_started {
1004                                stopping_started = true;
1005                                status_handle.begin_stopping();
1006                                shutdown_deadline = Some(Box::pin(tokio::time::sleep(restart_options.supervision.shutdown_grace_period)));
1007                                let _ = app_event_sender.send(Event::Control(ControlEvent::Shutdown(timestamp))).await;
1008                            }
1009                        } else {
1010                            drain_deadline = Some(Box::pin(tokio::time::sleep(restart_options.supervision.drain_grace_period)));
1011                        }
1012                    }
1013                }
1014                _ = async {
1015                    if let Some(deadline) = drain_deadline.as_mut() {
1016                        deadline.await;
1017                    }
1018                }, if drain_deadline.is_some() => {
1019                    drain_deadline = None;
1020                    if !stopping_started {
1021                        stopping_started = true;
1022                        status_handle.begin_stopping();
1023                        shutdown_deadline = Some(Box::pin(tokio::time::sleep(restart_options.supervision.shutdown_grace_period)));
1024                        let _ = app_event_sender.send(Event::Control(ControlEvent::Shutdown(shutdown_timestamp))).await;
1025                    }
1026                }
1027                _ = async {
1028                    if let Some(deadline) = shutdown_deadline.as_mut() {
1029                        deadline.await;
1030                    }
1031                }, if shutdown_deadline.is_some() => {
1032                    match shutdown_reason {
1033                        ShutdownReason::External => {
1034                            break MainRetryAction::UseExitCode(restart_options.supervision.forced_shutdown_exit_code);
1035                        }
1036                        ShutdownReason::Unhealthy => {
1037                            break match restart_options.supervision.unhealthy_action {
1038                                UnhealthyAction::Ignore => MainRetryAction::UseExitCode(restart_options.supervision.forced_shutdown_exit_code),
1039                                UnhealthyAction::RequestShutdown => MainRetryAction::UseExitCode(restart_options.supervision.forced_shutdown_exit_code),
1040                                UnhealthyAction::UseRestartPolicy => MainRetryAction::UseRestartPolicy,
1041                            };
1042                        }
1043                    }
1044                }
1045            }
1046        };
1047
1048        match main_result {
1049            MainRetryAction::UseExitCode(ec) => return Err(MainEarlyReturn::UseExitCode(ec)),
1050            MainRetryAction::Success => return Ok(()),
1051            MainRetryAction::UseRestartPolicy => match restart_options.policy {
1052                WorkloadRestartPolicy::ExitWithWorkloadReturn => return Err(MainEarlyReturn::UseExitCode(1)),
1053                WorkloadRestartPolicy::RetryInstantly => {
1054                    if !restart_options.can_retry(retry_count) {
1055                        return Err(MainEarlyReturn::UseExitCode(1));
1056                    }
1057                    retry_count = retry_count.saturating_add(1);
1058                    restart_counter.store(retry_count, Ordering::Relaxed);
1059                    info!("app:run:restart policy:retry_instantly retry:{}", retry_count);
1060                }
1061                WorkloadRestartPolicy::RetryPause => {
1062                    if !restart_options.can_retry(retry_count) {
1063                        return Err(MainEarlyReturn::UseExitCode(1));
1064                    }
1065                    retry_count = retry_count.saturating_add(1);
1066                    restart_counter.store(retry_count, Ordering::Relaxed);
1067                    let pause = restart_options.pause_for_retry(retry_count);
1068                    info!("app:run:restart policy:retry_pause retry:{} pause:{:?}", retry_count, pause);
1069                    tokio::time::sleep(pause).await;
1070                }
1071                WorkloadRestartPolicy::ExponentialBackoff => {
1072                    if !restart_options.can_retry(retry_count) {
1073                        return Err(MainEarlyReturn::UseExitCode(1));
1074                    }
1075                    retry_count = retry_count.saturating_add(1);
1076                    restart_counter.store(retry_count, Ordering::Relaxed);
1077                    let pause = restart_options.pause_for_retry(retry_count);
1078                    info!("app:run:restart policy:exponential_backoff retry:{} pause:{:?}", retry_count, pause);
1079                    tokio::time::sleep(pause).await;
1080                }
1081            },
1082        }
1083    }
1084}
1085
1086// TODOs
1087// - create a non-file locking variant
1088
1089/// Options used by the async runner.
1090///
1091/// This is the preferred extension point for app-typed runtime policies such
1092/// as signal mapping and for runner infrastructure choices such as lock
1093/// backend, restart behavior, boot-state location, and telemetry provider.
1094#[derive(Clone)]
1095pub struct RunnerOptions<AppEv, L = ProcLockBackend, R = SystemRuntimeEnvironment> {
1096    /// Optional shared count admission; `None` preserves legacy scheduling/counts.
1097    pub resources: Option<RuntimeResources>,
1098    /// Optional boot-state directory. Defaults to WORA's host temp boot root.
1099    pub boot_dir: Option<PathBuf>,
1100    /// Restart and supervision behavior.
1101    pub restart: RestartPolicyOptions,
1102    /// Translate platform signals into application/runtime events.
1103    ///
1104    /// The default mapper turns `SIGHUP` into
1105    /// `ControlEvent::ReloadConfiguration`, `SIGUSR1` into
1106    /// `ControlEvent::LogRotation`, and termination signals into shutdown.
1107    /// The runner supervises shutdown; reload/log-rotation controls are
1108    /// delivered to the application.
1109    pub signal_mapper: SignalMapper<AppEv>,
1110    /// Lock backend used to serialize workload execution.
1111    pub lock_backend: L,
1112    /// Runtime environment provider used for host and process telemetry.
1113    pub runtime_environment: R,
1114}
1115
1116impl<AppEv: Send + Sync + 'static> Default for RunnerOptions<AppEv> {
1117    fn default() -> Self {
1118        Self {
1119            resources: None,
1120            boot_dir: None,
1121            restart: RestartPolicyOptions::default(),
1122            signal_mapper: default_signal_mapper(),
1123            lock_backend: ProcLockBackend,
1124            runtime_environment: SystemRuntimeEnvironment::default(),
1125        }
1126    }
1127}
1128
1129impl<AppEv: Send + Sync + 'static> RunnerOptions<AppEv> {
1130    /// Create default runner options.
1131    pub fn new() -> Self {
1132        Self::default()
1133    }
1134}
1135
1136impl<AppEv: Send + Sync + 'static, L, R> RunnerOptions<AppEv, L, R> {
1137    /// Apply validated queue/task/sink/service/provider count admission.
1138    /// Reuse clones of this handle across replacement runners. These counts do
1139    /// not impose payload-byte, custom-provider heap or CPU-time ceilings.
1140    pub fn with_resources(mut self, resources: RuntimeResources) -> Self {
1141        self.resources = Some(resources);
1142        self
1143    }
1144
1145    /// Set the boot-state directory.
1146    pub fn with_boot_dir(mut self, boot_dir: impl Into<PathBuf>) -> Self {
1147        self.boot_dir = Some(boot_dir.into());
1148        self
1149    }
1150
1151    /// Set restart and supervision behavior.
1152    pub fn with_restart_options(mut self, restart: RestartPolicyOptions) -> Self {
1153        self.restart = restart;
1154        self
1155    }
1156
1157    /// Set a custom platform signal mapper.
1158    pub fn with_signal_mapper(mut self, mapper: impl Fn(RuntimeSignal) -> Option<Event<AppEv>> + Send + Sync + 'static) -> Self {
1159        self.signal_mapper = Arc::new(mapper);
1160        self
1161    }
1162
1163    /// Set the lock backend used to serialize workload execution.
1164    pub fn with_lock_backend<NewL>(self, lock_backend: NewL) -> RunnerOptions<AppEv, NewL, R> {
1165        RunnerOptions {
1166            resources: self.resources,
1167            boot_dir: self.boot_dir,
1168            restart: self.restart,
1169            signal_mapper: self.signal_mapper,
1170            lock_backend,
1171            runtime_environment: self.runtime_environment,
1172        }
1173    }
1174
1175    /// Set the runtime environment provider used for telemetry.
1176    pub fn with_runtime_environment<NewR>(self, runtime_environment: NewR) -> RunnerOptions<AppEv, L, NewR> {
1177        RunnerOptions {
1178            resources: self.resources,
1179            boot_dir: self.boot_dir,
1180            restart: self.restart,
1181            signal_mapper: self.signal_mapper,
1182            lock_backend: self.lock_backend,
1183            runtime_environment,
1184        }
1185    }
1186}
1187
1188/// Run apps via an `async` based executor with default [`RunnerOptions`].
1189///
1190/// The executor must create `metadata_root_dir` and `secrets_root_dir` during
1191/// setup, even for apps using [`NoConfig`] or [`NoSecrets`]. The runner installs
1192/// watchers on both roots after setup. The default filesystem lock backend
1193/// creates the parent of its lock path before executor setup.
1194pub async fn exec_async_runner<AppEv: Send + Sync + 'static, AppMetric: Debug + Send + Sync + 'static>(
1195    exec: impl AsyncExecutor<AppEv, AppMetric> + 'static,
1196    app: impl App<AppEv, AppMetric> + Send + 'static,
1197    fs: impl WFS + 'static,
1198    o11y: O11yPipeline<AppMetric>,
1199) -> Result<(), MainEarlyReturn> {
1200    exec_async_runner_with_options(exec, app, fs, o11y, RunnerOptions::default()).await
1201}
1202
1203/// Run apps with an owned observability pipeline and explicit runner options.
1204///
1205/// The runner supervises the application, observability processor, and
1206/// exporter services as one lifecycle. Infrastructure failures request a
1207/// graceful application shutdown and are returned as
1208/// [`MainEarlyReturn::InfrastructureTask`].
1209pub async fn exec_async_runner_with_options<AppEv: Send + Sync + 'static, AppMetric: Debug + Send + Sync + 'static, L: LockBackend, R: RuntimeEnvironment>(
1210    exec: impl AsyncExecutor<AppEv, AppMetric> + 'static,
1211    app: impl App<AppEv, AppMetric> + Send + 'static,
1212    fs: impl WFS + 'static,
1213    o11y: O11yPipeline<AppMetric>,
1214    options: RunnerOptions<AppEv, L, R>,
1215) -> Result<(), MainEarlyReturn> {
1216    let parts = o11y.into_parts();
1217    let pipeline_admission = options
1218        .resources
1219        .as_ref()
1220        .map(|resources| {
1221            resources.pipeline(
1222                parts.processor.sink_count(),
1223                parts.services.len(),
1224                parts.options.sender().max_capacity(),
1225                parts.commands.max_capacity(),
1226            )
1227        })
1228        .transpose()?
1229        .map(Arc::new);
1230    let initial_provider_admission = options.resources.as_ref().map(RuntimeResources::try_provider).transpose()?;
1231    parts.started_tx.send_replace(true);
1232    let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(None::<String>);
1233    let processor_admission = pipeline_admission.clone();
1234    let mut processor_task = tokio::spawn(async move {
1235        let _admission = processor_admission;
1236        parts.processor.run_controlled(parts.receiver, parts.commands, parts.settings_tx).await
1237    });
1238    let mut service_tasks = tokio::task::JoinSet::new();
1239    for service in parts.services {
1240        let admission = pipeline_admission.clone();
1241        let statuses = parts.service_statuses.clone();
1242        set_o11y_service_state(&statuses, &service.name, crate::o11y::O11yServiceState::Running);
1243        service_tasks.spawn(async move {
1244            let _admission = admission;
1245            let result = service.future.await;
1246            let state = match &result {
1247                Ok(()) => crate::o11y::O11yServiceState::Stopped,
1248                Err(error) => crate::o11y::O11yServiceState::Failed(error.clone()),
1249            };
1250            set_o11y_service_state(&statuses, &service.name, state);
1251            (service.name, result)
1252        });
1253    }
1254    let runner = exec_async_runner_inner(
1255        exec,
1256        app,
1257        fs,
1258        parts.options,
1259        options,
1260        Some(parts.settings_rx),
1261        Some(shutdown_rx),
1262        initial_provider_admission,
1263    );
1264    tokio::pin!(runner);
1265
1266    let mut processor_finished = false;
1267    let mut infrastructure_error = None;
1268    let runner_result = tokio::select! {
1269        result = &mut runner => result,
1270        result = &mut processor_task => {
1271            processor_finished = true;
1272            infrastructure_error = Some(match result {
1273                Ok(Ok(())) => "observability processor stopped unexpectedly".to_string(),
1274                Ok(Err(error)) => format!("observability processor failed: {error}"),
1275                Err(error) => format!("observability processor join failed: {error}"),
1276            });
1277            shutdown_tx.send_replace(infrastructure_error.clone());
1278            runner.await
1279        },
1280        result = service_tasks.join_next(), if !service_tasks.is_empty() => {
1281            infrastructure_error = Some(service_task_error(result));
1282            shutdown_tx.send_replace(infrastructure_error.clone());
1283            runner.await
1284        },
1285    };
1286
1287    if !processor_finished {
1288        match tokio::time::timeout(std::time::Duration::from_secs(10), &mut processor_task).await {
1289            Ok(Ok(Ok(()))) => {}
1290            Ok(Ok(Err(error))) => {
1291                infrastructure_error.get_or_insert_with(|| format!("observability processor failed: {error}"));
1292            }
1293            Ok(Err(error)) => {
1294                infrastructure_error.get_or_insert_with(|| format!("observability processor join failed: {error}"));
1295            }
1296            Err(_) => {
1297                processor_task.abort();
1298                let _ = (&mut processor_task).await;
1299                infrastructure_error.get_or_insert_with(|| "observability processor shutdown timed out".to_string());
1300            }
1301        };
1302    }
1303
1304    while let Some(result) = service_tasks.try_join_next() {
1305        infrastructure_error.get_or_insert_with(|| service_task_error(Some(result)));
1306    }
1307    service_tasks.abort_all();
1308    while service_tasks.join_next().await.is_some() {}
1309    let mut statuses = parts.service_statuses.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1310    for status in statuses.values_mut() {
1311        if status.state == crate::o11y::O11yServiceState::Running {
1312            status.state = crate::o11y::O11yServiceState::Stopped;
1313        }
1314    }
1315    parts.started_tx.send_replace(false);
1316
1317    match infrastructure_error {
1318        Some(error) => Err(MainEarlyReturn::InfrastructureTask(error)),
1319        None => runner_result,
1320    }
1321}
1322
1323fn set_o11y_service_state(
1324    statuses: &std::sync::Arc<std::sync::Mutex<std::collections::HashMap<String, crate::o11y::O11yServiceStatus>>>,
1325    name: &str,
1326    state: crate::o11y::O11yServiceState,
1327) {
1328    let mut statuses = statuses.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1329    if let Some(status) = statuses.get_mut(name) {
1330        status.state = state;
1331    }
1332}
1333
1334type O11yServiceTaskResult = Result<(String, Result<(), String>), tokio::task::JoinError>;
1335
1336fn publish_memory_snapshot<AppEv>(stats: &HostStats, cache: &watch::Sender<Arc<MemorySnapshot>>, events: &Sender<Event<AppEv>>) {
1337    let snapshot = Arc::new(MemorySnapshot::from_host_stats(stats));
1338    cache.send_replace(snapshot.clone());
1339    if let Err(error) = events.try_send(Event::SystemResource(SystemResourceStat::MemorySnapshot(snapshot))) {
1340        debug!(%error, "periodic memory event skipped");
1341    }
1342}
1343
1344fn service_task_error(result: Option<O11yServiceTaskResult>) -> String {
1345    match result {
1346        Some(Ok((name, Ok(())))) => format!("observability service {name} stopped unexpectedly"),
1347        Some(Ok((name, Err(error)))) => format!("observability service {name} failed: {error}"),
1348        Some(Err(error)) => format!("observability service join failed: {error}"),
1349        None => "observability service set stopped unexpectedly".to_string(),
1350    }
1351}
1352
1353#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
1354async fn exec_async_runner_inner<AppEv: Send + Sync + 'static, AppMetric: Debug + Send + Sync + 'static, L: LockBackend, R: RuntimeEnvironment>(
1355    exec: impl AsyncExecutor<AppEv, AppMetric> + 'static,
1356    app: impl App<AppEv, AppMetric> + Send + 'static,
1357    fs: impl WFS + 'static,
1358    o11y: O11yProcessorOptions<AppMetric>,
1359    options: RunnerOptions<AppEv, L, R>,
1360    runtime_settings: Option<tokio::sync::watch::Receiver<O11yRuntimeSettings>>,
1361    infrastructure_shutdown: Option<tokio::sync::watch::Receiver<Option<String>>>,
1362    initial_provider_admission: Option<tokio::sync::OwnedSemaphorePermit>,
1363) -> Result<(), MainEarlyReturn> {
1364    let RunnerOptions {
1365        resources,
1366        boot_dir,
1367        restart,
1368        signal_mapper,
1369        lock_backend,
1370        runtime_environment,
1371    } = options;
1372    let mut exec = exec;
1373    let mut app = app;
1374    let mut lock_path = PathBuf::new();
1375    lock_path.push(&exec.dirs().runtime_root_dir);
1376    let lock_fp = lock_file_name(app.name(), app.allow_concurrent_executions());
1377
1378    lock_path.push(&lock_fp);
1379
1380    let o11y_tx = o11y.sender().clone();
1381    let initial_runtime_settings = O11yRuntimeSettings {
1382        flush_interval: *o11y.flush_interval(),
1383        status_interval: *o11y.status_interval(),
1384        host_stats_interval: *o11y.host_stats_interval(),
1385        failure_policy: O11yFailurePolicy::Isolate,
1386    };
1387    let (static_settings_tx, runtime_settings) = match runtime_settings {
1388        Some(receiver) => (None, receiver),
1389        None => {
1390            let (sender, receiver) = tokio::sync::watch::channel(initial_runtime_settings);
1391            (Some(sender), receiver)
1392        }
1393    };
1394    let _static_settings_tx = static_settings_tx;
1395
1396    let _ = o11y_tx.send(o11y_new_ev_init(exec.dirs().log_root_dir.clone())).await;
1397
1398    match lock_backend.try_lock(&lock_path) {
1399        Ok(lock_guard) => {
1400            info!("exec:run:lock_file created:{:?}", &lock_path);
1401
1402            let result = async {
1403                let metrics_sender = o11y.sender().clone();
1404                let restart_counter = Arc::new(AtomicU32::new(0));
1405
1406                let app_capacity = resources.as_ref().map_or(EVENT_BUFFER_SIZE, |resources| resources.limits().app_event_records);
1407                let dispatch_capacity = resources
1408                    .as_ref()
1409                    .map_or(EVENT_BUFFER_SIZE, |resources| resources.limits().runtime_event_records);
1410                let supervision_capacity = resources.as_ref().map_or(EVENT_BUFFER_SIZE, |resources| resources.limits().supervision_records);
1411                let mut wora = Wora::new_with_runtime_environment(exec.dirs(), app.name().to_string(), app_capacity, o11y, runtime_environment.clone())?;
1412                wora.resources = resources.clone();
1413                let mut health_rx = wora.status_handle().subscribe_health();
1414                let readiness_rx = wora.status_handle().subscribe_readiness();
1415                let (runtime_event_tx, mut runtime_event_rx) = channel::<Event<AppEv>>(dispatch_capacity);
1416                let (supervision_tx, mut supervision_rx) = channel::<RuntimeSupervisionEvent>(supervision_capacity);
1417                let mut runtime_tasks = RuntimeTaskRegistry::new();
1418                if let Some(mut shutdown) = infrastructure_shutdown {
1419                    let shutdown_event_sender = runtime_event_tx.clone();
1420                    let shutdown_supervision_sender = supervision_tx.clone();
1421                    let cancelled = runtime_tasks.cancellation_token();
1422                    runtime_tasks.spawn("infrastructure shutdown", async move {
1423                        loop {
1424                            if shutdown.borrow().is_some() {
1425                                let timestamp = Some(Utc::now().naive_utc());
1426                                let _ = shutdown_event_sender.send(Event::Control(ControlEvent::Shutdown(timestamp))).await;
1427                                let _ = shutdown_supervision_sender
1428                                    .send(RuntimeSupervisionEvent::ShutdownRequested(ShutdownReason::External, timestamp))
1429                                    .await;
1430                                break;
1431                            }
1432                            tokio::select! {
1433                                _ = cancelled.cancelled() => break,
1434                                changed = shutdown.changed() => if changed.is_err() { break },
1435                            }
1436                        }
1437                        Ok(())
1438                    });
1439                }
1440                let dispatch_app_sender = wora.sender.clone();
1441                let dispatch_supervision_sender = supervision_tx.clone();
1442                let cancelled = runtime_tasks.cancellation_token();
1443                runtime_tasks.spawn("runtime event dispatch", async move {
1444                    loop {
1445                        let event = tokio::select! {
1446                            _ = cancelled.cancelled() => break,
1447                            event = runtime_event_rx.recv() => match event { Some(event) => event, None => break },
1448                        };
1449                        if let Event::Control(ControlEvent::Shutdown(timestamp)) = &event {
1450                            let _ = dispatch_supervision_sender
1451                                .send(RuntimeSupervisionEvent::ShutdownRequested(ShutdownReason::External, *timestamp))
1452                                .await;
1453                        }
1454
1455                        if dispatch_app_sender.send(event).await.is_err() {
1456                            break;
1457                        }
1458                    }
1459                    Ok(())
1460                });
1461
1462                let health_supervision_sender = supervision_tx.clone();
1463                let health_event_sender = wora.sender.clone();
1464                let unhealthy_action = restart.supervision.unhealthy_action.clone();
1465                let cancelled = runtime_tasks.cancellation_token();
1466                runtime_tasks.spawn("health supervision", async move {
1467                    loop {
1468                        tokio::select! {
1469                            _ = cancelled.cancelled() => break,
1470                            result = health_rx.changed() => if result.is_err() { break },
1471                        }
1472
1473                        if *health_rx.borrow() == HealthState::Failed && !matches!(unhealthy_action, UnhealthyAction::Ignore) {
1474                            let timestamp = Some(Utc::now().naive_utc());
1475                            let _ = health_event_sender.send(Event::Control(ControlEvent::Shutdown(timestamp))).await;
1476                            let _ = health_supervision_sender
1477                                .send(RuntimeSupervisionEvent::ShutdownRequested(ShutdownReason::Unhealthy, timestamp))
1478                                .await;
1479                        }
1480                    }
1481                    Ok(())
1482                });
1483
1484                let runtime_event_tasks = exec
1485                    .spawn_runtime_event_sources_with_signal_mapper(runtime_event_tx.clone(), signal_mapper.clone())
1486                    .instrument(tracing::info_span!("exec:run:event_sources"))
1487                    .await?;
1488                for task in runtime_event_tasks {
1489                    runtime_tasks.adopt("executor event source", task);
1490                }
1491
1492                let _ = metrics_sender.send(o11y_new_ev_hostinfo(wora.host.info())).await;
1493                let _ = metrics_sender
1494                    .send(o11y_new_ev_runtime_metrics(&runtime_metrics_snapshot(
1495                        &wora,
1496                        restart_counter.load(Ordering::Relaxed),
1497                    )))
1498                    .await;
1499                if let Some(process_stats) = runtime_environment.initial_process_stats() {
1500                    let _ = metrics_sender.send(o11y_new_ev_processstats(&process_stats)).await;
1501                }
1502                drop(initial_provider_admission);
1503
1504                let status_tx = wora.o11y.sender().clone();
1505                let cancelled = runtime_tasks.cancellation_token();
1506                let mut status_settings = runtime_settings.clone();
1507                runtime_tasks.spawn("status reporting", async move {
1508                    loop {
1509                        let interval = status_settings.borrow().status_interval;
1510                        tokio::select! {
1511                            _ = cancelled.cancelled() => break,
1512                            _ = tokio::time::sleep(interval) => {
1513                                let _ = status_tx.send(o11y_new_ev_status(status_tx.capacity(), status_tx.max_capacity())).await;
1514                            },
1515                            changed = status_settings.changed() => if changed.is_err() { break },
1516                        }
1517                    }
1518                    Ok(())
1519                });
1520
1521                let flush_tx = wora.o11y.sender().clone();
1522                let cancelled = runtime_tasks.cancellation_token();
1523                let mut flush_settings = runtime_settings.clone();
1524                runtime_tasks.spawn("observability flushing", async move {
1525                    loop {
1526                        let interval = flush_settings.borrow().flush_interval;
1527                        tokio::select! {
1528                            _ = cancelled.cancelled() => break,
1529                            _ = tokio::time::sleep(interval) => { let _ = flush_tx.send(o11y_new_ev_flush()).await; },
1530                            changed = flush_settings.changed() => if changed.is_err() { break },
1531                        }
1532                    }
1533                    Ok(())
1534                });
1535
1536                let runtime_status = wora.status_handle();
1537                let runtime_app_name = wora.app_name.clone();
1538                let runtime_leadership = wora.leadership.clone();
1539                let runtime_metrics_restart_counter = restart_counter.clone();
1540                let runtime_environment = runtime_environment.clone();
1541                let collector_resources = resources.clone();
1542                let runtime_event_sender = wora.sender.clone();
1543                let host_stats_tx = wora.o11y.sender().clone();
1544                let memory_stats_tx = wora.memory_stats_tx.clone();
1545                let cancelled = runtime_tasks.cancellation_token();
1546                let mut host_stats_settings = runtime_settings.clone();
1547                runtime_tasks.spawn("host and process sampling", async move {
1548                    loop {
1549                        let interval = host_stats_settings.borrow().host_stats_interval;
1550                        tokio::select! {
1551                            _ = cancelled.cancelled() => break,
1552                            _ = tokio::time::sleep(interval) => {},
1553                            changed = host_stats_settings.changed() => {
1554                                if changed.is_err() { break; }
1555                                continue;
1556                            },
1557                        }
1558                        let tx = host_stats_tx.clone();
1559                        let runtime_status = runtime_status.clone();
1560                        let runtime_app_name = runtime_app_name.clone();
1561                        let runtime_leadership = runtime_leadership.clone();
1562                        let runtime_metrics_restart_counter = runtime_metrics_restart_counter.clone();
1563                        let runtime_environment = runtime_environment.clone();
1564                        let runtime_event_sender = runtime_event_sender.clone();
1565                        let admission = match &collector_resources {
1566                            Some(resources) => tokio::select! {
1567                                _ = cancelled.cancelled() => break,
1568                                lease = resources.provider() => Some(lease.map_err(|error| error.to_string())?),
1569                            },
1570                            None => None,
1571                        };
1572                        let worker = tokio::task::spawn_blocking(move || {
1573                            let _admission = admission;
1574                            (runtime_environment.refresh_host_stats(), runtime_environment.refresh_process_stats())
1575                        });
1576                        let (host_stats, process_stats) = worker.await.map_err(|err| format!("host and process sampler: {err}"))?;
1577                        let host_stats = match host_stats {
1578                            Ok(stats) => stats,
1579                            Err(err) => {
1580                                error!("o11y:host stats refresh error: {}", err);
1581                                None
1582                            }
1583                        };
1584                        if let Some(host_stats) = host_stats {
1585                            publish_memory_snapshot(&host_stats, &memory_stats_tx, &runtime_event_sender);
1586                            let _ = tx.send(crate::o11y::o11y_new_ev_hoststats_owned(host_stats)).await;
1587                        }
1588
1589                        if let Some(process_stats) = process_stats {
1590                            let _ = tx.send(o11y_new_ev_processstats(&process_stats)).await;
1591                        }
1592
1593                        let runtime_metrics = RuntimeMetrics {
1594                            app_name: runtime_app_name.clone(),
1595                            pid: std::process::id(),
1596                            leadership: runtime_leadership.clone(),
1597                            health: runtime_status.health_state(),
1598                            readiness: runtime_status.readiness_state(),
1599                            restart_count: runtime_metrics_restart_counter.load(Ordering::Relaxed),
1600                            event_backlog_capacity: runtime_event_sender.capacity(),
1601                            event_backlog_max_capacity: runtime_event_sender.max_capacity(),
1602                        };
1603                        let _ = tx.send(o11y_new_ev_runtime_metrics(&runtime_metrics)).await;
1604                    }
1605                    Ok(())
1606                });
1607
1608                let boot_state = resolve_boot_state(fs.clone(), boot_dir.unwrap_or_else(default_boot_root), app.name())
1609                    .await
1610                    .map_err(WoraSetupError::from)?;
1611                let is_first_boot = boot_state.is_first_boot();
1612                debug!("boot:state app:{} state:{:?}", app.name(), boot_state);
1613
1614                exec.setup(&wora, fs.clone()).instrument(tracing::info_span!("exec:run:setup")).await?;
1615
1616                configure_app_from_metadata(&mut app, &wora, fs.clone()).await?;
1617                load_initial_secrets(&mut app, &wora, fs.clone()).await?;
1618
1619                let mut rc = Err(MainEarlyReturn::UseExitCode(1));
1620
1621                match app
1622                    .setup(&wora, exec.clone(), fs.clone(), metrics_sender.clone(), is_first_boot)
1623                    .instrument(tracing::info_span!("app:run:setup"))
1624                    .await
1625                {
1626                    Ok(_) => {
1627                        trace!("checking executor directories exist...");
1628
1629                        for dir in [
1630                            &wora.dirs.root_dir,
1631                            &wora.dirs.log_root_dir,
1632                            &wora.dirs.metadata_root_dir,
1633                            &wora.dirs.data_root_dir,
1634                            &wora.dirs.cache_root_dir,
1635                        ] {
1636                            if !fs.dir_exists(dir).await? {
1637                                error!("directory {:?} does not exist", dir);
1638                                return Err(MainEarlyReturn::WoraSetup(WoraSetupError::DirectoryDoesNotExistOnFilesystem(dir.clone())));
1639                            }
1640                        }
1641
1642                        info!(
1643                            host.hostname = wora.host_hostname(),
1644                            host.platform = wora.host_architecture(),
1645                            host.os_name = wora.host_os_name(),
1646                            host.os_version = wora.host_os_version(),
1647                            host.cpu_count = wora.host_cpu_count(),
1648                            host.cpu_max = wora.host_cpu_max(),
1649                        );
1650                        info!("dirs.root: {:?}", wora.dirs.root_dir);
1651                        info!("dirs.log: {:?}", wora.dirs.log_root_dir);
1652                        info!("dirs.metadata: {:?}", wora.dirs.metadata_root_dir);
1653                        info!("dirs.data: {:?}", wora.dirs.data_root_dir);
1654                        info!("dirs.runtime: {:?}", wora.dirs.runtime_root_dir);
1655                        info!("dirs.cache: {:?}", wora.dirs.cache_root_dir);
1656                        info!("dirs.secrets: {:?}", wora.dirs.secrets_root_dir);
1657
1658                        let cancelled = runtime_tasks.cancellation_token();
1659                        let readiness_exec = exec.clone();
1660                        let readiness_app_name = wora.app_name.clone();
1661                        let readiness_dirs = wora.dirs.clone();
1662                        let readiness_fs = fs.clone();
1663                        runtime_tasks.spawn("readiness notification", async move {
1664                            tokio::select! {
1665                                _ = cancelled.cancelled() => Ok(()),
1666                                result = wait_for_ready_notification::<AppEv, AppMetric, _, _>(
1667                                    readiness_exec, readiness_app_name, readiness_dirs, readiness_fs, readiness_rx,
1668                                ) => result.map_err(|err| err.to_string()),
1669                            }
1670                        });
1671
1672                        info!("notify:watch:dir: {:?}", &wora.dirs.metadata_root_dir);
1673                        let mut watcher = fs.watch_dir(&wora.dirs.metadata_root_dir).await?;
1674                        info!("notify:watch:secrets: {:?}", &wora.dirs.secrets_root_dir);
1675                        let mut secret_watcher = fs.watch_dir(&wora.dirs.secrets_root_dir).await?;
1676                        let metadata_root = wora.dirs.metadata_root_dir.clone();
1677                        let app_name = wora.app_name.clone();
1678                        let ev_sender = runtime_event_tx.clone();
1679                        let secret_ev_sender = runtime_event_tx.clone();
1680
1681                        let cancelled = runtime_tasks.cancellation_token();
1682                        runtime_tasks.spawn("metadata watcher", async move {
1683                            loop {
1684                                let res = tokio::select! {
1685                                    _ = cancelled.cancelled() => break,
1686                                    res = watcher.receiver().recv() => match res { Some(res) => res, None => break },
1687                                };
1688                                match res {
1689                                    Ok(event) => {
1690                                        info!("changed: {:?}", event);
1691                                        match ev_sender
1692                                            .send(Event::ConfigChanged(config_change_event(&app_name, &metadata_root, event)))
1693                                            .await
1694                                        {
1695                                            Ok(_) => {}
1696                                            Err(send_err) => {
1697                                                error!("send error: {:?}", send_err);
1698                                            }
1699                                        }
1700                                    }
1701                                    Err(e) => error!("watch error: {:?}", e),
1702                                }
1703                            }
1704                            Ok(())
1705                        });
1706                        let cancelled = runtime_tasks.cancellation_token();
1707                        runtime_tasks.spawn("secret watcher", async move {
1708                            loop {
1709                                let res = tokio::select! {
1710                                    _ = cancelled.cancelled() => break,
1711                                    res = secret_watcher.receiver().recv() => match res { Some(res) => res, None => break },
1712                                };
1713                                match res {
1714                                    Ok(event) => {
1715                                        info!("secret changed: {:?}", event);
1716                                        match secret_ev_sender.send(Event::SecretChanged(secret_change_event(event))).await {
1717                                            Ok(_) => {}
1718                                            Err(send_err) => {
1719                                                error!("send error: {:?}", send_err);
1720                                            }
1721                                        }
1722                                    }
1723                                    Err(e) => error!("watch error: {:?}", e),
1724                                }
1725                            }
1726                            Ok(())
1727                        });
1728
1729                        info!(process_id = wora.pid.to_string(), app_name = app.name());
1730
1731                        if exec.is_ready(&wora, fs.clone()).instrument(tracing::info_span!("exec:run:is_ready")).await {
1732                            rc = run_app_main_with_restart_policy(
1733                                &mut app,
1734                                &mut wora,
1735                                exec.clone(),
1736                                fs.clone(),
1737                                metrics_sender.clone(),
1738                                restart.clone(),
1739                                &mut supervision_rx,
1740                                restart_counter.clone(),
1741                            )
1742                            .await;
1743                        } else {
1744                            warn!(comp = "exec", method = "run", is_ready = false);
1745                        }
1746
1747                        exec.on_runtime_stopping(&wora.app_name, &wora.dirs, fs.clone())
1748                            .instrument(tracing::info_span!("exec:run:on_runtime_stopping"))
1749                            .await?;
1750
1751                        app.end(&wora, exec.clone(), fs.clone(), metrics_sender.clone())
1752                            .instrument(tracing::info_span!("app:run:end"))
1753                            .await;
1754                    }
1755                    Err(setup_err) => {
1756                        error!("app:run:setup error: {}", setup_err);
1757                        rc = Err(MainEarlyReturn::ApplicationSetup(setup_err.to_string()));
1758                    }
1759                }
1760
1761                exec.end(&wora, fs.clone()).instrument(tracing::info_span!("exec:run:end")).await;
1762
1763                runtime_tasks.shutdown().await?;
1764
1765                rc
1766            }
1767            .await;
1768
1769            drop(lock_guard);
1770            remove_lock_artifact(&lock_backend, &lock_path).await;
1771
1772            let _ = o11y_tx.send(o11y_new_ev_finish()).await;
1773
1774            result
1775        }
1776        Err(LockError::AlreadyLocked(path)) => {
1777            info!("lock already held: {:?}", &path);
1778
1779            let _ = o11y_tx.send(o11y_new_ev_finish()).await;
1780
1781            Err(MainEarlyReturn::AlreadyRunning(path))
1782        }
1783        Err(LockError::Io(err)) => {
1784            error!("lock file:{:?} error:{:?}", &lock_path, err);
1785
1786            let _ = o11y_tx.send(o11y_new_ev_finish()).await;
1787
1788            Err(MainEarlyReturn::IO(err))
1789        }
1790    }
1791}
1792
1793async fn remove_lock_artifact<L: LockBackend>(lock_backend: &L, lock_path: &PathBuf) {
1794    match lock_backend.cleanup(lock_path).await {
1795        Ok(_) => {
1796            debug!("lock:removed file:{:?}", lock_path);
1797        }
1798        Err(VfsError::Io(rm_err)) => {
1799            error!("lock file:{:?} error:{}", lock_path, rm_err);
1800        }
1801        Err(err) => {
1802            error!("lock artifact:{:?} error:{}", lock_path, err);
1803        }
1804    }
1805}
1806
1807fn lock_file_name(app_name: &str, allow_concurrent_executions: bool) -> String {
1808    if allow_concurrent_executions {
1809        let nonce = CONCURRENT_LOCK_NONCE.fetch_add(1, Ordering::Relaxed);
1810        format!("{app_name}-{}-{nonce}.lock", std::process::id())
1811    } else {
1812        format!("{app_name}.lock")
1813    }
1814}
1815
1816#[cfg(test)]
1817mod runner_tests {
1818    use std::sync::Arc;
1819    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1820
1821    use super::{Event, HostStats, MainEarlyReturn, MemorySnapshot, RuntimeTaskRegistry, SystemResourceStat, lock_file_name, publish_memory_snapshot};
1822
1823    #[tokio::test]
1824    async fn memory_cache_and_listeners_update_when_the_app_queue_is_full() -> Result<(), Box<dyn std::error::Error>> {
1825        let mut stats = HostStats::default();
1826        let (cache, mut updates) = tokio::sync::watch::channel(Arc::new(MemorySnapshot::from_host_stats(&stats)));
1827        let (sender, mut receiver) = tokio::sync::mpsc::channel(1);
1828        sender.try_send(Event::App(()))?;
1829
1830        stats.memory.used = 42;
1831        publish_memory_snapshot(&stats, &cache, &sender);
1832        updates.changed().await?;
1833        assert_eq!(updates.borrow_and_update().memory.used, 42);
1834        assert_eq!(cache.borrow().memory.used, 42);
1835        assert!(matches!(receiver.try_recv()?, Event::App(())));
1836
1837        stats.memory.used = 84;
1838        publish_memory_snapshot(&stats, &cache, &sender);
1839        let event = receiver.try_recv()?;
1840        let Event::SystemResource(SystemResourceStat::MemorySnapshot(snapshot)) = &event else {
1841            return Err("memory event missing".into());
1842        };
1843        assert!(Arc::ptr_eq(snapshot, &cache.borrow()));
1844        let json = serde_json::to_value(event)?;
1845        assert_eq!(json["SystemResource"]["MemorySnapshot"]["memory"]["used"], 84);
1846        Ok(())
1847    }
1848
1849    #[test]
1850    fn concurrent_lock_file_names_are_unique() {
1851        let first = lock_file_name("demo", true);
1852        let second = lock_file_name("demo", true);
1853
1854        assert_ne!(first, second);
1855        assert!(first.starts_with(&format!("demo-{}-", std::process::id())));
1856        assert!(first.ends_with(".lock"));
1857    }
1858
1859    #[test]
1860    fn single_instance_lock_file_name_remains_stable() {
1861        assert_eq!(lock_file_name("demo", false), "demo.lock");
1862    }
1863
1864    #[tokio::test]
1865    async fn runtime_tasks_cooperate_with_cancellation_and_stop_emitting() {
1866        let mut tasks = RuntimeTaskRegistry::new();
1867        let cancelled = tasks.cancellation_token();
1868        let emissions = Arc::new(AtomicUsize::new(0));
1869        let task_emissions = emissions.clone();
1870        let observed = Arc::new(AtomicBool::new(false));
1871        let task_observed = observed.clone();
1872        tasks.spawn("cooperative", async move {
1873            loop {
1874                tokio::select! {
1875                    _ = cancelled.cancelled() => {
1876                        task_observed.store(true, Ordering::SeqCst);
1877                        break;
1878                    }
1879                    _ = tokio::time::sleep(tokio::time::Duration::from_millis(1)) => {
1880                        task_emissions.fetch_add(1, Ordering::SeqCst);
1881                    }
1882                }
1883            }
1884            Ok(())
1885        });
1886
1887        tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
1888        assert!(tasks.shutdown().await.is_ok());
1889        let after_shutdown = emissions.load(Ordering::SeqCst);
1890        tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
1891        assert!(observed.load(Ordering::SeqCst));
1892        assert_eq!(emissions.load(Ordering::SeqCst), after_shutdown);
1893    }
1894
1895    #[tokio::test]
1896    async fn runtime_tasks_abort_tasks_that_ignore_cancellation() {
1897        let mut tasks = RuntimeTaskRegistry::new();
1898        let (dropped_tx, dropped_rx) = tokio::sync::oneshot::channel();
1899        tasks.spawn("stuck", async move {
1900            struct DropNotice(Option<tokio::sync::oneshot::Sender<()>>);
1901            impl Drop for DropNotice {
1902                fn drop(&mut self) {
1903                    if let Some(sender) = self.0.take() {
1904                        let _ = sender.send(());
1905                    }
1906                }
1907            }
1908            let _notice = DropNotice(Some(dropped_tx));
1909            std::future::pending::<()>().await;
1910            Ok(())
1911        });
1912
1913        assert!(tasks.shutdown().await.is_ok());
1914        assert!(dropped_rx.await.is_ok());
1915    }
1916
1917    #[tokio::test]
1918    async fn runtime_task_failures_are_reported() {
1919        let mut tasks = RuntimeTaskRegistry::new();
1920        tasks.spawn("broken", async { Err("boom".to_string()) });
1921
1922        let result = tasks.shutdown().await;
1923        assert!(matches!(result, Err(MainEarlyReturn::InfrastructureTask(message)) if message == "broken: boom"));
1924    }
1925}