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