agent_engine/events/
ingest.rs1use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7use super::queue::EventQueue;
8use super::types::Event;
9use notify::Watcher;
10
11async fn process_file(path: &Path, queue: &EventQueue) {
12 #[cfg(unix)]
13 {
14 if let Ok(meta) = tokio::fs::symlink_metadata(path).await {
15 if meta.file_type().is_symlink() {
16 tracing::warn!("refusing symlink in inbox: {}", path.display());
17 let _ = tokio::fs::remove_file(path).await;
18 return;
19 }
20 }
21 }
22 if path.extension().is_some_and(|e| e == "json") {
23 if let Ok(meta) = tokio::fs::metadata(path).await {
24 if meta.len() > 256 * 1024 {
25 tracing::warn!(
26 "inbox file too large ({}B), skipping: {}",
27 meta.len(),
28 path.display()
29 );
30 let _ = tokio::fs::rename(path, path.with_extension("json.oversized")).await;
31 return;
32 }
33 }
34 match tokio::fs::read_to_string(path).await {
35 Ok(content) => match serde_json::from_str::<Event>(&content) {
36 Ok(event) => {
37 tracing::info!(
38 "Inbox event: {} from {}",
39 event.id,
40 event.source.source_type
41 );
42 match queue.push(event) {
43 Ok(()) => {
44 let _ = tokio::fs::remove_file(path).await;
45 }
46 Err(e) => {
47 tracing::warn!("Queue full, retry later: {}", e);
48 }
49 }
50 }
51 Err(e) => {
52 tracing::warn!("Invalid event {}: {}", path.display(), e);
53 let _ = tokio::fs::rename(path, path.with_extension("json.error")).await;
54 }
55 },
56 Err(e) => tracing::warn!("Read failed {}: {}", path.display(), e),
57 }
58 }
59}
60
61async fn scan_inbox(dir: &Path, queue: &EventQueue) {
62 if let Ok(mut entries) = tokio::fs::read_dir(dir).await {
63 while let Ok(Some(entry)) = entries.next_entry().await {
64 process_file(&entry.path(), queue).await;
65 }
66 }
67}
68
69pub async fn watch_inbox(
70 inbox_dir: PathBuf,
71 queue: Arc<EventQueue>,
72 shutdown: Arc<std::sync::atomic::AtomicBool>,
73) {
74 let _ = tokio::fs::create_dir_all(&inbox_dir).await;
75 #[cfg(unix)]
76 {
77 use std::os::unix::fs::PermissionsExt;
78 if let Ok(meta) = tokio::fs::metadata(&inbox_dir).await {
79 let mut perms = meta.permissions();
80 perms.set_mode(0o700);
81 let _ = tokio::fs::set_permissions(&inbox_dir, perms).await;
82 }
83 }
84 scan_inbox(&inbox_dir, &queue).await;
85
86 let (async_tx, mut async_rx) = tokio::sync::mpsc::unbounded_channel::<Vec<PathBuf>>();
88
89 let shutdown_flag = shutdown.clone();
91
92 let inbox_clone = inbox_dir.clone();
94 let _watcher_handle = tokio::task::spawn_blocking(move || {
95 let (tx, rx) = std::sync::mpsc::channel();
96 let mut _watcher: notify::RecommendedWatcher =
97 match notify::RecommendedWatcher::new(tx, notify::Config::default()) {
98 Ok(w) => w,
99 Err(e) => {
100 tracing::warn!("inotify unavailable: {}", e);
101 return;
102 }
103 };
104
105 if let Err(e) = _watcher.watch(&inbox_clone, notify::RecursiveMode::NonRecursive) {
106 tracing::warn!("watch failed: {}", e);
107 return;
108 }
109 tracing::info!("Inbox watcher (inotify) on {}", inbox_clone.display());
110
111 loop {
113 if shutdown_flag.load(std::sync::atomic::Ordering::Relaxed) {
114 break;
115 }
116 match rx.recv_timeout(std::time::Duration::from_millis(500)) {
117 Ok(Ok(event)) => {
118 if !event.paths.is_empty() {
119 let _ = async_tx.send(event.paths);
120 }
121 }
122 Ok(Err(e)) => tracing::warn!("notify error: {}", e),
123 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => continue,
124 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
125 break;
126 }
127 }
128 }
129 });
130
131 let queue_ref = &queue;
133 let dir_ref = &inbox_dir;
134 loop {
135 if shutdown.load(std::sync::atomic::Ordering::Relaxed) {
136 break;
137 }
138 tokio::select! {
139 result = async_rx.recv() => {
140 match result {
141 Some(paths) => {
142 for path in &paths {
143 process_file(path, queue_ref).await;
144 }
145 scan_inbox(dir_ref, queue_ref).await;
147 }
148 None => break, }
150 }
151 _ = tokio::time::sleep(std::time::Duration::from_secs(2)) => {
153 if shutdown.load(std::sync::atomic::Ordering::Relaxed) {
154 break;
155 }
156 scan_inbox(dir_ref, queue_ref).await;
157 }
158 }
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::events::{Event, Severity};
166
167 #[tokio::test]
168 async fn picks_up_dropped_event() {
169 let dir = tempfile::tempdir().unwrap();
170 let inbox = dir.path().to_path_buf();
171 let queue = Arc::new(EventQueue::new(10));
172 let q = queue.clone();
173 let ibx = inbox.clone();
174 let shutdown = Arc::new(std::sync::atomic::AtomicBool::new(false));
175 let shutdown_for_task = shutdown.clone();
176 let handle = tokio::spawn(async move { watch_inbox(ibx, q, shutdown_for_task).await });
177
178 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
179 let event = Event::simple("test", "hello inbox", Some(Severity::High));
180 let path = inbox.join("1.json");
181 tokio::fs::write(&path, serde_json::to_string(&event).unwrap())
182 .await
183 .unwrap();
184
185 for _ in 0..30 {
186 if !queue.is_empty() {
187 break;
188 }
189 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
190 }
191 shutdown.store(true, std::sync::atomic::Ordering::Relaxed);
192 let _ = tokio::time::timeout(std::time::Duration::from_secs(2), handle).await;
193 let popped = queue.pop().expect("event should have been ingested");
194 assert_eq!(popped.content.text, "hello inbox");
195 }
196
197 #[tokio::test]
198 async fn invalid_json_moved_to_error() {
199 let dir = tempfile::tempdir().unwrap();
200 let inbox = dir.path().to_path_buf();
201 let queue = Arc::new(EventQueue::new(10));
202 let q = queue.clone();
203 let ibx = inbox.clone();
204 let shutdown = Arc::new(std::sync::atomic::AtomicBool::new(false));
205 let shutdown_for_task = shutdown.clone();
206 let handle = tokio::spawn(async move { watch_inbox(ibx, q, shutdown_for_task).await });
207
208 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
209 let path = inbox.join("bad.json");
210 tokio::fs::write(&path, "not json").await.unwrap();
211 let err_path = inbox.join("bad.json.error");
212 for _ in 0..30 {
213 if err_path.exists() {
214 break;
215 }
216 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
217 }
218 shutdown.store(true, std::sync::atomic::Ordering::Relaxed);
219 let _ = tokio::time::timeout(std::time::Duration::from_secs(2), handle).await;
220 assert!(err_path.exists());
221 assert_eq!(queue.len(), 0);
222 }
223}