1use std::sync::Arc;
12
13use serde::{Deserialize, Serialize};
14
15use crate::fs::VirtualFileSystem;
16
17#[derive(Default)]
22pub struct AgentOsConfig {
23 pub software: Vec<SoftwareInput>,
25 pub packages: Vec<PackageRef>,
29 pub packages_mount_at: Option<String>,
32 pub loopback_exempt_ports: Vec<u16>,
34 pub allowed_node_builtins: Option<Vec<String>>,
36 pub root_filesystem: RootFilesystemConfig,
38 pub mounts: Vec<MountConfig>,
40 pub additional_instructions: Option<String>,
42 pub schedule_driver: Option<Arc<dyn ScheduleDriver>>,
44 pub tool_kits: Vec<ToolKit>,
46 pub sidecar_js_bridge_callback: Option<SidecarJsBridgeCallback>,
48 pub permissions: Option<Permissions>,
50 pub limits: Option<AgentOsLimits>,
52 pub sidecar: Option<AgentOsSidecarConfig>,
54 pub sidecar_binary_path: Option<String>,
59}
60
61#[derive(Default)]
63pub struct AgentOsConfigBuilder {
64 config: AgentOsConfig,
65}
66
67impl AgentOsConfigBuilder {
68 pub fn new() -> Self {
69 Self::default()
70 }
71
72 pub fn packages(mut self, packages: Vec<PackageRef>) -> Self {
73 self.config.packages = packages;
74 self
75 }
76
77 pub fn packages_mount_at(mut self, mount_at: impl Into<String>) -> Self {
78 self.config.packages_mount_at = Some(mount_at.into());
79 self
80 }
81
82 pub fn loopback_exempt_ports(mut self, ports: Vec<u16>) -> Self {
83 self.config.loopback_exempt_ports = ports;
84 self
85 }
86
87 pub fn allowed_node_builtins(mut self, builtins: Vec<String>) -> Self {
88 self.config.allowed_node_builtins = Some(builtins);
89 self
90 }
91
92 pub fn root_filesystem(mut self, root: RootFilesystemConfig) -> Self {
93 self.config.root_filesystem = root;
94 self
95 }
96
97 pub fn mounts(mut self, mounts: Vec<MountConfig>) -> Self {
98 self.config.mounts = mounts;
99 self
100 }
101
102 pub fn additional_instructions(mut self, instructions: impl Into<String>) -> Self {
103 self.config.additional_instructions = Some(instructions.into());
104 self
105 }
106
107 pub fn schedule_driver(mut self, driver: Arc<dyn ScheduleDriver>) -> Self {
108 self.config.schedule_driver = Some(driver);
109 self
110 }
111
112 pub fn tool_kits(mut self, tool_kits: Vec<ToolKit>) -> Self {
113 self.config.tool_kits = tool_kits;
114 self
115 }
116
117 pub fn sidecar_js_bridge_callback(mut self, callback: SidecarJsBridgeCallback) -> Self {
118 self.config.sidecar_js_bridge_callback = Some(callback);
119 self
120 }
121
122 pub fn permissions(mut self, permissions: Permissions) -> Self {
123 self.config.permissions = Some(permissions);
124 self
125 }
126
127 pub fn limits(mut self, limits: AgentOsLimits) -> Self {
128 self.config.limits = Some(limits);
129 self
130 }
131
132 pub fn sidecar(mut self, sidecar: AgentOsSidecarConfig) -> Self {
133 self.config.sidecar = Some(sidecar);
134 self
135 }
136
137 pub fn sidecar_binary_path(mut self, path: impl Into<String>) -> Self {
138 self.config.sidecar_binary_path = Some(path.into());
139 self
140 }
141
142 pub fn build(self) -> AgentOsConfig {
143 self.config
144 }
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
150#[serde(rename_all = "kebab-case")]
151pub enum SoftwareKind {
152 #[default]
155 WasmCommands,
156 Agent,
158 Tool,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164pub struct SoftwareInput {
165 pub package: String,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub version: Option<String>,
168 #[serde(default)]
170 pub kind: SoftwareKind,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
177pub struct PackageRef {
178 #[serde(rename = "packagePath")]
181 pub path: String,
182}
183
184pub type ToolCallback = Arc<
188 dyn Fn(
189 serde_json::Value,
190 ) -> futures::future::BoxFuture<'static, Result<serde_json::Value, String>>
191 + Send
192 + Sync,
193>;
194
195#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct SidecarJsBridgeCall {
198 pub call_id: String,
199 pub mount_id: String,
200 pub operation: String,
201 pub args: serde_json::Value,
202}
203
204pub type SidecarJsBridgeCallback = Arc<
210 dyn Fn(
211 SidecarJsBridgeCall,
212 )
213 -> futures::future::BoxFuture<'static, Result<Option<serde_json::Value>, String>>
214 + Send
215 + Sync,
216>;
217
218#[derive(Clone)]
220pub struct HostTool {
221 pub name: String,
222 pub description: String,
223 pub input_schema: serde_json::Value,
225 pub timeout_ms: Option<u64>,
226 pub execute: ToolCallback,
228}
229
230#[derive(Clone)]
234pub struct ToolKit {
235 pub name: String,
236 pub description: String,
237 pub tools: Vec<HostTool>,
238}
239
240#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
247pub struct AgentOsLimits {
248 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub resources: Option<ResourceLimits>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub http: Option<HttpLimits>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub tools: Option<ToolLimits>,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub plugins: Option<PluginLimits>,
256 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub acp: Option<AcpLimits>,
258 #[serde(default, rename = "jsRuntime", skip_serializing_if = "Option::is_none")]
259 pub js_runtime: Option<JsRuntimeLimits>,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub python: Option<PythonLimits>,
262 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub wasm: Option<WasmLimits>,
264}
265
266#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
267pub struct ResourceLimits {
268 #[serde(default, rename = "cpuCount", skip_serializing_if = "Option::is_none")]
269 pub cpu_count: Option<u64>,
270 #[serde(
271 default,
272 rename = "maxProcesses",
273 skip_serializing_if = "Option::is_none"
274 )]
275 pub max_processes: Option<u64>,
276 #[serde(
277 default,
278 rename = "maxOpenFds",
279 skip_serializing_if = "Option::is_none"
280 )]
281 pub max_open_fds: Option<u64>,
282 #[serde(default, rename = "maxPipes", skip_serializing_if = "Option::is_none")]
283 pub max_pipes: Option<u64>,
284 #[serde(default, rename = "maxPtys", skip_serializing_if = "Option::is_none")]
285 pub max_ptys: Option<u64>,
286 #[serde(
287 default,
288 rename = "maxSockets",
289 skip_serializing_if = "Option::is_none"
290 )]
291 pub max_sockets: Option<u64>,
292 #[serde(
293 default,
294 rename = "maxConnections",
295 skip_serializing_if = "Option::is_none"
296 )]
297 pub max_connections: Option<u64>,
298 #[serde(
299 default,
300 rename = "maxSocketBufferedBytes",
301 skip_serializing_if = "Option::is_none"
302 )]
303 pub max_socket_buffered_bytes: Option<u64>,
304 #[serde(
305 default,
306 rename = "maxSocketDatagramQueueLen",
307 skip_serializing_if = "Option::is_none"
308 )]
309 pub max_socket_datagram_queue_len: Option<u64>,
310 #[serde(
311 default,
312 rename = "maxFilesystemBytes",
313 skip_serializing_if = "Option::is_none"
314 )]
315 pub max_filesystem_bytes: Option<u64>,
316 #[serde(
317 default,
318 rename = "maxInodeCount",
319 skip_serializing_if = "Option::is_none"
320 )]
321 pub max_inode_count: Option<u64>,
322 #[serde(
323 default,
324 rename = "maxBlockingReadMs",
325 skip_serializing_if = "Option::is_none"
326 )]
327 pub max_blocking_read_ms: Option<u64>,
328 #[serde(
329 default,
330 rename = "maxPreadBytes",
331 skip_serializing_if = "Option::is_none"
332 )]
333 pub max_pread_bytes: Option<u64>,
334 #[serde(
335 default,
336 rename = "maxFdWriteBytes",
337 skip_serializing_if = "Option::is_none"
338 )]
339 pub max_fd_write_bytes: Option<u64>,
340 #[serde(
341 default,
342 rename = "maxProcessArgvBytes",
343 skip_serializing_if = "Option::is_none"
344 )]
345 pub max_process_argv_bytes: Option<u64>,
346 #[serde(
347 default,
348 rename = "maxProcessEnvBytes",
349 skip_serializing_if = "Option::is_none"
350 )]
351 pub max_process_env_bytes: Option<u64>,
352 #[serde(
353 default,
354 rename = "maxReaddirEntries",
355 skip_serializing_if = "Option::is_none"
356 )]
357 pub max_readdir_entries: Option<u64>,
358 #[serde(
359 default,
360 rename = "maxWasmFuel",
361 skip_serializing_if = "Option::is_none"
362 )]
363 pub max_wasm_fuel: Option<u64>,
364 #[serde(
365 default,
366 rename = "maxWasmMemoryBytes",
367 skip_serializing_if = "Option::is_none"
368 )]
369 pub max_wasm_memory_bytes: Option<u64>,
370 #[serde(
371 default,
372 rename = "maxWasmStackBytes",
373 skip_serializing_if = "Option::is_none"
374 )]
375 pub max_wasm_stack_bytes: Option<u64>,
376}
377
378#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
379pub struct HttpLimits {
380 #[serde(
381 default,
382 rename = "maxFetchResponseBytes",
383 skip_serializing_if = "Option::is_none"
384 )]
385 pub max_fetch_response_bytes: Option<u64>,
386}
387
388#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
389pub struct ToolLimits {
390 #[serde(
391 default,
392 rename = "defaultToolTimeoutMs",
393 skip_serializing_if = "Option::is_none"
394 )]
395 pub default_tool_timeout_ms: Option<u64>,
396 #[serde(
397 default,
398 rename = "maxToolTimeoutMs",
399 skip_serializing_if = "Option::is_none"
400 )]
401 pub max_tool_timeout_ms: Option<u64>,
402 #[serde(
403 default,
404 rename = "maxRegisteredToolkits",
405 skip_serializing_if = "Option::is_none"
406 )]
407 pub max_registered_toolkits: Option<u64>,
408 #[serde(
409 default,
410 rename = "maxRegisteredToolsPerVm",
411 skip_serializing_if = "Option::is_none"
412 )]
413 pub max_registered_tools_per_vm: Option<u64>,
414 #[serde(
415 default,
416 rename = "maxToolsPerToolkit",
417 skip_serializing_if = "Option::is_none"
418 )]
419 pub max_tools_per_toolkit: Option<u64>,
420 #[serde(
421 default,
422 rename = "maxToolSchemaBytes",
423 skip_serializing_if = "Option::is_none"
424 )]
425 pub max_tool_schema_bytes: Option<u64>,
426 #[serde(
427 default,
428 rename = "maxToolExamplesPerTool",
429 skip_serializing_if = "Option::is_none"
430 )]
431 pub max_tool_examples_per_tool: Option<u64>,
432 #[serde(
433 default,
434 rename = "maxToolExampleInputBytes",
435 skip_serializing_if = "Option::is_none"
436 )]
437 pub max_tool_example_input_bytes: Option<u64>,
438}
439
440#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
441pub struct PluginLimits {
442 #[serde(
443 default,
444 rename = "maxPersistedManifestBytes",
445 skip_serializing_if = "Option::is_none"
446 )]
447 pub max_persisted_manifest_bytes: Option<u64>,
448 #[serde(
449 default,
450 rename = "maxPersistedManifestFileBytes",
451 skip_serializing_if = "Option::is_none"
452 )]
453 pub max_persisted_manifest_file_bytes: Option<u64>,
454}
455
456#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
457pub struct AcpLimits {
458 #[serde(
459 default,
460 rename = "maxReadLineBytes",
461 skip_serializing_if = "Option::is_none"
462 )]
463 pub max_read_line_bytes: Option<u64>,
464 #[serde(
465 default,
466 rename = "stdoutBufferByteLimit",
467 skip_serializing_if = "Option::is_none"
468 )]
469 pub stdout_buffer_byte_limit: Option<u64>,
470}
471
472#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
473pub struct JsRuntimeLimits {
474 #[serde(
475 default,
476 rename = "v8HeapLimitMb",
477 skip_serializing_if = "Option::is_none"
478 )]
479 pub v8_heap_limit_mb: Option<u64>,
480 #[serde(
481 default,
482 rename = "syncRpcWaitTimeoutMs",
483 skip_serializing_if = "Option::is_none"
484 )]
485 pub sync_rpc_wait_timeout_ms: Option<u64>,
486 #[serde(
487 default,
488 rename = "cpuTimeLimitMs",
489 skip_serializing_if = "Option::is_none"
490 )]
491 pub cpu_time_limit_ms: Option<u64>,
492 #[serde(
493 default,
494 rename = "wallClockLimitMs",
495 skip_serializing_if = "Option::is_none"
496 )]
497 pub wall_clock_limit_ms: Option<u64>,
498 #[serde(
499 default,
500 rename = "importCacheMaterializeTimeoutMs",
501 skip_serializing_if = "Option::is_none"
502 )]
503 pub import_cache_materialize_timeout_ms: Option<u64>,
504 #[serde(
505 default,
506 rename = "capturedOutputLimitBytes",
507 skip_serializing_if = "Option::is_none"
508 )]
509 pub captured_output_limit_bytes: Option<u64>,
510 #[serde(
511 default,
512 rename = "stdinBufferLimitBytes",
513 skip_serializing_if = "Option::is_none"
514 )]
515 pub stdin_buffer_limit_bytes: Option<u64>,
516 #[serde(
517 default,
518 rename = "eventPayloadLimitBytes",
519 skip_serializing_if = "Option::is_none"
520 )]
521 pub event_payload_limit_bytes: Option<u64>,
522 #[serde(
523 default,
524 rename = "v8IpcMaxFrameBytes",
525 skip_serializing_if = "Option::is_none"
526 )]
527 pub v8_ipc_max_frame_bytes: Option<u64>,
528}
529
530#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
531pub struct PythonLimits {
532 #[serde(
533 default,
534 rename = "outputBufferMaxBytes",
535 skip_serializing_if = "Option::is_none"
536 )]
537 pub output_buffer_max_bytes: Option<u64>,
538 #[serde(
539 default,
540 rename = "executionTimeoutMs",
541 skip_serializing_if = "Option::is_none"
542 )]
543 pub execution_timeout_ms: Option<u64>,
544 #[serde(
545 default,
546 rename = "maxOldSpaceMb",
547 skip_serializing_if = "Option::is_none"
548 )]
549 pub max_old_space_mb: Option<u64>,
550 #[serde(
551 default,
552 rename = "vfsRpcTimeoutMs",
553 skip_serializing_if = "Option::is_none"
554 )]
555 pub vfs_rpc_timeout_ms: Option<u64>,
556}
557
558#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
559pub struct WasmLimits {
560 #[serde(
561 default,
562 rename = "maxModuleFileBytes",
563 skip_serializing_if = "Option::is_none"
564 )]
565 pub max_module_file_bytes: Option<u64>,
566 #[serde(
567 default,
568 rename = "capturedOutputLimitBytes",
569 skip_serializing_if = "Option::is_none"
570 )]
571 pub captured_output_limit_bytes: Option<u64>,
572 #[serde(
573 default,
574 rename = "syncReadLimitBytes",
575 skip_serializing_if = "Option::is_none"
576 )]
577 pub sync_read_limit_bytes: Option<u64>,
578 #[serde(
579 default,
580 rename = "prewarmTimeoutMs",
581 skip_serializing_if = "Option::is_none"
582 )]
583 pub prewarm_timeout_ms: Option<u64>,
584 #[serde(
585 default,
586 rename = "runnerHeapLimitMb",
587 skip_serializing_if = "Option::is_none"
588 )]
589 pub runner_heap_limit_mb: Option<u64>,
590}
591
592#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
598pub struct Permissions {
599 #[serde(default, skip_serializing_if = "Option::is_none")]
600 pub fs: Option<FsPermissions>,
601 #[serde(default, skip_serializing_if = "Option::is_none")]
602 pub network: Option<PatternPermissions>,
603 #[serde(
604 default,
605 rename = "childProcess",
606 skip_serializing_if = "Option::is_none"
607 )]
608 pub child_process: Option<PatternPermissions>,
609 #[serde(default, skip_serializing_if = "Option::is_none")]
610 pub process: Option<PatternPermissions>,
611 #[serde(default, skip_serializing_if = "Option::is_none")]
612 pub env: Option<PatternPermissions>,
613 #[serde(default, skip_serializing_if = "Option::is_none")]
614 pub binding: Option<PatternPermissions>,
615}
616
617#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
619#[serde(rename_all = "lowercase")]
620pub enum PermissionMode {
621 Allow,
622 Deny,
623}
624
625#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
627#[serde(untagged)]
628pub enum FsPermissions {
629 Mode(PermissionMode),
630 Rules(RulePermissions<FsPermissionRule>),
631}
632
633#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
635#[serde(untagged)]
636pub enum PatternPermissions {
637 Mode(PermissionMode),
638 Rules(RulePermissions<PatternPermissionRule>),
639}
640
641#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
643pub struct RulePermissions<T> {
644 #[serde(default, skip_serializing_if = "Option::is_none")]
645 pub default: Option<PermissionMode>,
646 pub rules: Vec<T>,
647}
648
649#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
651pub struct FsPermissionRule {
652 pub mode: PermissionMode,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
654 pub operations: Option<Vec<String>>,
655 #[serde(default, skip_serializing_if = "Option::is_none")]
656 pub paths: Option<Vec<String>>,
657}
658
659#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
661pub struct PatternPermissionRule {
662 pub mode: PermissionMode,
663 #[serde(default, skip_serializing_if = "Option::is_none")]
664 pub operations: Option<Vec<String>>,
665 #[serde(default, skip_serializing_if = "Option::is_none")]
666 pub patterns: Option<Vec<String>>,
667}
668
669#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
675pub struct RootFilesystemConfig {
676 #[serde(default, rename = "type")]
677 pub kind: RootFilesystemKind,
678 #[serde(default, skip_serializing_if = "Option::is_none")]
679 pub mode: Option<RootFilesystemMode>,
680 #[serde(
681 default,
682 rename = "nativePlugin",
683 skip_serializing_if = "Option::is_none"
684 )]
685 pub native_plugin: Option<MountPlugin>,
686 #[serde(default, rename = "disableDefaultBaseLayer")]
687 pub disable_default_base_layer: bool,
688 #[serde(default)]
689 pub lowers: Vec<RootLowerInput>,
690}
691
692impl Default for RootFilesystemConfig {
693 fn default() -> Self {
694 Self {
695 kind: RootFilesystemKind::Overlay,
696 mode: None,
697 native_plugin: None,
698 disable_default_base_layer: false,
699 lowers: Vec::new(),
700 }
701 }
702}
703
704#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
706#[serde(rename_all = "lowercase")]
707pub enum RootFilesystemKind {
708 #[default]
709 Overlay,
710 Native,
711}
712
713#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
715#[serde(rename_all = "kebab-case")]
716pub enum RootFilesystemMode {
717 Ephemeral,
718 ReadOnly,
719}
720
721#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
723#[serde(tag = "kind", rename_all = "kebab-case")]
724pub enum RootLowerInput {
725 BundledBaseFilesystem,
727 #[serde(untagged)]
729 SnapshotExport(crate::fs::RootSnapshotExport),
730}
731
732pub enum MountConfig {
738 Plain {
740 path: String,
741 driver: Arc<dyn VirtualFileSystem>,
742 read_only: bool,
743 },
744 Native {
746 path: String,
747 plugin: MountPlugin,
748 read_only: bool,
749 },
750 Overlay {
752 path: String,
753 filesystem: OverlayMountConfig,
754 },
755}
756
757#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
759pub struct MountPlugin {
760 pub id: String,
761 #[serde(default, skip_serializing_if = "Option::is_none")]
762 pub config: Option<serde_json::Value>,
763}
764
765pub fn node_modules_mount(host_node_modules_dir: impl Into<String>) -> MountConfig {
773 MountConfig::Native {
774 path: "/root/node_modules".to_string(),
775 plugin: MountPlugin {
776 id: "host_dir".to_string(),
777 config: Some(serde_json::json!({
778 "hostPath": host_node_modules_dir.into(),
779 "readOnly": true,
780 })),
781 },
782 read_only: true,
783 }
784}
785
786#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
788pub struct OverlayMountConfig {
789 #[serde(rename = "type")]
790 pub kind: String,
791 pub store: serde_json::Value,
792 #[serde(default, skip_serializing_if = "Option::is_none")]
793 pub mode: Option<RootFilesystemMode>,
794 pub lowers: Vec<RootLowerInput>,
795}
796
797pub enum AgentOsSidecarConfig {
803 Shared { pool: Option<String> },
805 Explicit {
807 handle: Arc<crate::sidecar::AgentOsSidecar>,
808 },
809}
810
811pub type ScheduleCallback = Arc<dyn Fn() -> futures::future::BoxFuture<'static, ()> + Send + Sync>;
821
822#[derive(Clone)]
825pub struct ScheduleEntry {
826 pub id: String,
828 pub schedule: String,
830 pub callback: ScheduleCallback,
832}
833
834pub trait ScheduleDriver: Send + Sync {
840 fn schedule(&self, entry: ScheduleEntry) -> ScheduleHandle;
843
844 fn cancel(&self, handle: &ScheduleHandle);
846
847 fn dispose(&self);
849}
850
851#[derive(Clone)]
854pub struct ScheduleHandle {
855 pub id: String,
856}
857
858#[derive(Default)]
865pub struct TimerScheduleDriver {
866 timers: Arc<scc::HashMap<String, tokio_util::sync::CancellationToken>>,
867}
868
869impl TimerScheduleDriver {
870 pub fn new() -> Self {
871 Self {
872 timers: Arc::new(scc::HashMap::new()),
873 }
874 }
875
876 fn schedule_next(
880 timers: Arc<scc::HashMap<String, tokio_util::sync::CancellationToken>>,
881 entry: ScheduleEntry,
882 cancel: tokio_util::sync::CancellationToken,
883 ) {
884 let now = chrono::Utc::now();
885 let parsed = match crate::cron::parse_schedule(&entry.schedule) {
886 Ok(parsed) => parsed,
887 Err(_) => {
888 let _ = timers.remove(&entry.id);
889 return;
890 }
891 };
892 let is_cron = parsed.is_cron();
893 let next = match crate::cron::resolve_next_run(&parsed, now) {
894 Some(next) => next,
895 None => {
896 let _ = timers.remove(&entry.id);
898 return;
899 }
900 };
901
902 let delay = (next - now).to_std().unwrap_or(std::time::Duration::ZERO);
903
904 tokio::spawn(async move {
905 tokio::select! {
906 _ = cancel.cancelled() => {
907 return;
908 }
909 _ = tokio::time::sleep(delay) => {}
910 }
911 if cancel.is_cancelled() {
912 return;
913 }
914 (entry.callback)().await;
916
917 if is_cron && timers.contains(&entry.id) {
918 Self::schedule_next(Arc::clone(&timers), entry, cancel);
919 } else {
920 let _ = timers.remove(&entry.id);
921 }
922 });
923 }
924}
925
926impl ScheduleDriver for TimerScheduleDriver {
927 fn schedule(&self, entry: ScheduleEntry) -> ScheduleHandle {
928 let id = entry.id.clone();
929 let cancel = tokio_util::sync::CancellationToken::new();
930 if let Some((_, old)) = self.timers.remove(&id) {
932 old.cancel();
933 }
934 let _ = self.timers.insert(id.clone(), cancel.clone());
935
936 Self::schedule_next(Arc::clone(&self.timers), entry, cancel);
937
938 ScheduleHandle { id }
939 }
940
941 fn cancel(&self, handle: &ScheduleHandle) {
942 if let Some((_, cancel)) = self.timers.remove(&handle.id) {
943 cancel.cancel();
944 }
945 }
946
947 fn dispose(&self) {
948 self.timers.scan(|_, cancel| cancel.cancel());
949 self.timers.clear();
950 }
951}