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