1#![forbid(unsafe_code)]
2
3use std::cell::Cell;
10use std::fmt;
11use std::future::Future;
12use std::pin::Pin;
13use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
14use std::sync::{mpsc, Arc, Mutex, OnceLock};
15use std::task::{Context, Poll};
16use std::thread;
17use std::time::{Duration, Instant};
18
19use accounting::{LimitError, Reservation, ResourceClass, ResourceLedger, ResourceLimit};
20use fairness::{FairBudget, FairWorkBroker, FairnessConfig};
21use metrics::{
22 ExecutorMetricClass, RuntimeMetrics, TelemetryFallback, TelemetryFallbackCode,
23 TelemetrySeverity, TelemetrySubsystem, WatchdogMetric,
24};
25
26pub mod accounting;
27pub mod capability;
28pub mod fairness;
29pub mod metrics;
30pub mod readiness;
31pub mod supervision;
32
33pub use supervision::{
34 TaskClass, TaskClassSnapshot, TaskOwner, TaskSpawnError, TaskSupervisor, TaskTerminalReason,
35 TaskTerminalReport,
36};
37
38const DEFAULT_MAX_BLOCKING_JOB_BYTES: usize = 64 * 1024 * 1024;
39const DEFAULT_BLOCKING_JOB_TIMEOUT_MS: u64 = 30_000;
40const DEFAULT_MAX_BLOCKING_JOBS: usize = 1_028;
41const DEFAULT_MAX_QUEUED_BLOCKING_JOBS: usize = 1024;
42const DEFAULT_MAX_PROCESS_CAPABILITIES: usize = 16_384;
43const DEFAULT_MAX_PROCESS_SOCKETS: usize = 8_192;
44const DEFAULT_MAX_PROCESS_CONNECTIONS: usize = 8_192;
45const DEFAULT_MAX_PROCESS_SOCKET_BUFFERED_BYTES: usize = 1024 * 1024 * 1024;
46const DEFAULT_MAX_PROCESS_DATAGRAMS: usize = 65_536;
47const DEFAULT_MAX_PROCESS_TIMERS: usize = 65_536;
48const DEFAULT_MAX_PROCESS_TASKS: usize = 65_536;
49const DEFAULT_MAX_PROCESS_READY_HANDLES: usize = 16_384;
50const DEFAULT_MAX_PROCESS_HANDLE_COMMANDS: usize = 65_536;
51const DEFAULT_MAX_PROCESS_HANDLE_COMMAND_BYTES: usize = 256 * 1024 * 1024;
52const DEFAULT_MAX_PROCESS_BRIDGE_CALLS: usize = 65_536;
53const DEFAULT_MAX_PROCESS_BRIDGE_REQUEST_BYTES: usize = 256 * 1024 * 1024;
54const DEFAULT_MAX_PROCESS_BRIDGE_RESPONSE_BYTES: usize = 256 * 1024 * 1024;
55const DEFAULT_MAX_PROCESS_ASYNC_COMPLETIONS: usize = 65_536;
56const DEFAULT_MAX_PROCESS_ASYNC_COMPLETION_BYTES: usize = 256 * 1024 * 1024;
57const DEFAULT_MAX_PROCESS_UDP_DATAGRAMS: usize = 65_536;
58const DEFAULT_MAX_PROCESS_UDP_BYTES: usize = 256 * 1024 * 1024;
59const DEFAULT_MAX_PROCESS_TLS_BYTES: usize = 256 * 1024 * 1024;
60const DEFAULT_MAX_PROCESS_HTTP2_CONNECTIONS: usize = 4_096;
61const DEFAULT_MAX_PROCESS_HTTP2_STREAMS: usize = 65_536;
62const DEFAULT_MAX_PROCESS_HTTP2_BYTES: usize = 512 * 1024 * 1024;
63const DEFAULT_MAX_PROCESS_HTTP2_HEADER_BYTES: usize = 128 * 1024 * 1024;
64const DEFAULT_MAX_PROCESS_HTTP2_DATA_BYTES: usize = 512 * 1024 * 1024;
65const DEFAULT_MAX_PROCESS_HTTP2_COMMANDS: usize = 65_536;
66const DEFAULT_MAX_PROCESS_HTTP2_COMMAND_BYTES: usize = 256 * 1024 * 1024;
67const DEFAULT_MAX_PROCESS_HTTP2_EVENTS: usize = 65_536;
68const DEFAULT_MAX_PROCESS_HTTP2_EVENT_BYTES: usize = 512 * 1024 * 1024;
69const DEFAULT_TASK_POLL_WATCHDOG_MS: u64 = 100;
70const DEFAULT_MAX_TERMINAL_TASK_REPORTS: usize = 4_096;
71const DEFAULT_VM_EXECUTOR_TEARDOWN_TIMEOUT_MS: u64 = 5_000;
72pub const DEFAULT_PROTOCOL_MAX_INGRESS_FRAMES: usize = 128;
73pub const DEFAULT_PROTOCOL_MAX_INGRESS_BYTES: usize = 64 * 1024 * 1024;
74pub const DEFAULT_PROTOCOL_MAX_CONTROL_FRAMES: usize = 1_024;
75pub const DEFAULT_PROTOCOL_MAX_CONTROL_BYTES: usize = 64 * 1024 * 1024;
76pub const DEFAULT_PROTOCOL_MAX_EGRESS_FRAMES: usize = 4_096;
77pub const DEFAULT_PROTOCOL_MAX_EGRESS_BYTES: usize = 256 * 1024 * 1024;
78pub const DEFAULT_PROTOCOL_MAX_PENDING_RESPONSES: usize = 10_000;
79pub const DEFAULT_PROTOCOL_MAX_PENDING_RESPONSE_BYTES: usize = 256 * 1024 * 1024;
80pub const DEFAULT_PROTOCOL_MAX_PROCESS_EVENTS: usize = 10_000;
81pub const DEFAULT_PROTOCOL_MAX_OUTBOUND_REQUESTS: usize = 10_000;
82pub const DEFAULT_PROTOCOL_MAX_COMPLETED_RESPONSES: usize = 10_000;
83const DEFAULT_FAIRNESS_VM_OPERATIONS: usize = 64;
84const DEFAULT_FAIRNESS_VM_BYTES: usize = 1024 * 1024;
85const DEFAULT_FAIRNESS_CAPABILITY_OPERATIONS: usize = 16;
86const DEFAULT_FAIRNESS_CAPABILITY_BYTES: usize = 256 * 1024;
87const DEFAULT_FAIRNESS_MAX_VMS: usize = 4_096;
88const DEFAULT_FAIRNESS_MAX_CAPABILITIES_PER_VM: usize = 16_384;
89
90thread_local! {
91 static IS_AGENTOS_RUNTIME_WORKER: Cell<bool> = const { Cell::new(false) };
92}
93
94pub fn is_runtime_worker_thread() -> bool {
99 IS_AGENTOS_RUNTIME_WORKER.with(Cell::get)
100}
101
102#[derive(Clone, Debug, PartialEq, Eq)]
110pub struct RuntimeProtocolConfig {
111 pub max_ingress_frames: usize,
112 pub max_ingress_bytes: usize,
113 pub max_control_frames: usize,
114 pub max_control_bytes: usize,
115 pub max_egress_frames: usize,
116 pub max_egress_bytes: usize,
117 pub max_pending_responses: usize,
118 pub max_pending_response_bytes: usize,
119 pub max_process_events: usize,
120 pub max_outbound_requests: usize,
121 pub max_completed_responses: usize,
122}
123
124impl Default for RuntimeProtocolConfig {
125 fn default() -> Self {
126 Self {
127 max_ingress_frames: DEFAULT_PROTOCOL_MAX_INGRESS_FRAMES,
128 max_ingress_bytes: DEFAULT_PROTOCOL_MAX_INGRESS_BYTES,
129 max_control_frames: DEFAULT_PROTOCOL_MAX_CONTROL_FRAMES,
130 max_control_bytes: DEFAULT_PROTOCOL_MAX_CONTROL_BYTES,
131 max_egress_frames: DEFAULT_PROTOCOL_MAX_EGRESS_FRAMES,
132 max_egress_bytes: DEFAULT_PROTOCOL_MAX_EGRESS_BYTES,
133 max_pending_responses: DEFAULT_PROTOCOL_MAX_PENDING_RESPONSES,
134 max_pending_response_bytes: DEFAULT_PROTOCOL_MAX_PENDING_RESPONSE_BYTES,
135 max_process_events: DEFAULT_PROTOCOL_MAX_PROCESS_EVENTS,
136 max_outbound_requests: DEFAULT_PROTOCOL_MAX_OUTBOUND_REQUESTS,
137 max_completed_responses: DEFAULT_PROTOCOL_MAX_COMPLETED_RESPONSES,
138 }
139 }
140}
141
142#[derive(Clone, Debug, PartialEq, Eq)]
143pub struct RuntimeFairnessConfig {
144 pub vm_quantum_operations: usize,
145 pub vm_quantum_bytes: usize,
146 pub capability_quantum_operations: usize,
147 pub capability_quantum_bytes: usize,
148 pub max_vm_deficit_operations: usize,
149 pub max_vm_deficit_bytes: usize,
150 pub max_capability_deficit_operations: usize,
151 pub max_capability_deficit_bytes: usize,
152 pub max_vms: usize,
153 pub max_capabilities_per_vm: usize,
154}
155
156impl Default for RuntimeFairnessConfig {
157 fn default() -> Self {
158 Self {
159 vm_quantum_operations: DEFAULT_FAIRNESS_VM_OPERATIONS,
160 vm_quantum_bytes: DEFAULT_FAIRNESS_VM_BYTES,
161 capability_quantum_operations: DEFAULT_FAIRNESS_CAPABILITY_OPERATIONS,
162 capability_quantum_bytes: DEFAULT_FAIRNESS_CAPABILITY_BYTES,
163 max_vm_deficit_operations: DEFAULT_FAIRNESS_VM_OPERATIONS * 4,
164 max_vm_deficit_bytes: DEFAULT_FAIRNESS_VM_BYTES * 4,
165 max_capability_deficit_operations: DEFAULT_FAIRNESS_CAPABILITY_OPERATIONS * 4,
166 max_capability_deficit_bytes: DEFAULT_FAIRNESS_CAPABILITY_BYTES * 4,
167 max_vms: DEFAULT_FAIRNESS_MAX_VMS,
168 max_capabilities_per_vm: DEFAULT_FAIRNESS_MAX_CAPABILITIES_PER_VM,
169 }
170 }
171}
172
173impl RuntimeFairnessConfig {
174 fn scheduler_config(&self) -> FairnessConfig {
175 FairnessConfig {
176 vm_quantum: FairBudget::new(self.vm_quantum_operations, self.vm_quantum_bytes),
177 capability_quantum: FairBudget::new(
178 self.capability_quantum_operations,
179 self.capability_quantum_bytes,
180 ),
181 max_vm_deficit: FairBudget::new(
182 self.max_vm_deficit_operations,
183 self.max_vm_deficit_bytes,
184 ),
185 max_capability_deficit: FairBudget::new(
186 self.max_capability_deficit_operations,
187 self.max_capability_deficit_bytes,
188 ),
189 max_vms: self.max_vms,
190 max_capabilities_per_vm: self.max_capabilities_per_vm,
191 }
192 }
193}
194
195#[derive(Clone, Debug, PartialEq, Eq)]
196pub struct RuntimeResourceConfig {
197 pub max_capabilities: usize,
198 pub max_ready_handles: usize,
199 pub max_sockets: usize,
200 pub max_connections: usize,
201 pub max_socket_buffered_bytes: usize,
202 pub max_datagrams: usize,
203 pub max_timers: usize,
204 pub max_tasks: usize,
205 pub max_handle_commands: usize,
206 pub max_handle_command_bytes: usize,
207 pub max_bridge_calls: usize,
208 pub max_bridge_request_bytes: usize,
209 pub max_bridge_response_bytes: usize,
210 pub max_async_completions: usize,
211 pub max_async_completion_bytes: usize,
212 pub max_udp_datagrams: usize,
213 pub max_udp_bytes: usize,
214 pub max_tls_bytes: usize,
215 pub max_http2_connections: usize,
216 pub max_http2_streams: usize,
217 pub max_http2_buffered_bytes: usize,
218 pub max_http2_header_bytes: usize,
219 pub max_http2_data_bytes: usize,
220 pub max_http2_commands: usize,
221 pub max_http2_command_bytes: usize,
222 pub max_http2_events: usize,
223 pub max_http2_event_bytes: usize,
224}
225
226impl Default for RuntimeResourceConfig {
227 fn default() -> Self {
228 Self {
229 max_capabilities: DEFAULT_MAX_PROCESS_CAPABILITIES,
230 max_ready_handles: DEFAULT_MAX_PROCESS_READY_HANDLES,
231 max_sockets: DEFAULT_MAX_PROCESS_SOCKETS,
232 max_connections: DEFAULT_MAX_PROCESS_CONNECTIONS,
233 max_socket_buffered_bytes: DEFAULT_MAX_PROCESS_SOCKET_BUFFERED_BYTES,
234 max_datagrams: DEFAULT_MAX_PROCESS_DATAGRAMS,
235 max_timers: DEFAULT_MAX_PROCESS_TIMERS,
236 max_tasks: DEFAULT_MAX_PROCESS_TASKS,
237 max_handle_commands: DEFAULT_MAX_PROCESS_HANDLE_COMMANDS,
238 max_handle_command_bytes: DEFAULT_MAX_PROCESS_HANDLE_COMMAND_BYTES,
239 max_bridge_calls: DEFAULT_MAX_PROCESS_BRIDGE_CALLS,
240 max_bridge_request_bytes: DEFAULT_MAX_PROCESS_BRIDGE_REQUEST_BYTES,
241 max_bridge_response_bytes: DEFAULT_MAX_PROCESS_BRIDGE_RESPONSE_BYTES,
242 max_async_completions: DEFAULT_MAX_PROCESS_ASYNC_COMPLETIONS,
243 max_async_completion_bytes: DEFAULT_MAX_PROCESS_ASYNC_COMPLETION_BYTES,
244 max_udp_datagrams: DEFAULT_MAX_PROCESS_UDP_DATAGRAMS,
245 max_udp_bytes: DEFAULT_MAX_PROCESS_UDP_BYTES,
246 max_tls_bytes: DEFAULT_MAX_PROCESS_TLS_BYTES,
247 max_http2_connections: DEFAULT_MAX_PROCESS_HTTP2_CONNECTIONS,
248 max_http2_streams: DEFAULT_MAX_PROCESS_HTTP2_STREAMS,
249 max_http2_buffered_bytes: DEFAULT_MAX_PROCESS_HTTP2_BYTES,
250 max_http2_header_bytes: DEFAULT_MAX_PROCESS_HTTP2_HEADER_BYTES,
251 max_http2_data_bytes: DEFAULT_MAX_PROCESS_HTTP2_DATA_BYTES,
252 max_http2_commands: DEFAULT_MAX_PROCESS_HTTP2_COMMANDS,
253 max_http2_command_bytes: DEFAULT_MAX_PROCESS_HTTP2_COMMAND_BYTES,
254 max_http2_events: DEFAULT_MAX_PROCESS_HTTP2_EVENTS,
255 max_http2_event_bytes: DEFAULT_MAX_PROCESS_HTTP2_EVENT_BYTES,
256 }
257 }
258}
259
260impl RuntimeResourceConfig {
261 fn limits(&self) -> Vec<(ResourceClass, ResourceLimit)> {
262 vec![
263 (
264 ResourceClass::Capabilities,
265 ResourceLimit::new(self.max_capabilities, "runtime.resources.maxCapabilities"),
266 ),
267 (
268 ResourceClass::ReadyHandles,
269 ResourceLimit::new(self.max_ready_handles, "runtime.resources.maxReadyHandles"),
270 ),
271 (
272 ResourceClass::Sockets,
273 ResourceLimit::new(self.max_sockets, "runtime.resources.maxSockets"),
274 ),
275 (
276 ResourceClass::Connections,
277 ResourceLimit::new(self.max_connections, "runtime.resources.maxConnections"),
278 ),
279 (
280 ResourceClass::BufferedBytes,
281 ResourceLimit::new(
282 self.max_socket_buffered_bytes,
283 "runtime.resources.maxSocketBufferedBytes",
284 ),
285 ),
286 (
287 ResourceClass::Datagrams,
288 ResourceLimit::new(self.max_datagrams, "runtime.resources.maxDatagrams"),
289 ),
290 (
291 ResourceClass::Timers,
292 ResourceLimit::new(self.max_timers, "runtime.resources.maxTimers"),
293 ),
294 (
295 ResourceClass::Tasks,
296 ResourceLimit::new(self.max_tasks, "runtime.resources.maxTasks"),
297 ),
298 (
299 ResourceClass::HandleCommands,
300 ResourceLimit::new(
301 self.max_handle_commands,
302 "runtime.resources.maxHandleCommands",
303 ),
304 ),
305 (
306 ResourceClass::HandleCommandBytes,
307 ResourceLimit::new(
308 self.max_handle_command_bytes,
309 "runtime.resources.maxHandleCommandBytes",
310 ),
311 ),
312 (
313 ResourceClass::BridgeCalls,
314 ResourceLimit::new(self.max_bridge_calls, "runtime.resources.maxBridgeCalls"),
315 ),
316 (
317 ResourceClass::BridgeRequestBytes,
318 ResourceLimit::new(
319 self.max_bridge_request_bytes,
320 "runtime.resources.maxBridgeRequestBytes",
321 ),
322 ),
323 (
324 ResourceClass::BridgeResponseBytes,
325 ResourceLimit::new(
326 self.max_bridge_response_bytes,
327 "runtime.resources.maxBridgeResponseBytes",
328 ),
329 ),
330 (
331 ResourceClass::AsyncCompletions,
332 ResourceLimit::new(
333 self.max_async_completions,
334 "runtime.resources.maxAsyncCompletions",
335 ),
336 ),
337 (
338 ResourceClass::AsyncCompletionBytes,
339 ResourceLimit::new(
340 self.max_async_completion_bytes,
341 "runtime.resources.maxAsyncCompletionBytes",
342 ),
343 ),
344 (
345 ResourceClass::UdpDatagrams,
346 ResourceLimit::new(self.max_udp_datagrams, "runtime.resources.maxUdpDatagrams"),
347 ),
348 (
349 ResourceClass::UdpBytes,
350 ResourceLimit::new(self.max_udp_bytes, "runtime.resources.maxUdpBytes"),
351 ),
352 (
353 ResourceClass::TlsBytes,
354 ResourceLimit::new(self.max_tls_bytes, "runtime.resources.maxTlsBytes"),
355 ),
356 (
357 ResourceClass::Http2Connections,
358 ResourceLimit::new(
359 self.max_http2_connections,
360 "runtime.resources.maxHttp2Connections",
361 ),
362 ),
363 (
364 ResourceClass::Http2Streams,
365 ResourceLimit::new(self.max_http2_streams, "runtime.resources.maxHttp2Streams"),
366 ),
367 (
368 ResourceClass::Http2BufferedBytes,
369 ResourceLimit::new(
370 self.max_http2_buffered_bytes,
371 "runtime.resources.maxHttp2BufferedBytes",
372 ),
373 ),
374 (
375 ResourceClass::Http2HeaderBytes,
376 ResourceLimit::new(
377 self.max_http2_header_bytes,
378 "runtime.resources.maxHttp2HeaderBytes",
379 ),
380 ),
381 (
382 ResourceClass::Http2DataBytes,
383 ResourceLimit::new(
384 self.max_http2_data_bytes,
385 "runtime.resources.maxHttp2DataBytes",
386 ),
387 ),
388 (
389 ResourceClass::Http2Commands,
390 ResourceLimit::new(
391 self.max_http2_commands,
392 "runtime.resources.maxHttp2Commands",
393 ),
394 ),
395 (
396 ResourceClass::Http2CommandBytes,
397 ResourceLimit::new(
398 self.max_http2_command_bytes,
399 "runtime.resources.maxHttp2CommandBytes",
400 ),
401 ),
402 (
403 ResourceClass::Http2Events,
404 ResourceLimit::new(self.max_http2_events, "runtime.resources.maxHttp2Events"),
405 ),
406 (
407 ResourceClass::Http2EventBytes,
408 ResourceLimit::new(
409 self.max_http2_event_bytes,
410 "runtime.resources.maxHttp2EventBytes",
411 ),
412 ),
413 ]
414 }
415}
416
417#[derive(Clone, Debug, PartialEq, Eq)]
418pub struct RuntimeConfig {
419 pub worker_threads: usize,
420 pub max_active_vm_executors: usize,
421 pub vm_executor_teardown_timeout_ms: u64,
422 pub blocking_worker_threads: usize,
423 pub max_blocking_jobs: usize,
424 pub max_queued_blocking_jobs: usize,
425 pub max_blocking_job_bytes: usize,
426 pub blocking_job_timeout_ms: u64,
427 pub task_poll_watchdog_ms: u64,
428 pub max_terminal_task_reports: usize,
429 pub protocol: RuntimeProtocolConfig,
430 pub resources: RuntimeResourceConfig,
431 pub fairness: RuntimeFairnessConfig,
432}
433
434impl Default for RuntimeConfig {
435 fn default() -> Self {
436 let available = thread::available_parallelism()
437 .map(usize::from)
438 .unwrap_or(1);
439 Self {
440 worker_threads: available.clamp(1, 4),
441 max_active_vm_executors: available.max(1),
442 vm_executor_teardown_timeout_ms: DEFAULT_VM_EXECUTOR_TEARDOWN_TIMEOUT_MS,
443 blocking_worker_threads: available.clamp(1, 4),
444 max_blocking_jobs: DEFAULT_MAX_BLOCKING_JOBS,
445 max_queued_blocking_jobs: DEFAULT_MAX_QUEUED_BLOCKING_JOBS,
446 max_blocking_job_bytes: DEFAULT_MAX_BLOCKING_JOB_BYTES,
447 blocking_job_timeout_ms: DEFAULT_BLOCKING_JOB_TIMEOUT_MS,
448 task_poll_watchdog_ms: DEFAULT_TASK_POLL_WATCHDOG_MS,
449 max_terminal_task_reports: DEFAULT_MAX_TERMINAL_TASK_REPORTS,
450 protocol: RuntimeProtocolConfig::default(),
451 resources: RuntimeResourceConfig::default(),
452 fairness: RuntimeFairnessConfig::default(),
453 }
454 }
455}
456
457impl RuntimeConfig {
458 pub fn validate(&self) -> Result<(), RuntimeBuildError> {
459 for (field, value) in [
460 ("runtime.workerThreads", self.worker_threads),
461 (
462 "runtime.executor.maxActiveVms",
463 self.max_active_vm_executors,
464 ),
465 (
466 "runtime.blocking.workerThreads",
467 self.blocking_worker_threads,
468 ),
469 ("runtime.blocking.maxJobs", self.max_blocking_jobs),
470 (
471 "runtime.blocking.maxQueuedJobs",
472 self.max_queued_blocking_jobs,
473 ),
474 (
475 "runtime.blocking.maxQueuedBytes",
476 self.max_blocking_job_bytes,
477 ),
478 (
479 "runtime.tasks.maxTerminalReports",
480 self.max_terminal_task_reports,
481 ),
482 (
483 "runtime.protocol.maxIngressFrames",
484 self.protocol.max_ingress_frames,
485 ),
486 (
487 "runtime.protocol.maxIngressBytes",
488 self.protocol.max_ingress_bytes,
489 ),
490 (
491 "runtime.protocol.maxControlFrames",
492 self.protocol.max_control_frames,
493 ),
494 (
495 "runtime.protocol.maxControlBytes",
496 self.protocol.max_control_bytes,
497 ),
498 (
499 "runtime.protocol.maxEgressFrames",
500 self.protocol.max_egress_frames,
501 ),
502 (
503 "runtime.protocol.maxEgressBytes",
504 self.protocol.max_egress_bytes,
505 ),
506 (
507 "runtime.protocol.maxPendingResponses",
508 self.protocol.max_pending_responses,
509 ),
510 (
511 "runtime.protocol.maxPendingResponseBytes",
512 self.protocol.max_pending_response_bytes,
513 ),
514 (
515 "runtime.protocol.maxProcessEvents",
516 self.protocol.max_process_events,
517 ),
518 (
519 "runtime.protocol.maxOutboundRequests",
520 self.protocol.max_outbound_requests,
521 ),
522 (
523 "runtime.protocol.maxCompletedResponses",
524 self.protocol.max_completed_responses,
525 ),
526 (
527 "runtime.resources.maxCapabilities",
528 self.resources.max_capabilities,
529 ),
530 (
531 "runtime.resources.maxReadyHandles",
532 self.resources.max_ready_handles,
533 ),
534 ("runtime.resources.maxSockets", self.resources.max_sockets),
535 (
536 "runtime.resources.maxConnections",
537 self.resources.max_connections,
538 ),
539 (
540 "runtime.resources.maxSocketBufferedBytes",
541 self.resources.max_socket_buffered_bytes,
542 ),
543 (
544 "runtime.resources.maxDatagrams",
545 self.resources.max_datagrams,
546 ),
547 ("runtime.resources.maxTimers", self.resources.max_timers),
548 ("runtime.resources.maxTasks", self.resources.max_tasks),
549 (
550 "runtime.resources.maxHandleCommands",
551 self.resources.max_handle_commands,
552 ),
553 (
554 "runtime.resources.maxHandleCommandBytes",
555 self.resources.max_handle_command_bytes,
556 ),
557 (
558 "runtime.resources.maxBridgeCalls",
559 self.resources.max_bridge_calls,
560 ),
561 (
562 "runtime.resources.maxBridgeRequestBytes",
563 self.resources.max_bridge_request_bytes,
564 ),
565 (
566 "runtime.resources.maxBridgeResponseBytes",
567 self.resources.max_bridge_response_bytes,
568 ),
569 (
570 "runtime.resources.maxAsyncCompletions",
571 self.resources.max_async_completions,
572 ),
573 (
574 "runtime.resources.maxAsyncCompletionBytes",
575 self.resources.max_async_completion_bytes,
576 ),
577 (
578 "runtime.resources.maxUdpDatagrams",
579 self.resources.max_udp_datagrams,
580 ),
581 (
582 "runtime.resources.maxUdpBytes",
583 self.resources.max_udp_bytes,
584 ),
585 (
586 "runtime.resources.maxTlsBytes",
587 self.resources.max_tls_bytes,
588 ),
589 (
590 "runtime.resources.maxHttp2Connections",
591 self.resources.max_http2_connections,
592 ),
593 (
594 "runtime.resources.maxHttp2Streams",
595 self.resources.max_http2_streams,
596 ),
597 (
598 "runtime.resources.maxHttp2BufferedBytes",
599 self.resources.max_http2_buffered_bytes,
600 ),
601 (
602 "runtime.resources.maxHttp2HeaderBytes",
603 self.resources.max_http2_header_bytes,
604 ),
605 (
606 "runtime.resources.maxHttp2DataBytes",
607 self.resources.max_http2_data_bytes,
608 ),
609 (
610 "runtime.resources.maxHttp2Commands",
611 self.resources.max_http2_commands,
612 ),
613 (
614 "runtime.resources.maxHttp2CommandBytes",
615 self.resources.max_http2_command_bytes,
616 ),
617 (
618 "runtime.resources.maxHttp2Events",
619 self.resources.max_http2_events,
620 ),
621 (
622 "runtime.resources.maxHttp2EventBytes",
623 self.resources.max_http2_event_bytes,
624 ),
625 ] {
626 if value == 0 {
627 return Err(RuntimeBuildError(format!(
628 "ERR_AGENTOS_RUNTIME_CONFIG: {field} must be greater than zero"
629 )));
630 }
631 }
632 if self.task_poll_watchdog_ms == 0 {
633 return Err(RuntimeBuildError(String::from(
634 "ERR_AGENTOS_RUNTIME_CONFIG: runtime.watchdog.taskPollMs must be greater than zero",
635 )));
636 }
637 if self.vm_executor_teardown_timeout_ms == 0 {
638 return Err(RuntimeBuildError(String::from(
639 "ERR_AGENTOS_RUNTIME_CONFIG: runtime.executor.teardownTimeoutMs must be greater than zero",
640 )));
641 }
642 if self.blocking_job_timeout_ms == 0 {
643 return Err(RuntimeBuildError(String::from(
644 "ERR_AGENTOS_RUNTIME_CONFIG: runtime.blocking.jobTimeoutMs must be greater than zero",
645 )));
646 }
647 for (field, value) in [
648 (
649 "runtime.fairness.vmQuantumOperations",
650 self.fairness.vm_quantum_operations,
651 ),
652 (
653 "runtime.fairness.vmQuantumBytes",
654 self.fairness.vm_quantum_bytes,
655 ),
656 (
657 "runtime.fairness.capabilityQuantumOperations",
658 self.fairness.capability_quantum_operations,
659 ),
660 (
661 "runtime.fairness.capabilityQuantumBytes",
662 self.fairness.capability_quantum_bytes,
663 ),
664 (
665 "runtime.fairness.maxVmDeficitOperations",
666 self.fairness.max_vm_deficit_operations,
667 ),
668 (
669 "runtime.fairness.maxVmDeficitBytes",
670 self.fairness.max_vm_deficit_bytes,
671 ),
672 (
673 "runtime.fairness.maxCapabilityDeficitOperations",
674 self.fairness.max_capability_deficit_operations,
675 ),
676 (
677 "runtime.fairness.maxCapabilityDeficitBytes",
678 self.fairness.max_capability_deficit_bytes,
679 ),
680 ("runtime.fairness.maxVms", self.fairness.max_vms),
681 (
682 "runtime.fairness.maxCapabilitiesPerVm",
683 self.fairness.max_capabilities_per_vm,
684 ),
685 ] {
686 if value == 0 {
687 return Err(RuntimeBuildError(format!(
688 "ERR_AGENTOS_RUNTIME_CONFIG: {field} must be greater than zero"
689 )));
690 }
691 }
692 if self.fairness.max_vm_deficit_operations < self.fairness.vm_quantum_operations
693 || self.fairness.max_vm_deficit_bytes < self.fairness.vm_quantum_bytes
694 || self.fairness.max_capability_deficit_operations
695 < self.fairness.capability_quantum_operations
696 || self.fairness.max_capability_deficit_bytes < self.fairness.capability_quantum_bytes
697 {
698 return Err(RuntimeBuildError(String::from(
699 "ERR_AGENTOS_RUNTIME_CONFIG: fairness deficits must be at least their quantum",
700 )));
701 }
702 if self.max_blocking_jobs < self.blocking_worker_threads {
703 return Err(RuntimeBuildError(format!(
704 "ERR_AGENTOS_RUNTIME_CONFIG: runtime.blocking.maxJobs ({}) must be >= runtime.blocking.workerThreads ({})",
705 self.max_blocking_jobs, self.blocking_worker_threads
706 )));
707 }
708 if self.max_queued_blocking_jobs > self.max_blocking_jobs {
709 return Err(RuntimeBuildError(format!(
710 "ERR_AGENTOS_RUNTIME_CONFIG: runtime.blocking.maxQueuedJobs ({}) must be <= runtime.blocking.maxJobs ({})",
711 self.max_queued_blocking_jobs, self.max_blocking_jobs
712 )));
713 }
714 Ok(())
715 }
716}
717
718#[derive(Clone, Debug, PartialEq, Eq)]
719pub struct RuntimeBuildError(String);
720
721impl fmt::Display for RuntimeBuildError {
722 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
723 formatter.write_str(&self.0)
724 }
725}
726
727impl std::error::Error for RuntimeBuildError {}
728
729#[derive(Debug, Clone, PartialEq, Eq)]
730pub enum BlockingJobError {
731 ResourceLimit(LimitError),
732 Capacity { limit: usize },
733 ShuttingDown,
734 WorkerDropped,
735 TimedOut { timeout: Duration },
736}
737
738impl fmt::Display for BlockingJobError {
739 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
740 match self {
741 Self::ResourceLimit(error) => error.fmt(formatter),
742 Self::Capacity { limit } => write!(
743 formatter,
744 "ERR_AGENTOS_BLOCKING_JOB_LIMIT: blocking executor queue exceeded {limit} jobs; raise runtime.blocking.maxQueuedJobs"
745 ),
746 Self::ShuttingDown => formatter.write_str(
747 "ERR_AGENTOS_BLOCKING_EXECUTOR_SHUTDOWN: blocking executor is shutting down",
748 ),
749 Self::WorkerDropped => formatter.write_str(
750 "ERR_AGENTOS_BLOCKING_WORKER_DROPPED: blocking worker ended without a result",
751 ),
752 Self::TimedOut { timeout } => write!(
753 formatter,
754 "ERR_AGENTOS_BLOCKING_JOB_TIMEOUT: blocking job exceeded its {}ms deadline",
755 timeout.as_millis()
756 ),
757 }
758 }
759}
760
761impl std::error::Error for BlockingJobError {}
762
763type BlockingOperation = Box<dyn FnOnce(Reservation, Reservation) + Send + 'static>;
764
765struct BlockingJob {
766 operation: BlockingOperation,
767 _slot: Reservation,
768 _bytes: Reservation,
769}
770
771struct BlockingExecutorState {
772 metrics: RuntimeMetrics,
773 queued: AtomicUsize,
774 active: AtomicUsize,
775}
776
777struct BlockingExecutorInner {
778 sender: Mutex<Option<mpsc::SyncSender<BlockingJob>>>,
779 workers: Mutex<Vec<thread::JoinHandle<()>>>,
780 max_queued_jobs: usize,
781 max_bytes: usize,
782 state: Arc<BlockingExecutorState>,
783}
784
785impl Drop for BlockingExecutorInner {
786 fn drop(&mut self) {
787 self.sender.get_mut().ok().and_then(Option::take);
788 let workers = self
789 .workers
790 .get_mut()
791 .map(std::mem::take)
792 .unwrap_or_default();
793 for worker in workers {
794 if worker.join().is_err() {
795 eprintln!("ERR_AGENTOS_BLOCKING_WORKER_PANIC: blocking executor worker panicked");
796 }
797 }
798 }
799}
800
801#[derive(Clone)]
802pub struct BlockingExecutor {
803 inner: Arc<BlockingExecutorInner>,
804 resources: Arc<ResourceLedger>,
805 admission_open: Arc<AtomicBool>,
806 admission_gate: Arc<Mutex<()>>,
807}
808
809impl fmt::Debug for BlockingExecutor {
810 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
811 formatter
812 .debug_struct("BlockingExecutor")
813 .field("worker_count", &self.worker_count())
814 .field("max_queued_jobs", &self.inner.max_queued_jobs)
815 .field("max_bytes", &self.inner.max_bytes)
816 .field("reserved_bytes", &self.reserved_bytes())
817 .finish()
818 }
819}
820
821impl BlockingExecutor {
822 fn new(
823 config: &RuntimeConfig,
824 resources: Arc<ResourceLedger>,
825 metrics: RuntimeMetrics,
826 admission_open: Arc<AtomicBool>,
827 admission_gate: Arc<Mutex<()>>,
828 ) -> Result<Self, RuntimeBuildError> {
829 let (sender, receiver) = mpsc::sync_channel::<BlockingJob>(config.max_queued_blocking_jobs);
830 let receiver = Arc::new(Mutex::new(receiver));
831 let executor_state = Arc::new(BlockingExecutorState {
832 metrics,
833 queued: AtomicUsize::new(0),
834 active: AtomicUsize::new(0),
835 });
836 let mut workers = Vec::with_capacity(config.blocking_worker_threads);
837 for index in 0..config.blocking_worker_threads {
838 let receiver = Arc::clone(&receiver);
839 let executor_state = Arc::clone(&executor_state);
840 let worker = thread::Builder::new()
842 .name(format!("agentos-blocking-{index}"))
843 .spawn(move || loop {
844 let job = match receiver.lock() {
845 Ok(receiver) => receiver.recv(),
846 Err(_) => {
847 eprintln!(
848 "ERR_AGENTOS_BLOCKING_QUEUE_POISONED: blocking job receiver lock poisoned"
849 );
850 break;
851 }
852 };
853 match job {
854 Ok(job) => {
855 decrement_saturating(&executor_state.queued);
856 executor_state.active.fetch_add(1, Ordering::Relaxed);
857 executor_state.metrics.observe_executor(
858 ExecutorMetricClass::Blocking,
859 executor_state.active.load(Ordering::Relaxed),
860 executor_state.queued.load(Ordering::Relaxed),
861 );
862 let BlockingJob {
863 operation,
864 _slot,
865 _bytes,
866 } = job;
867 if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
868 operation(_slot, _bytes)
869 }))
870 .is_err()
871 {
872 eprintln!(
873 "ERR_AGENTOS_BLOCKING_JOB_PANIC: blocking job panicked"
874 );
875 }
876 decrement_saturating(&executor_state.active);
877 executor_state.metrics.observe_executor(
878 ExecutorMetricClass::Blocking,
879 executor_state.active.load(Ordering::Relaxed),
880 executor_state.queued.load(Ordering::Relaxed),
881 );
882 }
883 Err(_) => break,
884 }
885 })
886 .map_err(|error| {
887 RuntimeBuildError(format!(
888 "ERR_AGENTOS_BLOCKING_WORKER_START: failed to start blocking worker {index}: {error}"
889 ))
890 })?;
891 workers.push(worker);
892 }
893
894 Ok(Self {
895 inner: Arc::new(BlockingExecutorInner {
896 sender: Mutex::new(Some(sender)),
897 workers: Mutex::new(workers),
898 max_queued_jobs: config.max_queued_blocking_jobs,
899 max_bytes: config.max_blocking_job_bytes,
900 state: executor_state,
901 }),
902 resources,
903 admission_open,
904 admission_gate,
905 })
906 }
907
908 pub fn worker_count(&self) -> usize {
909 self.inner
910 .workers
911 .lock()
912 .map(|workers| workers.len())
913 .unwrap_or(0)
914 }
915
916 pub fn reserved_bytes(&self) -> usize {
917 self.resources.usage(ResourceClass::ExecutorBytes).used
918 }
919
920 pub fn scoped(
923 &self,
924 resources: Arc<ResourceLedger>,
925 admission_open: Arc<AtomicBool>,
926 admission_gate: Arc<Mutex<()>>,
927 ) -> Self {
928 Self {
929 inner: Arc::clone(&self.inner),
930 resources,
931 admission_open,
932 admission_gate,
933 }
934 }
935
936 pub fn submit<F>(&self, reserved_bytes: usize, operation: F) -> Result<(), BlockingJobError>
937 where
938 F: FnOnce() + Send + 'static,
939 {
940 let (admission, _slot, _bytes) = self.reserve(reserved_bytes)?;
941 let job = BlockingJob {
942 operation: Box::new(move |_slot, _bytes| operation()),
943 _slot,
944 _bytes,
945 };
946 let result = self.try_enqueue(job);
947 drop(admission);
948 result
949 }
950
951 pub async fn run<T, F>(
952 &self,
953 reserved_bytes: usize,
954 operation: F,
955 ) -> Result<T, BlockingJobError>
956 where
957 T: Send + 'static,
958 F: FnOnce() -> T + Send + 'static,
959 {
960 let (admission, _slot, _bytes) = self.reserve(reserved_bytes)?;
961 let (result_tx, result_rx) = tokio::sync::oneshot::channel();
962 let job = BlockingJob {
963 operation: Box::new(move |slot, bytes| {
964 let result = operation();
965 drop(slot);
969 drop(bytes);
970 if result_tx.send(result).is_err() {
971 eprintln!(
972 "ERR_AGENTOS_BLOCKING_RESULT_DROPPED: asynchronous caller stopped waiting"
973 );
974 }
975 }),
976 _slot,
977 _bytes,
978 };
979
980 let enqueue_result = self.try_enqueue(job);
981 drop(admission);
982 enqueue_result?;
983
984 result_rx.await.map_err(|_| BlockingJobError::WorkerDropped)
985 }
986
987 pub fn run_sync<T, F>(
988 &self,
989 reserved_bytes: usize,
990 timeout: Duration,
991 operation: F,
992 ) -> Result<T, BlockingJobError>
993 where
994 T: Send + 'static,
995 F: FnOnce() -> T + Send + 'static,
996 {
997 let (admission, _slot, _bytes) = self.reserve(reserved_bytes)?;
998 let (result_tx, result_rx) = mpsc::sync_channel(1);
999 let job = BlockingJob {
1000 operation: Box::new(move |slot, bytes| {
1001 let result = operation();
1002 drop(slot);
1005 drop(bytes);
1006 if result_tx.send(result).is_err() {
1007 eprintln!(
1008 "ERR_AGENTOS_BLOCKING_RESULT_DROPPED: synchronous caller stopped waiting"
1009 );
1010 }
1011 }),
1012 _slot,
1013 _bytes,
1014 };
1015
1016 let enqueue_result = self.try_enqueue(job);
1017 drop(admission);
1018 enqueue_result?;
1019
1020 result_rx
1021 .recv_timeout(timeout)
1022 .map_err(|error| match error {
1023 mpsc::RecvTimeoutError::Timeout => BlockingJobError::TimedOut { timeout },
1024 mpsc::RecvTimeoutError::Disconnected => BlockingJobError::WorkerDropped,
1025 })
1026 }
1027
1028 fn reserve(
1029 &self,
1030 requested: usize,
1031 ) -> Result<(std::sync::MutexGuard<'_, ()>, Reservation, Reservation), BlockingJobError> {
1032 let admission = self
1036 .admission_gate
1037 .lock()
1038 .map_err(|_| BlockingJobError::ShuttingDown)?;
1039 if !self.admission_open.load(Ordering::Acquire) {
1040 return Err(BlockingJobError::ShuttingDown);
1041 }
1042 let slot = self
1043 .resources
1044 .reserve(ResourceClass::ExecutorSlots, 1)
1045 .map_err(BlockingJobError::ResourceLimit)?;
1046 let bytes = self
1047 .resources
1048 .reserve(ResourceClass::ExecutorBytes, requested)
1049 .map_err(BlockingJobError::ResourceLimit)?;
1050 Ok((admission, slot, bytes))
1051 }
1052
1053 fn try_enqueue(&self, job: BlockingJob) -> Result<(), BlockingJobError> {
1054 let sender = self
1055 .inner
1056 .sender
1057 .lock()
1058 .map_err(|_| BlockingJobError::ShuttingDown)?;
1059 let sender = sender.as_ref().ok_or(BlockingJobError::ShuttingDown)?;
1060 self.record_enqueued();
1061 if let Err(error) = sender.try_send(job) {
1062 self.record_enqueue_failed();
1063 return Err(match error {
1064 mpsc::TrySendError::Full(_) => BlockingJobError::Capacity {
1065 limit: self.inner.max_queued_jobs,
1066 },
1067 mpsc::TrySendError::Disconnected(_) => BlockingJobError::ShuttingDown,
1068 });
1069 }
1070 Ok(())
1071 }
1072
1073 fn record_enqueued(&self) {
1074 self.inner.state.queued.fetch_add(1, Ordering::Relaxed);
1075 self.observe_executor();
1076 }
1077
1078 fn record_enqueue_failed(&self) {
1079 decrement_saturating(&self.inner.state.queued);
1080 self.observe_executor();
1081 }
1082
1083 fn observe_executor(&self) {
1084 self.inner.state.metrics.observe_executor(
1085 ExecutorMetricClass::Blocking,
1086 self.inner.state.active.load(Ordering::Relaxed),
1087 self.inner.state.queued.load(Ordering::Relaxed),
1088 );
1089 }
1090}
1091
1092fn decrement_saturating(counter: &AtomicUsize) {
1093 let mut current = counter.load(Ordering::Relaxed);
1094 while current != 0 {
1095 match counter.compare_exchange_weak(
1096 current,
1097 current - 1,
1098 Ordering::Relaxed,
1099 Ordering::Relaxed,
1100 ) {
1101 Ok(_) => return,
1102 Err(observed) => current = observed,
1103 }
1104 }
1105}
1106
1107#[derive(Clone, Debug)]
1108pub struct RuntimeContext {
1109 handle: tokio::runtime::Handle,
1110 blocking: BlockingExecutor,
1111 resources: Arc<ResourceLedger>,
1112 tasks: TaskSupervisor,
1113 metrics: RuntimeMetrics,
1114 fairness: FairWorkBroker,
1115 terminal_failure: Arc<Mutex<Option<TaskTerminalReport>>>,
1116 task_poll_watchdog: Duration,
1117 max_active_vm_executors: usize,
1118 vm_executor_teardown_timeout: Duration,
1119 blocking_job_timeout: Duration,
1120 admission_open: Arc<AtomicBool>,
1121 admission_closed: Arc<tokio::sync::Notify>,
1122 next_vm_generation: Arc<AtomicU64>,
1123 default_owner: TaskOwner,
1124}
1125
1126impl RuntimeContext {
1127 pub fn handle(&self) -> &tokio::runtime::Handle {
1128 &self.handle
1129 }
1130
1131 pub fn blocking(&self) -> &BlockingExecutor {
1132 &self.blocking
1133 }
1134
1135 pub fn resources(&self) -> &Arc<ResourceLedger> {
1137 &self.resources
1138 }
1139
1140 pub fn tasks(&self) -> &TaskSupervisor {
1141 &self.tasks
1142 }
1143
1144 pub fn metrics(&self) -> &RuntimeMetrics {
1145 &self.metrics
1146 }
1147
1148 pub fn max_active_vm_executors(&self) -> usize {
1149 self.max_active_vm_executors
1150 }
1151
1152 pub fn vm_executor_teardown_timeout(&self) -> Duration {
1153 self.vm_executor_teardown_timeout
1154 }
1155
1156 pub fn blocking_job_timeout(&self) -> Duration {
1157 self.blocking_job_timeout
1158 }
1159
1160 pub fn fairness(&self) -> &FairWorkBroker {
1161 &self.fairness
1162 }
1163
1164 pub fn allocate_vm_generation(&self) -> Result<u64, RuntimeBuildError> {
1168 self.next_vm_generation
1169 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
1170 current.checked_add(1)
1171 })
1172 .map(|previous| previous + 1)
1173 .map_err(|_| {
1174 RuntimeBuildError(String::from(
1175 "ERR_AGENTOS_VM_GENERATION_EXHAUSTED: process VM generation counter overflowed",
1176 ))
1177 })
1178 }
1179
1180 pub fn vm_generation(&self) -> Option<u64> {
1184 match &self.default_owner {
1185 TaskOwner::Vm { generation } => Some(*generation),
1186 _ => None,
1187 }
1188 }
1189
1190 pub fn terminal_failure(&self) -> Option<TaskTerminalReport> {
1193 self.terminal_failure
1194 .lock()
1195 .map(|failure| failure.clone())
1196 .unwrap_or_else(|_| {
1197 eprintln!(
1198 "ERR_AGENTOS_TASK_FAILURE_LATCH_POISONED: scope={}",
1199 self.resources.scope()
1200 );
1201 Some(TaskTerminalReport {
1202 class: TaskClass::Runtime,
1203 owner: self.default_owner.clone(),
1204 scope: self.resources.scope().to_owned(),
1205 reason: TaskTerminalReason::Panicked,
1206 })
1207 })
1208 }
1209
1210 pub fn close_admission(&self) {
1214 self.tasks.close_admission();
1217 self.admission_closed.notify_waiters();
1218 }
1219
1220 pub fn admission_is_open(&self) -> bool {
1221 self.admission_open.load(Ordering::Acquire)
1222 }
1223
1224 pub async fn admission_closed(&self) {
1231 loop {
1232 let notified = self.admission_closed.notified();
1233 if !self.admission_is_open() {
1234 return;
1235 }
1236 notified.await;
1237 }
1238 }
1239
1240 pub fn scoped(&self, resources: Arc<ResourceLedger>) -> Self {
1243 let admission_open = Arc::new(AtomicBool::new(true));
1244 let admission_gate = Arc::new(Mutex::new(()));
1245 Self {
1246 handle: self.handle.clone(),
1247 blocking: self.blocking.scoped(
1248 Arc::clone(&resources),
1249 Arc::clone(&admission_open),
1250 Arc::clone(&admission_gate),
1251 ),
1252 resources: Arc::clone(&resources),
1253 tasks: self
1254 .tasks
1255 .scoped(resources, Arc::clone(&admission_open), admission_gate),
1256 metrics: self.metrics.clone(),
1257 fairness: self.fairness.clone(),
1258 terminal_failure: Arc::new(Mutex::new(None)),
1259 task_poll_watchdog: self.task_poll_watchdog,
1260 max_active_vm_executors: self.max_active_vm_executors,
1261 vm_executor_teardown_timeout: self.vm_executor_teardown_timeout,
1262 blocking_job_timeout: self.blocking_job_timeout,
1263 admission_open,
1264 admission_closed: Arc::new(tokio::sync::Notify::new()),
1265 next_vm_generation: Arc::clone(&self.next_vm_generation),
1266 default_owner: self.default_owner.clone(),
1267 }
1268 }
1269
1270 pub fn scoped_for_vm(&self, resources: Arc<ResourceLedger>, generation: u64) -> Self {
1271 let mut scoped = self.scoped(resources);
1272 scoped.default_owner = TaskOwner::Vm { generation };
1273 scoped
1274 }
1275
1276 pub fn spawn<F>(
1277 &self,
1278 class: TaskClass,
1279 future: F,
1280 ) -> Result<tokio::task::JoinHandle<F::Output>, TaskSpawnError>
1281 where
1282 F: Future + Send + 'static,
1283 F::Output: Send + 'static,
1284 {
1285 let failure_latch = Arc::clone(&self.terminal_failure);
1286 let handler = supervision::terminal_handler(move |report| {
1287 latch_terminal_failure(&failure_latch, report);
1288 });
1289 let mut guard = self
1290 .tasks
1291 .admit(class, self.default_owner.clone(), Some(handler))?;
1292 let future =
1293 WatchdogFuture::new(future, class, self.task_poll_watchdog, self.metrics.clone());
1294 Ok(self.handle.spawn(async move {
1295 let output = future.await;
1296 guard.complete();
1297 output
1298 }))
1299 }
1300
1301 pub fn spawn_result<F, T, E>(
1302 &self,
1303 class: TaskClass,
1304 future: F,
1305 ) -> Result<tokio::task::JoinHandle<Result<T, E>>, TaskSpawnError>
1306 where
1307 F: Future<Output = Result<T, E>> + Send + 'static,
1308 T: Send + 'static,
1309 E: Send + 'static,
1310 {
1311 let failure_latch = Arc::clone(&self.terminal_failure);
1312 let handler = supervision::terminal_handler(move |report| {
1313 latch_terminal_failure(&failure_latch, report);
1314 });
1315 let mut guard = self
1316 .tasks
1317 .admit(class, self.default_owner.clone(), Some(handler))?;
1318 let future =
1319 WatchdogFuture::new(future, class, self.task_poll_watchdog, self.metrics.clone());
1320 Ok(self.handle.spawn(async move {
1321 let output = future.await;
1322 if output.is_ok() {
1323 guard.complete();
1324 } else {
1325 guard.fail();
1326 }
1327 output
1328 }))
1329 }
1330
1331 pub fn spawn_owned<F, H>(
1335 &self,
1336 class: TaskClass,
1337 owner: TaskOwner,
1338 on_terminal: H,
1339 future: F,
1340 ) -> Result<tokio::task::JoinHandle<F::Output>, TaskSpawnError>
1341 where
1342 F: Future + Send + 'static,
1343 F::Output: Send + 'static,
1344 H: Fn(&TaskTerminalReport) + Send + Sync + 'static,
1345 {
1346 let failure_latch = Arc::clone(&self.terminal_failure);
1347 let handler = supervision::terminal_handler(move |report| {
1348 latch_terminal_failure(&failure_latch, report);
1349 on_terminal(report);
1350 });
1351 let mut guard = self.tasks.admit(class, owner, Some(handler))?;
1352 let future =
1353 WatchdogFuture::new(future, class, self.task_poll_watchdog, self.metrics.clone());
1354 Ok(self.handle.spawn(async move {
1355 let output = future.await;
1356 guard.complete();
1357 output
1358 }))
1359 }
1360
1361 pub fn spawn_owned_result<F, T, E, H>(
1362 &self,
1363 class: TaskClass,
1364 owner: TaskOwner,
1365 on_terminal: H,
1366 future: F,
1367 ) -> Result<tokio::task::JoinHandle<Result<T, E>>, TaskSpawnError>
1368 where
1369 F: Future<Output = Result<T, E>> + Send + 'static,
1370 T: Send + 'static,
1371 E: Send + 'static,
1372 H: Fn(&TaskTerminalReport) + Send + Sync + 'static,
1373 {
1374 let failure_latch = Arc::clone(&self.terminal_failure);
1375 let handler = supervision::terminal_handler(move |report| {
1376 latch_terminal_failure(&failure_latch, report);
1377 on_terminal(report);
1378 });
1379 let mut guard = self.tasks.admit(class, owner, Some(handler))?;
1380 let future =
1381 WatchdogFuture::new(future, class, self.task_poll_watchdog, self.metrics.clone());
1382 Ok(self.handle.spawn(async move {
1383 let output = future.await;
1384 if output.is_ok() {
1385 guard.complete();
1386 } else {
1387 guard.fail();
1388 }
1389 output
1390 }))
1391 }
1392}
1393
1394fn latch_terminal_failure(latch: &Mutex<Option<TaskTerminalReport>>, report: &TaskTerminalReport) {
1395 if !matches!(
1396 report.reason,
1397 TaskTerminalReason::Failed | TaskTerminalReason::Panicked
1398 ) {
1399 return;
1400 }
1401 let mut failure = latch.lock().unwrap_or_else(|poisoned| {
1402 eprintln!("ERR_AGENTOS_TASK_FAILURE_LATCH_POISONED: recovering terminal failure");
1403 poisoned.into_inner()
1404 });
1405 if failure.is_none() {
1406 *failure = Some(report.clone());
1407 }
1408}
1409
1410struct WatchdogFuture<F> {
1411 inner: Pin<Box<F>>,
1412 class: TaskClass,
1413 threshold: Duration,
1414 metrics: RuntimeMetrics,
1415 reported: bool,
1416}
1417
1418impl<F> WatchdogFuture<F> {
1419 fn new(future: F, class: TaskClass, threshold: Duration, metrics: RuntimeMetrics) -> Self {
1420 Self {
1421 inner: Box::pin(future),
1422 class,
1423 threshold,
1424 metrics,
1425 reported: false,
1426 }
1427 }
1428}
1429
1430impl<F: Future> Future for WatchdogFuture<F> {
1431 type Output = F::Output;
1432
1433 fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
1434 let this = self.as_mut().get_mut();
1435 let started = Instant::now();
1436 let result = this.inner.as_mut().poll(context);
1437 let elapsed = started.elapsed();
1438 if elapsed >= this.threshold {
1439 this.metrics
1440 .record_watchdog(WatchdogMetric::LongTaskPoll, elapsed);
1441 if !this.reported {
1442 this.reported = true;
1443 let message = format!(
1444 "task_class={:?} poll_ms={} threshold_ms={}",
1445 this.class,
1446 elapsed.as_millis(),
1447 this.threshold.as_millis()
1448 );
1449 this.metrics.emit_stderr_fallback(TelemetryFallback {
1450 severity: TelemetrySeverity::Warning,
1451 code: TelemetryFallbackCode::RuntimeWorkerStall,
1452 subsystem: TelemetrySubsystem::Runtime,
1453 message: &message,
1454 });
1455 }
1456 }
1457 result
1458 }
1459}
1460
1461pub struct SidecarRuntime {
1462 config: RuntimeConfig,
1463 runtime: tokio::runtime::Runtime,
1464 context: RuntimeContext,
1465}
1466
1467static PROCESS_RUNTIME: OnceLock<Result<SidecarRuntime, RuntimeBuildError>> = OnceLock::new();
1468
1469impl SidecarRuntime {
1470 fn build(config: RuntimeConfig) -> Result<Self, RuntimeBuildError> {
1471 config.validate()?;
1472 let mut resource_limits = config.resources.limits();
1473 resource_limits.push((
1474 ResourceClass::ExecutorSlots,
1475 ResourceLimit::new(config.max_blocking_jobs, "runtime.blocking.maxJobs"),
1476 ));
1477 resource_limits.push((
1478 ResourceClass::ExecutorBytes,
1479 ResourceLimit::new(
1480 config.max_blocking_job_bytes,
1481 "runtime.blocking.maxQueuedBytes",
1482 ),
1483 ));
1484 let metrics = RuntimeMetrics::new();
1485 let fairness = FairWorkBroker::new(config.fairness.scheduler_config(), metrics.clone())
1486 .map_err(|error| {
1487 RuntimeBuildError(format!(
1488 "ERR_AGENTOS_RUNTIME_FAIRNESS_START: failed to build process fairness broker: {error}"
1489 ))
1490 })?;
1491 let resources = Arc::new(ResourceLedger::root_with_metrics(
1492 "sidecar-process",
1493 resource_limits,
1494 metrics.clone(),
1495 ));
1496 let admission_open = Arc::new(AtomicBool::new(true));
1497 let admission_gate = Arc::new(Mutex::new(()));
1498 let blocking = BlockingExecutor::new(
1499 &config,
1500 Arc::clone(&resources),
1501 metrics.clone(),
1502 Arc::clone(&admission_open),
1503 Arc::clone(&admission_gate),
1504 )?;
1505 let runtime = tokio::runtime::Builder::new_multi_thread()
1506 .worker_threads(config.worker_threads)
1507 .thread_name_fn(|| {
1508 static NEXT_WORKER: AtomicUsize = AtomicUsize::new(0);
1509 format!(
1510 "agentos-runtime-{}",
1511 NEXT_WORKER.fetch_add(1, Ordering::Relaxed)
1512 )
1513 })
1514 .on_thread_start(|| IS_AGENTOS_RUNTIME_WORKER.with(|marker| marker.set(true)))
1515 .on_thread_stop(|| IS_AGENTOS_RUNTIME_WORKER.with(|marker| marker.set(false)))
1516 .enable_all()
1517 .build()
1518 .map_err(|error| {
1519 RuntimeBuildError(format!(
1520 "ERR_AGENTOS_RUNTIME_START: failed to build process runtime: {error}"
1521 ))
1522 })?;
1523 let context = RuntimeContext {
1524 handle: runtime.handle().clone(),
1525 blocking,
1526 resources: Arc::clone(&resources),
1527 tasks: TaskSupervisor::new(
1528 resources,
1529 metrics.clone(),
1530 Arc::clone(&admission_open),
1531 admission_gate,
1532 config.max_terminal_task_reports,
1533 ),
1534 metrics,
1535 fairness,
1536 terminal_failure: Arc::new(Mutex::new(None)),
1537 task_poll_watchdog: Duration::from_millis(config.task_poll_watchdog_ms),
1538 max_active_vm_executors: config.max_active_vm_executors,
1539 vm_executor_teardown_timeout: Duration::from_millis(
1540 config.vm_executor_teardown_timeout_ms,
1541 ),
1542 blocking_job_timeout: Duration::from_millis(config.blocking_job_timeout_ms),
1543 admission_open,
1544 admission_closed: Arc::new(tokio::sync::Notify::new()),
1545 next_vm_generation: Arc::new(AtomicU64::new(0)),
1546 default_owner: TaskOwner::Process,
1547 };
1548 Ok(Self {
1549 config,
1550 runtime,
1551 context,
1552 })
1553 }
1554
1555 pub fn process(config: &RuntimeConfig) -> Result<&'static Self, RuntimeBuildError> {
1561 match PROCESS_RUNTIME.get_or_init(|| Self::build(config.clone())) {
1562 Ok(runtime) if &runtime.config == config => Ok(runtime),
1563 Ok(runtime) => Err(RuntimeBuildError(format!(
1564 "ERR_AGENTOS_RUNTIME_ALREADY_CONFIGURED: process runtime uses {:?}, requested {:?}",
1565 runtime.config, config
1566 ))),
1567 Err(error) => Err(error.clone()),
1568 }
1569 }
1570
1571 pub fn process_context() -> Result<RuntimeContext, RuntimeBuildError> {
1576 match PROCESS_RUNTIME.get() {
1577 Some(Ok(runtime)) => Ok(runtime.context()),
1578 Some(Err(error)) => Err(error.clone()),
1579 #[cfg(test)]
1580 None => Self::process(&RuntimeConfig::default()).map(Self::context),
1581 #[cfg(not(test))]
1582 None => Err(RuntimeBuildError(String::from(
1583 "ERR_AGENTOS_RUNTIME_NOT_INITIALIZED: the process entrypoint must construct SidecarRuntime before starting subsystems",
1584 ))),
1585 }
1586 }
1587
1588 pub fn context(&self) -> RuntimeContext {
1589 self.context.clone()
1590 }
1591
1592 pub fn block_on<F: Future>(&self, future: F) -> F::Output {
1593 self.runtime.block_on(future)
1594 }
1595}
1596
1597#[cfg(test)]
1598mod tests {
1599 use super::*;
1600
1601 #[test]
1602 fn process_runtime_bounds_every_resource_class_by_default() {
1603 let runtime = SidecarRuntime::build(RuntimeConfig::default()).expect("build runtime");
1604 for resource in ResourceClass::ALL {
1605 let usage = runtime.context().resources().usage(resource);
1606 assert_eq!(usage.used, 0, "{} starts charged", resource.name());
1607 assert!(
1608 usage.limit.is_some_and(|limit| limit > 0),
1609 "{} has no positive process limit",
1610 resource.name()
1611 );
1612 }
1613 }
1614
1615 #[test]
1616 fn vm_generation_allocator_is_shared_by_scoped_contexts() {
1617 let runtime = SidecarRuntime::build(RuntimeConfig::default()).expect("build runtime");
1618 let process = runtime.context();
1619 let resources = Arc::new(ResourceLedger::root("vm-generation-test", []));
1620 let scoped = process.scoped(Arc::clone(&resources));
1621
1622 let first = process
1623 .allocate_vm_generation()
1624 .expect("allocate process generation");
1625 let second = scoped
1626 .allocate_vm_generation()
1627 .expect("allocate scoped generation");
1628
1629 assert_eq!(second, first + 1);
1630 }
1631
1632 #[test]
1633 fn validates_nonzero_runtime_limits() {
1634 let error = RuntimeConfig {
1635 worker_threads: 0,
1636 ..RuntimeConfig::default()
1637 }
1638 .validate()
1639 .expect_err("zero worker count must be rejected");
1640 assert!(error.to_string().contains("runtime.workerThreads"));
1641
1642 let error = RuntimeConfig {
1643 max_terminal_task_reports: 0,
1644 ..RuntimeConfig::default()
1645 }
1646 .validate()
1647 .expect_err("zero terminal-report capacity must be rejected");
1648 assert!(error
1649 .to_string()
1650 .contains("runtime.tasks.maxTerminalReports"));
1651
1652 let error = RuntimeConfig {
1653 max_active_vm_executors: 0,
1654 ..RuntimeConfig::default()
1655 }
1656 .validate()
1657 .expect_err("zero VM executor capacity must be rejected");
1658 assert!(error.to_string().contains("runtime.executor.maxActiveVms"));
1659
1660 let error = RuntimeConfig {
1661 vm_executor_teardown_timeout_ms: 0,
1662 ..RuntimeConfig::default()
1663 }
1664 .validate()
1665 .expect_err("zero VM executor teardown timeout must be rejected");
1666 assert!(error
1667 .to_string()
1668 .contains("runtime.executor.teardownTimeoutMs"));
1669
1670 let error = RuntimeConfig {
1671 blocking_job_timeout_ms: 0,
1672 ..RuntimeConfig::default()
1673 }
1674 .validate()
1675 .expect_err("zero blocking-job timeout must be rejected");
1676 assert!(error.to_string().contains("runtime.blocking.jobTimeoutMs"));
1677
1678 let error = RuntimeConfig {
1679 protocol: RuntimeProtocolConfig {
1680 max_ingress_bytes: 0,
1681 ..RuntimeProtocolConfig::default()
1682 },
1683 ..RuntimeConfig::default()
1684 }
1685 .validate()
1686 .expect_err("zero protocol byte capacity must be rejected");
1687 assert!(error
1688 .to_string()
1689 .contains("runtime.protocol.maxIngressBytes"));
1690 }
1691
1692 #[test]
1693 fn blocking_executor_enforces_byte_reservations() {
1694 let runtime = SidecarRuntime::build(RuntimeConfig {
1695 worker_threads: 1,
1696 blocking_worker_threads: 1,
1697 max_queued_blocking_jobs: 1,
1698 max_blocking_job_bytes: 8,
1699 ..RuntimeConfig::default()
1700 })
1701 .expect("build runtime");
1702 let blocking = runtime.context().blocking().clone();
1703 let error = runtime
1704 .block_on(blocking.run(9, || 1usize))
1705 .expect_err("oversize blocking job must be rejected");
1706 assert!(matches!(error, BlockingJobError::ResourceLimit(_)));
1707 assert_eq!(blocking.reserved_bytes(), 0);
1708 }
1709
1710 #[test]
1711 fn blocking_executor_runs_on_fixed_workers_and_releases_bytes() {
1712 let runtime = SidecarRuntime::build(RuntimeConfig {
1713 worker_threads: 1,
1714 blocking_worker_threads: 2,
1715 max_queued_blocking_jobs: 2,
1716 max_blocking_job_bytes: 32,
1717 ..RuntimeConfig::default()
1718 })
1719 .expect("build runtime");
1720 let blocking = runtime.context().blocking().clone();
1721 let worker_name = runtime
1722 .block_on(blocking.run(4, || {
1723 thread::current().name().unwrap_or_default().to_owned()
1724 }))
1725 .expect("blocking job result");
1726 assert!(worker_name.starts_with("agentos-blocking-"));
1727 assert_eq!(blocking.worker_count(), 2);
1728 assert_eq!(blocking.reserved_bytes(), 0);
1729 let metrics = runtime.context().metrics().snapshot();
1730 assert_eq!(
1731 metrics.buffers[metrics::BufferMetricClass::Executor.index()].current,
1732 0
1733 );
1734 assert_eq!(
1735 metrics.buffers[metrics::BufferMetricClass::Executor.index()].high_water,
1736 4
1737 );
1738 assert!(
1739 metrics.executors[ExecutorMetricClass::Blocking.index()]
1740 .active
1741 .high_water
1742 >= 1
1743 );
1744 }
1745
1746 #[test]
1747 fn blocking_executor_supports_bounded_synchronous_callers() {
1748 let runtime = SidecarRuntime::build(RuntimeConfig {
1749 worker_threads: 1,
1750 blocking_worker_threads: 1,
1751 max_queued_blocking_jobs: 1,
1752 max_blocking_job_bytes: 32,
1753 ..RuntimeConfig::default()
1754 })
1755 .expect("build runtime");
1756 let blocking = runtime.context().blocking().clone();
1757 let value = blocking
1758 .run_sync(4, Duration::from_secs(1), || 42usize)
1759 .expect("synchronous blocking job result");
1760 assert_eq!(value, 42);
1761 assert_eq!(blocking.reserved_bytes(), 0);
1762 }
1763
1764 #[test]
1765 fn task_supervisor_reports_every_terminal_reason_and_reconciles() {
1766 let runtime = SidecarRuntime::build(RuntimeConfig {
1767 worker_threads: 1,
1768 blocking_worker_threads: 1,
1769 max_queued_blocking_jobs: 1,
1770 ..RuntimeConfig::default()
1771 })
1772 .expect("build runtime");
1773 let context = runtime.context();
1774
1775 runtime.block_on(async {
1776 context
1777 .spawn(TaskClass::Runtime, async {})
1778 .expect("completed task admission")
1779 .await
1780 .expect("completed task join");
1781
1782 let failed = context
1783 .spawn_result(TaskClass::Runtime, async { Err::<(), _>("failed") })
1784 .expect("failed task admission")
1785 .await
1786 .expect("failed task join");
1787 assert_eq!(failed, Err("failed"));
1788
1789 let panicked = context
1790 .spawn(TaskClass::Runtime, async { panic!("task panic fixture") })
1791 .expect("panicked task admission")
1792 .await;
1793 assert!(panicked.expect_err("task must panic").is_panic());
1794
1795 let cancelled = context
1796 .spawn(TaskClass::Runtime, std::future::pending::<()>())
1797 .expect("cancelled task admission");
1798 cancelled.abort();
1799 assert!(cancelled
1800 .await
1801 .expect_err("task must cancel")
1802 .is_cancelled());
1803 });
1804
1805 let snapshot = context.tasks().snapshot(TaskClass::Runtime);
1806 assert_eq!(snapshot.active, 0);
1807 assert_eq!(snapshot.completed, 1);
1808 assert_eq!(snapshot.failed, 1);
1809 assert_eq!(snapshot.panicked, 1);
1810 assert_eq!(snapshot.cancelled, 1);
1811 assert_eq!(context.resources().usage(ResourceClass::Tasks).used, 0);
1812 let terminal_failure = context
1813 .terminal_failure()
1814 .expect("failed task must latch owner failure");
1815 assert_eq!(terminal_failure.reason, TaskTerminalReason::Failed);
1816 assert_eq!(terminal_failure.owner, TaskOwner::Process);
1817 let metric = context.metrics().snapshot().task(TaskClass::Runtime);
1818 assert_eq!(metric.active, 0);
1819 assert_eq!(metric.terminal_count(TaskTerminalReason::Completed), 1);
1820 assert_eq!(metric.terminal_count(TaskTerminalReason::Failed), 1);
1821 assert_eq!(metric.terminal_count(TaskTerminalReason::Panicked), 1);
1822 assert_eq!(metric.terminal_count(TaskTerminalReason::Cancelled), 1);
1823 }
1824
1825 #[test]
1826 fn long_task_poll_records_watchdog_once_without_dynamic_labels() {
1827 let runtime = SidecarRuntime::build(RuntimeConfig {
1828 worker_threads: 1,
1829 blocking_worker_threads: 1,
1830 max_queued_blocking_jobs: 1,
1831 task_poll_watchdog_ms: 1,
1832 ..RuntimeConfig::default()
1833 })
1834 .expect("build runtime");
1835 let context = runtime.context();
1836 runtime.block_on(async {
1837 context
1838 .spawn(TaskClass::Runtime, async {
1839 std::thread::sleep(Duration::from_millis(5));
1840 })
1841 .expect("task admission")
1842 .await
1843 .expect("task join");
1844 });
1845 let snapshot = context.metrics().snapshot();
1846 let watchdog = snapshot.watchdogs[WatchdogMetric::LongTaskPoll.index()];
1847 assert_eq!(watchdog.events, 1);
1848 assert!(watchdog.max_stall_micros >= 1_000);
1849 assert_eq!(
1850 snapshot.stderr_fallbacks[TelemetrySeverity::Warning.index()],
1851 1
1852 );
1853 }
1854
1855 #[test]
1856 fn vm_scoped_context_shares_workers_and_charges_both_ledgers() {
1857 let runtime = SidecarRuntime::build(RuntimeConfig {
1858 worker_threads: 1,
1859 blocking_worker_threads: 1,
1860 max_queued_blocking_jobs: 1,
1861 max_blocking_job_bytes: 32,
1862 ..RuntimeConfig::default()
1863 })
1864 .expect("build runtime");
1865 let process = Arc::clone(runtime.context().resources());
1866 let vm_ledger = Arc::new(ResourceLedger::child(
1867 "vm=1 generation=1",
1868 [
1869 (
1870 ResourceClass::Tasks,
1871 ResourceLimit::new(1, "limits.reactor.maxTasks"),
1872 ),
1873 (
1874 ResourceClass::ExecutorSlots,
1875 ResourceLimit::new(1, "limits.blocking.maxJobs"),
1876 ),
1877 (
1878 ResourceClass::ExecutorBytes,
1879 ResourceLimit::new(8, "limits.blocking.maxQueuedBytes"),
1880 ),
1881 ],
1882 Arc::clone(&process),
1883 ));
1884 let scoped = runtime.context().scoped(Arc::clone(&vm_ledger));
1885 assert_eq!(scoped.blocking().worker_count(), 1);
1886
1887 let value = runtime
1888 .block_on(scoped.blocking().run(8, || 42usize))
1889 .expect("VM-scoped blocking job");
1890 assert_eq!(value, 42);
1891 assert!(vm_ledger.is_zero());
1892 assert_eq!(
1893 process.usage(ResourceClass::ExecutorBytes).used,
1894 0,
1895 "child release must reconcile the process parent"
1896 );
1897
1898 runtime.block_on(async {
1899 scoped
1900 .spawn(TaskClass::Vm, async {})
1901 .expect("VM task admission")
1902 .await
1903 .expect("VM task join");
1904 });
1905 assert!(vm_ledger.is_zero());
1906 }
1907
1908 fn assert_vm_generation_churn_reconciles(generation_count: u64, logical_vm_count: u64) {
1909 use crate::capability::{CapabilityBackend, CapabilityKind, CapabilityRegistry};
1910
1911 assert!(generation_count > 0);
1912 assert!(logical_vm_count > 0);
1913
1914 let runtime = SidecarRuntime::build(RuntimeConfig {
1915 worker_threads: 2,
1916 blocking_worker_threads: 2,
1917 max_queued_blocking_jobs: 4,
1918 max_blocking_jobs: 8,
1919 max_blocking_job_bytes: 1024,
1920 ..RuntimeConfig::default()
1921 })
1922 .expect("build runtime");
1923 let process = Arc::clone(runtime.context().resources());
1924
1925 runtime.block_on(async {
1926 for generation in 1..=generation_count {
1927 let vm_index = (generation - 1) % logical_vm_count;
1928 let resources = Arc::new(ResourceLedger::child(
1929 format!("vm=churn-{vm_index} generation={generation}"),
1930 [
1931 (
1932 ResourceClass::Capabilities,
1933 ResourceLimit::new(2, "limits.reactor.maxCapabilities"),
1934 ),
1935 (
1936 ResourceClass::ReadyHandles,
1937 ResourceLimit::new(2, "limits.reactor.maxReadyHandles"),
1938 ),
1939 (
1940 ResourceClass::Sockets,
1941 ResourceLimit::new(2, "limits.resources.maxSockets"),
1942 ),
1943 (
1944 ResourceClass::Connections,
1945 ResourceLimit::new(2, "limits.resources.maxConnections"),
1946 ),
1947 (
1948 ResourceClass::Tasks,
1949 ResourceLimit::new(2, "limits.reactor.maxTasks"),
1950 ),
1951 (
1952 ResourceClass::ExecutorSlots,
1953 ResourceLimit::new(4, "limits.reactor.maxBlockingJobs"),
1954 ),
1955 (
1956 ResourceClass::ExecutorBytes,
1957 ResourceLimit::new(64, "limits.reactor.maxBlockingBytes"),
1958 ),
1959 ],
1960 Arc::clone(&process),
1961 ));
1962 let context = runtime
1963 .context()
1964 .scoped_for_vm(Arc::clone(&resources), generation);
1965 let capabilities = CapabilityRegistry::new(generation, Arc::clone(&resources));
1966 let lease = capabilities
1967 .reserve(CapabilityKind::TcpSocket)
1968 .expect("reserve churn socket before allocation")
1969 .commit(CapabilityBackend::Kernel {
1970 socket_id: generation,
1971 })
1972 .expect("commit churn socket");
1973
1974 let task = context
1975 .spawn(TaskClass::Socket, async { tokio::task::yield_now().await })
1976 .expect("admit churn task");
1977 let blocking = context
1978 .blocking()
1979 .run(16, move || generation)
1980 .await
1981 .expect("fixed blocking worker result");
1982 assert_eq!(blocking, generation);
1983 task.await.expect("churn task join");
1984
1985 let capability_id = lease.id();
1986 let turn = context
1987 .fairness()
1988 .acquire(generation, capability_id, FairBudget::new(1, 64))
1989 .await
1990 .expect("churn fairness turn");
1991 turn.complete(FairBudget::new(1, 16), false)
1992 .expect("complete churn fairness turn");
1993 drop(lease);
1994 capabilities
1995 .close_admission()
1996 .expect("close churn capability admission");
1997 context.close_admission();
1998 context.tasks().wait_empty().await;
1999 capabilities.wait_empty().await;
2000 context
2001 .fairness()
2002 .retire_capability(generation, capability_id)
2003 .expect("retire churn capability fairness state");
2004 context
2005 .fairness()
2006 .retire_vm(generation)
2007 .expect("retire churn VM fairness state");
2008 assert!(
2009 resources.is_zero(),
2010 "generation {generation} leaked accounting"
2011 );
2012 assert!(resources.integrity_ok());
2013 }
2014 });
2015
2016 assert!(process.is_zero(), "VM churn drifted process accounting");
2017 assert!(process.integrity_ok());
2018 assert_eq!(runtime.context().tasks().active_total(), 0);
2019 }
2020
2021 #[test]
2022 fn vm_generation_churn_reconciles_tasks_capabilities_fairness_and_bytes() {
2023 assert_vm_generation_churn_reconciles(256, 8);
2026 }
2027
2028 #[test]
2029 #[ignore = "expensive: multi-VM runtime accounting soak; run explicitly with --ignored"]
2030 fn multi_vm_generation_soak_has_no_accounting_or_scheduler_drift() {
2031 assert_vm_generation_churn_reconciles(50_000, 64);
2034 }
2035
2036 #[test]
2037 fn closing_vm_context_rejects_stale_task_and_blocking_clones() {
2038 let runtime = SidecarRuntime::build(RuntimeConfig {
2039 worker_threads: 1,
2040 blocking_worker_threads: 1,
2041 max_queued_blocking_jobs: 1,
2042 ..RuntimeConfig::default()
2043 })
2044 .expect("build runtime");
2045 let vm_ledger = Arc::new(ResourceLedger::child(
2046 "vm-generation-77",
2047 [
2048 (
2049 ResourceClass::Tasks,
2050 ResourceLimit::new(4, "limits.reactor.maxTasks"),
2051 ),
2052 (
2053 ResourceClass::ExecutorSlots,
2054 ResourceLimit::new(4, "limits.reactor.maxBlockingJobs"),
2055 ),
2056 (
2057 ResourceClass::ExecutorBytes,
2058 ResourceLimit::new(64, "limits.reactor.maxBlockingBytes"),
2059 ),
2060 ],
2061 Arc::clone(runtime.context().resources()),
2062 ));
2063 let scoped = runtime.context().scoped_for_vm(Arc::clone(&vm_ledger), 77);
2064 let stale = scoped.clone();
2065 scoped.close_admission();
2066
2067 let task_error = stale
2068 .spawn(TaskClass::Vm, async {})
2069 .expect_err("closed VM must reject stale task clone");
2070 assert!(matches!(task_error, TaskSpawnError::AdmissionClosed { .. }));
2071 let blocking_error = stale
2072 .blocking()
2073 .submit(1, || {})
2074 .expect_err("closed VM must reject stale blocking clone");
2075 assert_eq!(blocking_error, BlockingJobError::ShuttingDown);
2076 assert!(vm_ledger.is_zero());
2077 }
2078
2079 #[test]
2080 fn close_admission_linearizes_task_and_blocking_rejection() {
2081 let runtime = SidecarRuntime::build(RuntimeConfig {
2082 worker_threads: 1,
2083 blocking_worker_threads: 1,
2084 max_queued_blocking_jobs: 1,
2085 ..RuntimeConfig::default()
2086 })
2087 .expect("build runtime");
2088 let vm_ledger = Arc::new(ResourceLedger::child(
2089 "vm-generation-88",
2090 [
2091 (
2092 ResourceClass::Tasks,
2093 ResourceLimit::new(2, "limits.reactor.maxTasks"),
2094 ),
2095 (
2096 ResourceClass::ExecutorSlots,
2097 ResourceLimit::new(2, "limits.reactor.maxBlockingJobs"),
2098 ),
2099 (
2100 ResourceClass::ExecutorBytes,
2101 ResourceLimit::new(8, "limits.reactor.maxBlockingBytes"),
2102 ),
2103 ],
2104 Arc::clone(runtime.context().resources()),
2105 ));
2106 let scoped = runtime.context().scoped_for_vm(Arc::clone(&vm_ledger), 88);
2107
2108 let gate = Arc::clone(&scoped.blocking.admission_gate);
2111 let held = gate.lock().expect("admission gate");
2112 let closing = scoped.clone();
2113 let (started_tx, started_rx) = mpsc::sync_channel(1);
2114 let (closed_tx, closed_rx) = mpsc::sync_channel(1);
2115 let close_thread = thread::spawn(move || {
2116 started_tx.send(()).expect("publish close start");
2117 closing.close_admission();
2118 closed_tx.send(()).expect("publish close completion");
2119 });
2120 started_rx.recv().expect("close started");
2121 assert_eq!(
2122 closed_rx.recv_timeout(Duration::from_millis(20)),
2123 Err(mpsc::RecvTimeoutError::Timeout),
2124 "close must wait for the shared admission linearization gate"
2125 );
2126 drop(held);
2127 closed_rx
2128 .recv_timeout(Duration::from_secs(1))
2129 .expect("close completed after admission released");
2130 close_thread.join().expect("close thread");
2131
2132 let task_error = scoped
2133 .spawn(TaskClass::Vm, async {})
2134 .expect_err("task admission after linearized close must fail");
2135 assert!(matches!(task_error, TaskSpawnError::AdmissionClosed { .. }));
2136 let blocking_error = scoped
2137 .blocking()
2138 .submit(1, || {})
2139 .expect_err("blocking admission after linearized close must fail");
2140 assert_eq!(blocking_error, BlockingJobError::ShuttingDown);
2141 assert!(vm_ledger.is_zero());
2142 }
2143
2144 #[test]
2145 fn closing_vm_context_wakes_admitted_readiness_waiters() {
2146 let runtime = SidecarRuntime::build(RuntimeConfig {
2147 worker_threads: 1,
2148 ..RuntimeConfig::default()
2149 })
2150 .expect("build runtime");
2151 let resources = Arc::new(ResourceLedger::child(
2152 "vm-generation-88",
2153 [(
2154 ResourceClass::Tasks,
2155 ResourceLimit::new(2, "limits.reactor.maxTasks"),
2156 )],
2157 Arc::clone(runtime.context().resources()),
2158 ));
2159 let scoped = runtime.context().scoped_for_vm(resources, 88);
2160 let waiter_context = scoped.clone();
2161 let (started_tx, started_rx) = std::sync::mpsc::sync_channel(1);
2162 let waiter = scoped
2163 .spawn(TaskClass::Socket, async move {
2164 started_tx.send(()).expect("signal waiter admission");
2165 waiter_context.admission_closed().await;
2166 })
2167 .expect("spawn readiness waiter");
2168
2169 started_rx
2170 .recv_timeout(Duration::from_secs(1))
2171 .expect("waiter started");
2172 scoped.close_admission();
2173 runtime.context().handle().block_on(async {
2174 tokio::time::timeout(Duration::from_secs(1), waiter)
2175 .await
2176 .expect("close wakes waiter before teardown deadline")
2177 .expect("waiter joins")
2178 });
2179 assert_eq!(scoped.tasks().active_scoped(), 0);
2180 }
2181}