1use 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
147pub struct Wora<AppEv, AppMetric> {
152 pub app_name: String,
154 pub dirs: Dirs,
156 pub initial_working_dir: PathBuf,
158 pub host: Host,
160 pid: nix::unistd::Pid,
162 pub sender: Sender<Event<AppEv>>,
164 pub receiver: Receiver<Event<AppEv>>,
166 pub leadership: Leadership,
168 pub(crate) o11y: O11yProcessorOptions<AppMetric>,
170 status: RuntimeStatusHandle,
172 memory_stats_tx: watch::Sender<Arc<MemorySnapshot>>,
173 resources: Option<RuntimeResources>,
174}
175
176#[derive(Clone, Debug, Serialize)]
178pub enum Leadership {
179 Leader,
181 Follower,
183 Unknown,
185}
186
187#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
189pub enum HealthState {
190 Ok,
192 Suspended,
194 TryAgain,
196 Failed,
198 Unknown,
200}
201
202#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
204pub enum ReadinessState {
205 Ready,
207 NotReady,
209 Draining,
211 Stopping,
213 Unknown,
215}
216
217#[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 pub fn report_health(&self, state: HealthState) {
233 let _ = self.health.send(state);
234 }
235
236 pub fn report_readiness(&self, state: ReadinessState) {
238 let _ = self.readiness.send(state);
239 }
240
241 pub fn begin_draining(&self) {
243 self.report_readiness(ReadinessState::Draining);
244 }
245
246 pub fn begin_stopping(&self) {
248 self.report_readiness(ReadinessState::Stopping);
249 }
250
251 pub fn subscribe_health(&self) -> watch::Receiver<HealthState> {
253 self.health.subscribe()
254 }
255
256 pub fn subscribe_readiness(&self) -> watch::Receiver<ReadinessState> {
258 self.readiness.subscribe()
259 }
260
261 pub fn health_state(&self) -> HealthState {
263 self.health.borrow().clone()
264 }
265
266 pub fn readiness_state(&self) -> ReadinessState {
268 self.readiness.borrow().clone()
269 }
270}
271
272impl<AppEv: Send + Sync + 'static, AppMetric: Send + Sync + 'static> Wora<AppEv, AppMetric> {
274 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 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 pub fn stats_from_start(&self) -> &HostStats {
315 &self.host.stats
316 }
317
318 pub fn resources(&self) -> Option<&RuntimeResources> {
320 self.resources.as_ref()
321 }
322
323 pub fn memory_stats(&self) -> Arc<MemorySnapshot> {
330 self.memory_stats_tx.borrow().clone()
331 }
332
333 pub fn subscribe_memory_stats(&self) -> watch::Receiver<Arc<MemorySnapshot>> {
340 self.memory_stats_tx.subscribe()
341 }
342
343 pub fn dirs(&self) -> &Dirs {
345 &self.dirs
346 }
347
348 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 pub fn host_os_name(&self) -> &str {
362 &self.host.info.os_name
363 }
364 pub fn host_os_version(&self) -> Option<String> {
366 self.host.info.os_version.clone()
367 }
368 pub fn host_architecture(&self) -> Option<String> {
370 self.host.info.architecture.clone()
371 }
372 pub fn host_hostname(&self) -> Option<String> {
374 self.host.info.hostname.clone()
375 }
376 pub fn host_cpu_count(&self) -> usize {
378 self.host.info.ncpus
379 }
380 pub fn host_cpu_max(&self) -> usize {
382 self.host.info.maxcpus
383 }
384
385 pub fn status_handle(&self) -> RuntimeStatusHandle {
391 self.status.clone()
392 }
393
394 pub fn report_health(&self, state: HealthState) {
396 self.status.report_health(state);
397 }
398
399 pub fn report_readiness(&self, state: ReadinessState) {
401 self.status.report_readiness(state);
402 }
403
404 pub fn begin_draining(&self) {
406 self.status.begin_draining();
407 }
408
409 pub fn begin_stopping(&self) {
411 self.status.begin_stopping();
412 }
413
414 pub fn health_state(&self) -> HealthState {
416 self.status.health_state()
417 }
418
419 pub fn readiness_state(&self) -> ReadinessState {
421 self.status.readiness_state()
422 }
423
424 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 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 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 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 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#[derive(Debug, Clone)]
544pub enum TaskOp {
545 Requeue,
547 Abort,
549}
550
551pub trait Config {
553 type ConfigT: Default + Send + 'static;
555 fn parse_main_config_file(data: String) -> Result<Self::ConfigT, Box<dyn std::error::Error>>;
557
558 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#[derive(Clone, Debug)]
566pub struct ConfigFile<T> {
567 pub path: PathBuf,
569 pub value: T,
571}
572
573#[derive(Clone, Debug)]
575pub struct ConfigReload<T> {
576 pub main: Option<T>,
578 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#[derive(Clone, Debug)]
593pub struct SecretFile<T> {
594 pub path: PathBuf,
596 pub key: String,
598 pub value: T,
600}
601
602#[derive(Clone, Debug)]
604pub struct SecretReload<T> {
605 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
615pub trait Secrets {
617 type SecretT: Send + 'static;
619
620 fn parse_secret_file(file_path: PathBuf, data: Vec<u8>) -> Result<Self::SecretT, Box<dyn std::error::Error>>;
622}
623
624pub 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#[derive(Clone, Debug, PartialEq, Eq)]
636pub enum ReloadHandling {
637 NotHandled,
639 ConfigApplied,
641 SecretsApplied,
643}
644
645#[derive(Clone, Debug)]
647pub enum EventLoopAction {
648 Continue,
650 Exit(MainRetryAction),
652}
653
654pub 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]
663pub trait App<AppEv: Send + Sync + 'static, AppMetric: Send + Sync + 'static> {
669 type AppConfig: Config;
671 type AppSecrets: Secrets;
673 type Setup;
675 fn name(&self) -> &'static str;
677
678 fn allow_concurrent_executions(&self) -> bool {
680 false
681 }
682
683 async fn configure(&mut self, _config: <Self::AppConfig as Config>::ConfigT) -> Result<(), Box<dyn std::error::Error>> {
688 Ok(())
689 }
690
691 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 async fn reload_secrets(&mut self, _reload: SecretReload<<Self::AppSecrets as Secrets>::SecretT>) -> Result<(), Box<dyn std::error::Error>> {
701 Ok(())
702 }
703
704 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 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 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(¬ify_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(¬ify_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#[derive(Clone)]
1095pub struct RunnerOptions<AppEv, L = ProcLockBackend, R = SystemRuntimeEnvironment> {
1096 pub resources: Option<RuntimeResources>,
1098 pub boot_dir: Option<PathBuf>,
1100 pub restart: RestartPolicyOptions,
1102 pub signal_mapper: SignalMapper<AppEv>,
1110 pub lock_backend: L,
1112 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 pub fn new() -> Self {
1132 Self::default()
1133 }
1134}
1135
1136impl<AppEv: Send + Sync + 'static, L, R> RunnerOptions<AppEv, L, R> {
1137 pub fn with_resources(mut self, resources: RuntimeResources) -> Self {
1141 self.resources = Some(resources);
1142 self
1143 }
1144
1145 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 pub fn with_restart_options(mut self, restart: RestartPolicyOptions) -> Self {
1153 self.restart = restart;
1154 self
1155 }
1156
1157 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 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 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
1188pub 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
1203pub 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}