1use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::{Arc, Condvar, Mutex, MutexGuard};
5
6use beamr::process::ExitReason;
7use dashmap::mapref::entry::Entry;
8
9use crate::{EngineError, Pid, RuntimeHandle};
10
11use super::cleanup_executor::CleanupSubmitError;
12use super::outcome::{self, WorkflowProcessOutcome};
13use super::process_exit::{ObservedProcessExit, OwnedProcessExitOutcome};
14
15pub(super) struct MonitorInstallation {
17 committed: AtomicBool,
18}
19
20impl MonitorInstallation {
21 fn uncommitted() -> Self {
22 Self {
23 committed: AtomicBool::new(false),
24 }
25 }
26
27 pub(super) fn commit(&self) {
28 self.committed.store(true, Ordering::Release);
29 }
30}
31
32#[derive(Debug, thiserror::Error)]
34pub(crate) enum UnmonitoredProcessAbortError {
35 #[error("process {process_id} did not complete unmonitored abort within {timeout_millis}ms")]
37 TimedOut {
38 process_id: Pid,
40 timeout_millis: u128,
42 },
43 #[error("process cleanup executor is unavailable for process {process_id}")]
45 ExecutorUnavailable {
46 process_id: Pid,
48 },
49 #[error("process cleanup executor is exhausted for process {process_id}")]
51 ExecutorExhausted {
52 process_id: Pid,
54 },
55 #[error("process cleanup executor state is poisoned for process {process_id}")]
57 ExecutorPoisoned {
58 process_id: Pid,
60 },
61 #[error("process {process_id} already has a completion monitor owner")]
63 MonitorInstalled {
64 process_id: Pid,
66 },
67 #[error("process exit ownership gate for process {process_id} was poisoned")]
69 OwnershipPoisoned {
70 process_id: Pid,
72 },
73 #[error("unmonitored abort state for process {process_id} was poisoned")]
75 StatePoisoned {
76 process_id: Pid,
78 },
79 #[error("process {process_id} cleanup failed: {reason}")]
81 CleanupFailed {
82 process_id: Pid,
84 reason: String,
86 },
87}
88
89impl UnmonitoredProcessAbortError {
90 pub(crate) fn into_engine_error(self) -> EngineError {
91 EngineError::Runtime {
92 reason: self.to_string(),
93 }
94 }
95}
96
97#[derive(Clone)]
99pub struct ProcessMonitorHandle {
100 installed: Arc<AtomicBool>,
101}
102
103impl ProcessMonitorHandle {
104 fn installed() -> Self {
105 Self {
106 installed: Arc::new(AtomicBool::new(true)),
107 }
108 }
109
110 #[must_use]
112 pub fn is_installed(&self) -> bool {
113 self.installed.load(Ordering::Acquire)
114 }
115}
116
117#[derive(Clone)]
118enum AbortJobTerminal {
119 Succeeded,
120 CleanupFailed(String),
121}
122
123enum AbortJobPhase {
124 Running,
125 Finalizing,
126 Complete(AbortJobTerminal),
127}
128
129struct AbortJobState {
130 phase: AbortJobPhase,
131 installation: Option<Arc<MonitorInstallation>>,
132 finalizers: Vec<Box<dyn FnOnce() + Send>>,
139}
140
141pub(super) struct UnmonitoredProcessAbortJob {
143 pid: Pid,
144 state: Mutex<AbortJobState>,
145 ready: Condvar,
146}
147
148impl UnmonitoredProcessAbortJob {
149 fn new(pid: Pid, installation: Option<Arc<MonitorInstallation>>) -> Self {
150 Self {
151 pid,
152 state: Mutex::new(AbortJobState {
153 phase: AbortJobPhase::Running,
154 installation,
155 finalizers: Vec::new(),
156 }),
157 ready: Condvar::new(),
158 }
159 }
160
161 fn attach_finalizer(
165 &self,
166 finalizer: Box<dyn FnOnce() + Send>,
167 ) -> Result<(), UnmonitoredProcessAbortError> {
168 let run_now = {
169 let mut state = self.lock_state()?;
170 if matches!(state.phase, AbortJobPhase::Complete(_)) {
171 Some(finalizer)
172 } else {
173 state.finalizers.push(finalizer);
174 None
175 }
176 };
177 if let Some(finalizer) = run_now {
178 finalizer();
179 }
180 Ok(())
181 }
182
183 fn attach_installation(
184 &self,
185 installation: Option<Arc<MonitorInstallation>>,
186 ) -> Result<(), UnmonitoredProcessAbortError> {
187 let Some(installation) = installation else {
188 return Ok(());
189 };
190 let mut state = self.lock_state()?;
191 if state.installation.is_none() {
192 state.installation = Some(installation);
193 }
194 Ok(())
195 }
196
197 fn complete_cleanup(
198 self: &Arc<Self>,
199 runtime: &RuntimeHandle,
200 record: Option<&Arc<super::process_exit::ProcessExitRecord>>,
201 cleanup: Result<(), EngineError>,
202 ) -> Result<(), UnmonitoredProcessAbortError> {
203 let (installation, terminal) = {
204 let mut state = self.lock_state()?;
205 let terminal = match cleanup {
206 Ok(()) => AbortJobTerminal::Succeeded,
207 Err(error) => AbortJobTerminal::CleanupFailed(error.to_string()),
208 };
209 state.phase = AbortJobPhase::Finalizing;
210 (state.installation.take(), terminal)
211 };
212 let ownership = record
213 .map(|record| record.lock_ownership())
214 .transpose()
215 .map_err(|_| UnmonitoredProcessAbortError::OwnershipPoisoned {
216 process_id: self.pid,
217 })?;
218 if let Some(installation) = installation {
219 runtime.release_monitor_installation(self.pid, &installation);
220 }
221 if let Some(record) = record {
222 runtime.process_exits.retire(self.pid, record);
223 }
224 let finalizers = {
225 let mut state = self.lock_state()?;
226 state.phase = AbortJobPhase::Complete(terminal);
227 std::mem::take(&mut state.finalizers)
228 };
229 if let Entry::Occupied(entry) = runtime.abort_jobs.entry(self.pid) {
230 if Arc::ptr_eq(entry.get(), self) {
231 entry.remove();
232 }
233 }
234 self.ready.notify_all();
235 drop(ownership);
236 for finalizer in finalizers {
239 finalizer();
240 }
241 Ok(())
242 }
243
244 fn wait(&self, timeout: std::time::Duration) -> Result<(), UnmonitoredProcessAbortError> {
245 let state = self.lock_state()?;
246 let (state, wait) = self
247 .ready
248 .wait_timeout_while(state, timeout, |state| {
249 matches!(
250 state.phase,
251 AbortJobPhase::Running | AbortJobPhase::Finalizing
252 )
253 })
254 .map_err(|_| UnmonitoredProcessAbortError::StatePoisoned {
255 process_id: self.pid,
256 })?;
257 match &state.phase {
258 AbortJobPhase::Running | AbortJobPhase::Finalizing if wait.timed_out() => {
259 Err(UnmonitoredProcessAbortError::TimedOut {
260 process_id: self.pid,
261 timeout_millis: timeout.as_millis(),
262 })
263 }
264 AbortJobPhase::Running | AbortJobPhase::Finalizing => {
265 Err(UnmonitoredProcessAbortError::StatePoisoned {
266 process_id: self.pid,
267 })
268 }
269 AbortJobPhase::Complete(terminal) => terminal.result(self.pid),
270 }
271 }
272
273 fn lock_state(&self) -> Result<MutexGuard<'_, AbortJobState>, UnmonitoredProcessAbortError> {
274 self.state
275 .lock()
276 .map_err(|_| UnmonitoredProcessAbortError::StatePoisoned {
277 process_id: self.pid,
278 })
279 }
280}
281
282impl AbortJobTerminal {
283 fn result(&self, pid: Pid) -> Result<(), UnmonitoredProcessAbortError> {
284 match self {
285 Self::Succeeded => Ok(()),
286 Self::CleanupFailed(reason) => Err(UnmonitoredProcessAbortError::CleanupFailed {
287 process_id: pid,
288 reason: reason.clone(),
289 }),
290 }
291 }
292}
293
294impl RuntimeHandle {
295 pub fn monitor_process<F>(
302 self: &Arc<Self>,
303 pid: Pid,
304 callback: F,
305 ) -> Result<ProcessMonitorHandle, EngineError>
306 where
307 F: FnOnce(Result<WorkflowProcessOutcome, EngineError>) + Send + 'static,
308 {
309 self.ensure_monitorable_pid(pid)?;
310 let record = self.process_exits.get(pid)?;
311 let ownership = record.lock_ownership()?;
312 if !self.process_exits.is_current(pid, &record) {
313 return Err(EngineError::ProcessExitAlreadyTerminal { process_id: pid });
314 }
315 let installation = self.reserve_monitor_installation(pid)?;
316 #[cfg(test)]
317 if self.take_monitor_installation_failure_for_test() {
318 let error = EngineError::Runtime {
319 reason: format!(
320 "failed to install workflow monitor for process {pid}: forced test failure"
321 ),
322 };
323 drop(ownership);
324 self.rollback_failed_monitor_installation(pid, &installation)?;
325 return Err(error);
326 }
327
328 let runtime = Arc::clone(self);
329 let callback_record = Arc::clone(&record);
330 let callback_installation = Arc::clone(&installation);
331 let completion = Box::new(move |owned| {
332 let process_outcome =
333 outcome::workflow_outcome_from_owned_exit(&runtime.atom_table, pid, &owned);
334 let monitored_outcome = match runtime.finish_process_monitor_cleanup(pid) {
335 Ok(()) => process_outcome,
336 Err(error) => {
337 tracing::error!(%error, pid, "workflow activity cleanup failed");
338 Err(error)
339 }
340 };
341 let callback_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
342 callback(monitored_outcome);
343 }));
344 match callback_record.lock_ownership() {
345 Ok(retirement) => {
346 runtime.release_monitor_installation(pid, &callback_installation);
347 runtime.process_exits.retire(pid, &callback_record);
348 drop(retirement);
349 }
350 Err(error) => {
351 tracing::error!(pid, %error, "failed to retire completed process monitor");
352 }
353 }
354 if let Err(panic) = callback_result {
355 std::panic::resume_unwind(panic);
356 }
357 });
358 if let Err(error) = self
359 .process_exits
360 .attach_callback(&record, &installation, completion)
361 {
362 drop(ownership);
363 self.rollback_failed_monitor_installation(pid, &installation)?;
364 return Err(error);
365 }
366 drop(ownership);
367 Ok(ProcessMonitorHandle::installed())
368 }
369
370 fn reserve_monitor_installation(
371 &self,
372 pid: Pid,
373 ) -> Result<Arc<MonitorInstallation>, EngineError> {
374 if self.abort_jobs.contains_key(&pid) {
375 return Err(EngineError::Runtime {
376 reason: format!("process {pid} already has an abort job"),
377 });
378 }
379 match self.nif_state().monitor_installations.entry(pid) {
380 Entry::Vacant(entry) => {
381 let installation = Arc::new(MonitorInstallation::uncommitted());
382 entry.insert(Arc::clone(&installation));
383 Ok(installation)
384 }
385 Entry::Occupied(_) => Err(EngineError::Runtime {
386 reason: format!("process {pid} already has a completion monitor installation"),
387 }),
388 }
389 }
390
391 fn release_monitor_installation(&self, pid: Pid, installation: &Arc<MonitorInstallation>) {
392 if let Entry::Occupied(entry) = self.nif_state().monitor_installations.entry(pid) {
393 if Arc::ptr_eq(entry.get(), installation) {
394 entry.remove();
395 }
396 }
397 }
398
399 fn rollback_failed_monitor_installation(
400 self: &Arc<Self>,
401 pid: Pid,
402 installation: &Arc<MonitorInstallation>,
403 ) -> Result<(), EngineError> {
404 if installation.committed.load(Ordering::Acquire) {
405 return Ok(());
406 }
407 self.abort_unmonitored_process_with_installation(pid, Some(Arc::clone(installation)))
408 .map_err(UnmonitoredProcessAbortError::into_engine_error)
409 }
410
411 pub(crate) fn abort_unmonitored_process(
413 self: &Arc<Self>,
414 pid: Pid,
415 ) -> Result<(), UnmonitoredProcessAbortError> {
416 self.abort_unmonitored_process_with_installation(pid, None)
417 }
418
419 pub(crate) fn attach_unmonitored_abort_finalizer<F>(
435 &self,
436 pid: Pid,
437 finalizer: F,
438 ) -> Result<bool, EngineError>
439 where
440 F: FnOnce() + Send + 'static,
441 {
442 let Some(job) = self.abort_jobs.get(&pid).map(|job| Arc::clone(job.value())) else {
443 return Ok(false);
444 };
445 job.attach_finalizer(Box::new(finalizer))
446 .map_err(UnmonitoredProcessAbortError::into_engine_error)?;
447 Ok(true)
448 }
449
450 fn abort_unmonitored_process_with_installation(
451 self: &Arc<Self>,
452 pid: Pid,
453 installation: Option<Arc<MonitorInstallation>>,
454 ) -> Result<(), UnmonitoredProcessAbortError> {
455 if self.process_exits.is_retired(pid) {
456 return Ok(());
457 }
458 let record = self.process_exits.find(pid);
459 if record.is_none() && self.process_exits.is_retired(pid) {
460 return Ok(());
461 }
462 let ownership = record
463 .as_ref()
464 .map(|record| record.lock_ownership())
465 .transpose()
466 .map_err(|_| UnmonitoredProcessAbortError::OwnershipPoisoned { process_id: pid })?;
467 if record
468 .as_ref()
469 .is_some_and(|record| !self.process_exits.is_current(pid, record))
470 {
471 return Ok(());
472 }
473 let job = match self.abort_jobs.entry(pid) {
474 Entry::Occupied(entry) => {
475 let job = Arc::clone(entry.get());
476 drop(entry);
477 job.attach_installation(installation)?;
478 job
479 }
480 Entry::Vacant(entry) => {
481 let owns_uncommitted_installation = installation.as_ref().is_some_and(|expected| {
482 !expected.committed.load(Ordering::Acquire)
483 && self
484 .nif_state()
485 .monitor_installations
486 .get(&pid)
487 .is_some_and(|current| Arc::ptr_eq(current.value(), expected))
488 });
489 if self.nif_state().monitor_installations.contains_key(&pid)
490 && !owns_uncommitted_installation
491 {
492 return Err(UnmonitoredProcessAbortError::MonitorInstalled { process_id: pid });
493 }
494 let refused_installation = installation.clone();
495 let job = Arc::new(UnmonitoredProcessAbortJob::new(pid, installation));
496 let runtime = Arc::clone(self);
497 let worker_job = Arc::clone(&job);
498 let worker_record = record.clone();
499 let submission = self.cleanup_executor.submit(Box::new(move || {
500 if runtime.is_live(pid) {
501 runtime.scheduler.terminate_process(pid, ExitReason::Kill);
502 }
503 let mut cleanup = runtime.finish_process_monitor_cleanup(pid);
504 if let Some(record) = worker_record.as_ref() {
505 if let Err(error) = record.wait() {
506 if cleanup.is_ok() {
507 cleanup = Err(error);
508 }
509 }
510 if let Err(error) = record.close_without_monitor() {
511 if cleanup.is_ok() {
512 cleanup = Err(error);
513 }
514 }
515 }
516 if let Err(error) =
517 worker_job.complete_cleanup(&runtime, worker_record.as_ref(), cleanup)
518 {
519 tracing::error!(pid, %error, "failed to publish process abort completion");
520 }
521 }));
522 if let Err(error) = submission {
523 if let Some(installation) = refused_installation {
524 self.release_monitor_installation(pid, &installation);
525 }
526 return Err(match error {
527 CleanupSubmitError::Unavailable => {
528 UnmonitoredProcessAbortError::ExecutorUnavailable { process_id: pid }
529 }
530 CleanupSubmitError::Exhausted => {
531 UnmonitoredProcessAbortError::ExecutorExhausted { process_id: pid }
532 }
533 CleanupSubmitError::Poisoned => {
534 UnmonitoredProcessAbortError::ExecutorPoisoned { process_id: pid }
535 }
536 });
537 }
538 entry.insert(Arc::clone(&job));
539 job
540 }
541 };
542 drop(ownership);
543 job.wait(self.signal_delivery().ready_timeout)
544 }
545
546 #[cfg(test)]
547 pub(super) fn process_exit_outcome(
548 &self,
549 pid: Pid,
550 ) -> Result<Arc<ObservedProcessExit>, EngineError> {
551 match self.process_exits.get(pid)?.wait()? {
552 OwnedProcessExitOutcome::Observed(observed) => Ok(observed),
553 OwnedProcessExitOutcome::ObservationFailed {
554 process_id,
555 failure,
556 } => Err(failure.into_engine_error(process_id)),
557 }
558 }
559
560 pub(super) fn activity_process_exit_outcome(
561 &self,
562 pid: Pid,
563 ) -> Result<Arc<ObservedProcessExit>, EngineError> {
564 let record = self.process_exits.get(pid)?;
565 let outcome = match record.wait()? {
566 OwnedProcessExitOutcome::Observed(observed) => Ok(observed),
567 OwnedProcessExitOutcome::ObservationFailed {
568 process_id,
569 failure,
570 } => Err(failure.into_engine_error(process_id)),
571 };
572 record.close_without_monitor()?;
573 let retirement = record.lock_ownership()?;
574 self.process_exits.retire(pid, &record);
575 drop(retirement);
576 outcome
577 }
578
579 pub(super) fn finish_process_monitor_cleanup(&self, pid: Pid) -> Result<(), EngineError> {
580 self.release_spawn_heaps(pid);
581 self.nif_state().cleanup_process(pid);
582 self.kill_in_vm_children(pid);
583 let activity_cleanup = self.drain_activity_completions(pid);
584 self.finish_activity_delivery_cleanup(pid);
585 activity_cleanup
586 }
587
588 #[cfg(test)]
594 pub fn monitor_process_for_test<F>(
595 self: &Arc<Self>,
596 pid: Pid,
597 callback: F,
598 ) -> Result<ProcessMonitorHandle, EngineError>
599 where
600 F: FnOnce(Result<WorkflowProcessOutcome, EngineError>) + Send + 'static,
601 {
602 self.monitor_process(pid, callback)
603 }
604
605 #[cfg(test)]
606 pub(crate) fn process_cleanup_started_for_test(&self, pid: Pid) -> bool {
607 self.nif_state().process_cleanup_started(pid)
608 }
609
610 #[cfg(test)]
611 pub(crate) fn process_cleanup_complete_for_test(&self, pid: Pid) -> bool {
612 self.process_cleanup_started_for_test(pid)
613 && !self.is_live(pid)
614 && !self.abort_jobs.contains_key(&pid)
615 && !self.nif_state().monitor_installations.contains_key(&pid)
616 && !self.process_exits.contains(pid)
617 }
618}
619
620#[cfg(test)]
621#[path = "monitor_tests.rs"]
622mod tests;