1use std::collections::{BTreeMap, VecDeque};
4use std::fmt;
5use std::panic::{catch_unwind, AssertUnwindSafe};
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex};
8
9use crate::accounting::{LimitError, Reservation, ResourceClass, ResourceLedger};
10use crate::metrics::{
11 RuntimeMetrics, TelemetryFallback, TelemetryFallbackCode, TelemetrySeverity, TelemetrySubsystem,
12};
13
14#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
15pub enum TaskClass {
16 Runtime,
17 Dns,
18 Socket,
19 Listener,
20 Udp,
21 Tls,
22 Http2,
23 Timer,
24 Vm,
25 Plugin,
26}
27
28#[derive(Clone, Debug, Eq, PartialEq)]
29pub enum TaskOwner {
30 Process,
31 Vm { generation: u64 },
32 Capability { id: u64, generation: u64 },
33 Connection { id: u64, generation: u64 },
34 Background { name: &'static str },
35}
36
37impl fmt::Display for TaskOwner {
38 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
39 match self {
40 Self::Process => formatter.write_str("process"),
41 Self::Vm { generation } => write!(formatter, "vm-generation={generation}"),
42 Self::Capability { id, generation } => {
43 write!(formatter, "capability={id} generation={generation}")
44 }
45 Self::Connection { id, generation } => {
46 write!(formatter, "connection={id} generation={generation}")
47 }
48 Self::Background { name } => write!(formatter, "background={name}"),
49 }
50 }
51}
52
53#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
54pub enum TaskTerminalReason {
55 Completed,
56 Cancelled,
57 Failed,
58 Panicked,
59}
60
61#[derive(Clone, Debug, Eq, PartialEq)]
62pub struct TaskTerminalReport {
63 pub class: TaskClass,
64 pub owner: TaskOwner,
65 pub scope: String,
66 pub reason: TaskTerminalReason,
67}
68
69#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
70pub struct TaskClassSnapshot {
71 pub active: usize,
72 pub completed: u64,
73 pub cancelled: u64,
74 pub failed: u64,
75 pub panicked: u64,
76}
77
78#[derive(Clone, Debug, Eq, PartialEq)]
79pub enum TaskSpawnError {
80 ResourceLimit(LimitError),
81 AdmissionClosed { scope: String },
82}
83
84impl fmt::Display for TaskSpawnError {
85 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
86 match self {
87 Self::ResourceLimit(error) => error.fmt(formatter),
88 Self::AdmissionClosed { scope } => write!(
89 formatter,
90 "ERR_AGENTOS_TASK_ADMISSION_CLOSED: scope={scope} is closing; new task admission is disabled"
91 ),
92 }
93 }
94}
95
96impl std::error::Error for TaskSpawnError {}
97
98impl From<LimitError> for TaskSpawnError {
99 fn from(error: LimitError) -> Self {
100 Self::ResourceLimit(error)
101 }
102}
103
104impl TaskClassSnapshot {
105 fn record(&mut self, reason: TaskTerminalReason) {
106 let counter = match reason {
107 TaskTerminalReason::Completed => &mut self.completed,
108 TaskTerminalReason::Cancelled => &mut self.cancelled,
109 TaskTerminalReason::Failed => &mut self.failed,
110 TaskTerminalReason::Panicked => &mut self.panicked,
111 };
112 *counter = counter.saturating_add(1);
113 }
114}
115
116#[derive(Debug, Default)]
117struct TaskSupervisorState {
118 classes: BTreeMap<TaskClass, TaskClassSnapshot>,
119 active_scopes: BTreeMap<String, usize>,
120 reports: VecDeque<TaskTerminalReport>,
121 dropped_reports: u64,
122 report_overflow_warned: bool,
123}
124
125type TerminalHandler = Arc<dyn Fn(&TaskTerminalReport) + Send + Sync>;
126
127#[derive(Clone, Debug)]
128pub struct TaskSupervisor {
129 ledger: Arc<ResourceLedger>,
130 metrics: RuntimeMetrics,
131 state: Arc<Mutex<TaskSupervisorState>>,
132 settled: Arc<tokio::sync::Notify>,
133 admission_open: Arc<AtomicBool>,
134 admission_gate: Arc<Mutex<()>>,
135 report_capacity: usize,
136}
137
138impl TaskSupervisor {
139 pub(crate) fn new(
140 ledger: Arc<ResourceLedger>,
141 metrics: RuntimeMetrics,
142 admission_open: Arc<AtomicBool>,
143 admission_gate: Arc<Mutex<()>>,
144 report_capacity: usize,
145 ) -> Self {
146 Self::with_report_capacity(
147 ledger,
148 metrics,
149 admission_open,
150 admission_gate,
151 report_capacity,
152 )
153 }
154
155 fn with_report_capacity(
156 ledger: Arc<ResourceLedger>,
157 metrics: RuntimeMetrics,
158 admission_open: Arc<AtomicBool>,
159 admission_gate: Arc<Mutex<()>>,
160 report_capacity: usize,
161 ) -> Self {
162 Self {
163 ledger,
164 metrics,
165 state: Arc::new(Mutex::new(TaskSupervisorState::default())),
166 settled: Arc::new(tokio::sync::Notify::new()),
167 admission_open,
168 admission_gate,
169 report_capacity: report_capacity.max(1),
170 }
171 }
172
173 pub(crate) fn admit(
174 &self,
175 class: TaskClass,
176 owner: TaskOwner,
177 handler: Option<TerminalHandler>,
178 ) -> Result<TaskGuard, TaskSpawnError> {
179 let _admission = self.admission_gate.lock().unwrap_or_else(|poisoned| {
182 eprintln!("ERR_AGENTOS_TASK_ADMISSION_GATE_POISONED: recovering task admission");
183 poisoned.into_inner()
184 });
185 self.ensure_admission_open()?;
186 let reservation = self
187 .ledger
188 .reserve(ResourceClass::Tasks, 1)
189 .map_err(TaskSpawnError::ResourceLimit)?;
190 let scope = self.ledger.scope().to_owned();
191 let mut state = self.state.lock().unwrap_or_else(|poisoned| {
192 eprintln!("ERR_AGENTOS_TASK_SUPERVISOR_POISONED: recovering task admission");
193 poisoned.into_inner()
194 });
195 state.classes.entry(class).or_default().active += 1;
196 *state.active_scopes.entry(scope.clone()).or_default() += 1;
197 drop(state);
198 self.metrics.task_started(class);
199 Ok(TaskGuard {
200 class,
201 owner,
202 scope,
203 supervisor: self.clone(),
204 handler,
205 reservation: Some(reservation),
206 terminal: None,
207 })
208 }
209
210 pub fn snapshot(&self, class: TaskClass) -> TaskClassSnapshot {
211 self.state
212 .lock()
213 .map(|state| state.classes.get(&class).copied().unwrap_or_default())
214 .unwrap_or_else(|_| {
215 eprintln!("ERR_AGENTOS_TASK_SUPERVISOR_POISONED: failed to read task census");
216 TaskClassSnapshot::default()
217 })
218 }
219
220 pub fn active_total(&self) -> usize {
221 self.state
222 .lock()
223 .map(|state| state.classes.values().map(|stats| stats.active).sum())
224 .unwrap_or_else(|_| {
225 eprintln!("ERR_AGENTOS_TASK_SUPERVISOR_POISONED: failed to read active census");
226 0
227 })
228 }
229
230 pub fn active_scoped(&self) -> usize {
231 let scope = self.ledger.scope();
232 self.state
233 .lock()
234 .map(|state| state.active_scopes.get(scope).copied().unwrap_or(0))
235 .unwrap_or_else(|_| {
236 eprintln!(
237 "ERR_AGENTOS_TASK_SUPERVISOR_POISONED: failed to read scoped task census"
238 );
239 usize::MAX
240 })
241 }
242
243 pub async fn wait_empty(&self) {
247 loop {
248 let settled = self.settled.notified();
249 if self.active_scoped() == 0 {
250 return;
251 }
252 settled.await;
253 }
254 }
255
256 pub(crate) fn close_admission(&self) {
257 let _admission = self.admission_gate.lock().unwrap_or_else(|poisoned| {
258 eprintln!("ERR_AGENTOS_TASK_ADMISSION_GATE_POISONED: recovering task close");
259 poisoned.into_inner()
260 });
261 self.admission_open.store(false, Ordering::Release);
262 }
263
264 fn ensure_admission_open(&self) -> Result<(), TaskSpawnError> {
265 if self.admission_open.load(Ordering::Acquire) {
266 Ok(())
267 } else {
268 Err(TaskSpawnError::AdmissionClosed {
269 scope: self.ledger.scope().to_owned(),
270 })
271 }
272 }
273
274 pub fn drain_terminal_reports(&self) -> Vec<TaskTerminalReport> {
275 self.state
276 .lock()
277 .map(|mut state| {
278 state.report_overflow_warned = false;
279 state.reports.drain(..).collect()
280 })
281 .unwrap_or_else(|_| {
282 eprintln!("ERR_AGENTOS_TASK_SUPERVISOR_POISONED: failed to drain terminal reports");
283 Vec::new()
284 })
285 }
286
287 pub fn dropped_terminal_reports(&self) -> u64 {
288 self.state
289 .lock()
290 .map(|state| state.dropped_reports)
291 .unwrap_or(u64::MAX)
292 }
293
294 pub(crate) fn scoped(
295 &self,
296 ledger: Arc<ResourceLedger>,
297 admission_open: Arc<AtomicBool>,
298 admission_gate: Arc<Mutex<()>>,
299 ) -> Self {
300 Self {
301 ledger,
302 metrics: self.metrics.clone(),
303 state: Arc::clone(&self.state),
304 settled: Arc::clone(&self.settled),
305 admission_open,
306 admission_gate,
307 report_capacity: self.report_capacity,
308 }
309 }
310
311 fn terminal(&self, report: TaskTerminalReport, handler: Option<&TerminalHandler>) {
312 {
313 let mut state = self.state.lock().unwrap_or_else(|poisoned| {
314 eprintln!("ERR_AGENTOS_TASK_SUPERVISOR_POISONED: recovering terminal task report");
315 poisoned.into_inner()
316 });
317 let stats = state.classes.entry(report.class).or_default();
318 if stats.active == 0 {
319 eprintln!(
320 "ERR_AGENTOS_TASK_ACCOUNTING_UNDERFLOW: class={:?} owner={}",
321 report.class, report.owner
322 );
323 } else {
324 stats.active -= 1;
325 }
326 stats.record(report.reason);
327 match state.active_scopes.get_mut(&report.scope) {
328 Some(active) if *active > 1 => *active -= 1,
329 Some(_) => {
330 state.active_scopes.remove(&report.scope);
331 }
332 None => eprintln!(
333 "ERR_AGENTOS_TASK_ACCOUNTING_UNDERFLOW: scope={} class={:?} owner={}",
334 report.scope, report.class, report.owner
335 ),
336 }
337 if matches!(
343 report.reason,
344 TaskTerminalReason::Failed | TaskTerminalReason::Panicked
345 ) {
346 if state.reports.len() == self.report_capacity {
347 state.reports.pop_front();
348 state.dropped_reports = state.dropped_reports.saturating_add(1);
349 if !state.report_overflow_warned {
350 state.report_overflow_warned = true;
351 eprintln!(
352 "ERR_AGENTOS_TASK_REPORT_LIMIT: failed task report buffer exceeded {}; latest_class={:?} latest_owner={}; oldest reports will be dropped until drained; raise runtime.tasks.maxTerminalReports",
353 self.report_capacity, report.class, report.owner
354 );
355 }
356 }
357 state.reports.push_back(report.clone());
358 }
359 }
360 self.metrics.task_finished(report.class, report.reason);
361 self.settled.notify_waiters();
362
363 if matches!(
364 report.reason,
365 TaskTerminalReason::Failed | TaskTerminalReason::Panicked
366 ) {
367 let message = format!(
368 "class={:?} owner={} reason={:?}",
369 report.class, report.owner, report.reason
370 );
371 self.metrics.emit_stderr_fallback(TelemetryFallback {
372 severity: TelemetrySeverity::Fatal,
373 code: TelemetryFallbackCode::SupervisedTaskExit,
374 subsystem: TelemetrySubsystem::Runtime,
375 message: &message,
376 });
377 }
378 if let Some(handler) = handler {
379 if catch_unwind(AssertUnwindSafe(|| handler(&report))).is_err() {
380 eprintln!(
381 "ERR_AGENTOS_TASK_TERMINAL_HANDLER_PANIC: class={:?} owner={} reason={:?}",
382 report.class, report.owner, report.reason
383 );
384 }
385 }
386 }
387}
388
389pub(crate) struct TaskGuard {
390 class: TaskClass,
391 owner: TaskOwner,
392 scope: String,
393 supervisor: TaskSupervisor,
394 handler: Option<TerminalHandler>,
395 reservation: Option<Reservation>,
396 terminal: Option<TaskTerminalReason>,
397}
398
399impl TaskGuard {
400 pub(crate) fn complete(&mut self) {
401 self.terminal = Some(TaskTerminalReason::Completed);
402 }
403
404 pub(crate) fn fail(&mut self) {
405 self.terminal = Some(TaskTerminalReason::Failed);
406 }
407}
408
409impl Drop for TaskGuard {
410 fn drop(&mut self) {
411 let reason = if std::thread::panicking() {
412 TaskTerminalReason::Panicked
413 } else {
414 self.terminal.unwrap_or(TaskTerminalReason::Cancelled)
415 };
416 let report = TaskTerminalReport {
417 class: self.class,
418 owner: self.owner.clone(),
419 scope: self.scope.clone(),
420 reason,
421 };
422 self.reservation.take();
426 self.supervisor.terminal(report, self.handler.as_ref());
427 }
428}
429
430pub(crate) fn terminal_handler(
431 handler: impl Fn(&TaskTerminalReport) + Send + Sync + 'static,
432) -> TerminalHandler {
433 Arc::new(handler)
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use crate::accounting::ResourceLimit;
440
441 fn supervisor(report_capacity: usize) -> TaskSupervisor {
442 let ledger = Arc::new(ResourceLedger::root(
443 "process",
444 [(
445 ResourceClass::Tasks,
446 ResourceLimit::new(4, "runtime.resources.maxTasks"),
447 )],
448 ));
449 TaskSupervisor::with_report_capacity(
450 ledger,
451 RuntimeMetrics::new(),
452 Arc::new(AtomicBool::new(true)),
453 Arc::new(Mutex::new(())),
454 report_capacity,
455 )
456 }
457
458 #[test]
459 fn failed_reports_are_bounded_and_metrics_are_low_cardinality() {
460 let supervisor = supervisor(2);
461 for generation in 1..=4 {
462 let mut guard = supervisor
463 .admit(TaskClass::Vm, TaskOwner::Vm { generation }, None)
464 .expect("admit");
465 guard.fail();
466 }
467 assert_eq!(supervisor.snapshot(TaskClass::Vm).failed, 4);
468 assert_eq!(supervisor.dropped_terminal_reports(), 2);
469 assert!(
470 supervisor
471 .state
472 .lock()
473 .expect("supervisor state")
474 .report_overflow_warned,
475 "repeated drops should retain one edge-triggered warning state"
476 );
477 let reports = supervisor.drain_terminal_reports();
478 assert_eq!(reports.len(), 2);
479 assert_eq!(reports[0].owner, TaskOwner::Vm { generation: 3 });
480 assert!(
481 !supervisor
482 .state
483 .lock()
484 .expect("supervisor state")
485 .report_overflow_warned,
486 "draining reports should re-arm the next overflow warning"
487 );
488 }
489
490 #[test]
491 fn successful_and_cancelled_tasks_do_not_consume_failure_report_capacity() {
492 let supervisor = supervisor(1);
493 for generation in 1..=4 {
494 let mut completed = supervisor
495 .admit(TaskClass::Vm, TaskOwner::Vm { generation }, None)
496 .expect("admit completed task");
497 completed.complete();
498
499 let cancelled = supervisor
500 .admit(TaskClass::Vm, TaskOwner::Vm { generation }, None)
501 .expect("admit cancelled task");
502 drop(cancelled);
503 }
504
505 let snapshot = supervisor.snapshot(TaskClass::Vm);
506 assert_eq!(snapshot.completed, 4);
507 assert_eq!(snapshot.cancelled, 4);
508 assert_eq!(supervisor.dropped_terminal_reports(), 0);
509 assert!(supervisor.drain_terminal_reports().is_empty());
510 }
511
512 #[test]
513 fn owner_handler_runs_after_terminal_accounting() {
514 let supervisor = supervisor(2);
515 let observed = Arc::new(Mutex::new(None));
516 let observed_for_handler = Arc::clone(&observed);
517 let handler = terminal_handler(move |report| {
518 *observed_for_handler.lock().expect("handler state") = Some(report.clone());
519 });
520 let guard = supervisor
521 .admit(
522 TaskClass::Socket,
523 TaskOwner::Capability {
524 id: 9,
525 generation: 4,
526 },
527 Some(handler),
528 )
529 .expect("admit");
530 drop(guard);
531 assert_eq!(supervisor.snapshot(TaskClass::Socket).cancelled, 1);
532 assert_eq!(
533 observed.lock().expect("observed").as_ref().unwrap().reason,
534 TaskTerminalReason::Cancelled
535 );
536 }
537
538 #[tokio::test]
539 async fn scoped_wait_ignores_other_scopes_and_cannot_miss_final_exit() {
540 let process = Arc::new(ResourceLedger::root(
541 "process",
542 [(
543 ResourceClass::Tasks,
544 ResourceLimit::new(4, "runtime.resources.maxTasks"),
545 )],
546 ));
547 let base = TaskSupervisor::new(
548 Arc::clone(&process),
549 RuntimeMetrics::new(),
550 Arc::new(AtomicBool::new(true)),
551 Arc::new(Mutex::new(())),
552 4_096,
553 );
554 let vm_1_ledger = Arc::new(ResourceLedger::child(
555 "vm=1 generation=1",
556 [(
557 ResourceClass::Tasks,
558 ResourceLimit::new(2, "limits.reactor.maxTasks"),
559 )],
560 Arc::clone(&process),
561 ));
562 let vm_2_ledger = Arc::new(ResourceLedger::child(
563 "vm=2 generation=1",
564 [(
565 ResourceClass::Tasks,
566 ResourceLimit::new(2, "limits.reactor.maxTasks"),
567 )],
568 Arc::clone(&process),
569 ));
570 let vm_1 = base.scoped(
571 Arc::clone(&vm_1_ledger),
572 Arc::new(AtomicBool::new(true)),
573 Arc::new(Mutex::new(())),
574 );
575 let vm_2 = base.scoped(
576 Arc::clone(&vm_2_ledger),
577 Arc::new(AtomicBool::new(true)),
578 Arc::new(Mutex::new(())),
579 );
580 let vm_1_guard = vm_1
581 .admit(TaskClass::Socket, TaskOwner::Vm { generation: 1 }, None)
582 .expect("vm 1 task");
583 let _vm_2_guard = vm_2
584 .admit(TaskClass::Socket, TaskOwner::Vm { generation: 1 }, None)
585 .expect("vm 2 task");
586 let waiter = tokio::spawn({
587 let vm_1 = vm_1.clone();
588 async move { vm_1.wait_empty().await }
589 });
590 tokio::task::yield_now().await;
591 assert!(!waiter.is_finished());
592 drop(vm_1_guard);
593 tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
594 .await
595 .expect("vm 1 waiter")
596 .expect("wait task");
597 assert_eq!(vm_1.active_scoped(), 0);
598 assert_eq!(vm_2.active_scoped(), 1);
599 assert_eq!(vm_1_ledger.usage(ResourceClass::Tasks).used, 0);
600 }
601}