1use chrono::{DateTime, Utc};
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21use std::sync::Arc;
22use tokio::sync::{mpsc, Mutex, Notify, RwLock};
23use tokio::task::JoinHandle;
24use uuid::Uuid;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum TaskStatus {
34 Running,
36 Completed,
38 Failed,
40 Stopped,
42}
43
44impl TaskStatus {
45 pub fn as_str(&self) -> &'static str {
46 match self {
47 TaskStatus::Running => "running",
48 TaskStatus::Completed => "completed",
49 TaskStatus::Failed => "failed",
50 TaskStatus::Stopped => "stopped",
51 }
52 }
53
54 pub fn is_terminal(self) -> bool {
56 !matches!(self, TaskStatus::Running)
57 }
58}
59
60#[derive(Debug)]
66pub struct TaskState {
67 pub id: TaskId,
69 pub description: String,
71 pub team: String,
73 pub name: String,
75 pub started_at: DateTime<Utc>,
77 status: Mutex<TaskStatus>,
79 pub(crate) output: Mutex<Vec<String>>,
84 pub output_tx: mpsc::UnboundedSender<String>,
86 output_rx: Mutex<Option<mpsc::UnboundedReceiver<String>>>,
88 handle: Mutex<Option<JoinHandle<()>>>,
90 pub final_result: Mutex<Option<Result<String, String>>>,
95 pub completed_notify: Arc<Notify>,
102}
103
104impl TaskState {
105 pub fn new(
108 description: impl Into<String>,
109 team: impl Into<String>,
110 name: impl Into<String>,
111 ) -> (Self, TaskId) {
112 let id = TaskId::new();
113 let (tx, rx) = mpsc::unbounded_channel();
114 let state = Self {
115 id: id.clone(),
116 description: description.into(),
117 team: team.into(),
118 name: name.into(),
119 started_at: Utc::now(),
120 status: Mutex::new(TaskStatus::Running),
121 output: Mutex::new(Vec::new()),
122 output_tx: tx,
123 output_rx: Mutex::new(Some(rx)),
124 handle: Mutex::new(None),
125 final_result: Mutex::new(None),
126 completed_notify: Arc::new(Notify::new()),
127 };
128 (state, id)
129 }
130
131 pub async fn set_handle(&self, handle: JoinHandle<()>) {
133 *self.handle.lock().await = Some(handle);
134 }
135
136 pub async fn status(&self) -> TaskStatus {
138 *self.status.lock().await
139 }
140
141 pub async fn append_output(&self, line: String) {
143 self.output.lock().await.push(line);
144 }
145
146 pub async fn output_snapshot(&self) -> Vec<String> {
148 self.output.lock().await.clone()
149 }
150
151 pub async fn drain_output(&self) -> usize {
154 let mut rx_guard = self.output_rx.lock().await;
155 let Some(rx) = rx_guard.as_mut() else {
156 return 0;
157 };
158 let mut count = 0;
159 let mut out = self.output.lock().await;
161 loop {
162 match rx.try_recv() {
163 Ok(line) => {
164 out.push(line);
165 count += 1;
166 }
167 Err(mpsc::error::TryRecvError::Empty) => break,
168 Err(mpsc::error::TryRecvError::Disconnected) => {
169 *rx_guard = None;
171 break;
172 }
173 }
174 }
175 count
176 }
177
178 pub async fn stop(&self) -> bool {
181 let h = self.handle.lock().await;
182 if let Some(handle) = h.as_ref() {
183 handle.abort();
184 *self.status.lock().await = TaskStatus::Stopped;
185 self.completed_notify.notify_one();
186 true
187 } else {
188 false
189 }
190 }
191
192 pub async fn mark_completed(&self, output: String) {
194 *self.status.lock().await = TaskStatus::Completed;
195 *self.final_result.lock().await = Some(Ok(output));
196 self.completed_notify.notify_one();
197 }
198
199 pub async fn mark_failed(&self, err: String) {
201 *self.status.lock().await = TaskStatus::Failed;
202 *self.final_result.lock().await = Some(Err(err));
203 self.completed_notify.notify_one();
204 }
205
206 pub fn completed_notify(&self) -> Arc<Notify> {
213 self.completed_notify.clone()
214 }
215}
216
217#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
224pub struct TaskId(pub String);
225
226impl TaskId {
227 pub fn new() -> Self {
228 Self(format!("task-{}", Uuid::new_v4()))
229 }
230}
231
232impl Default for TaskId {
233 fn default() -> Self {
234 Self::new()
235 }
236}
237
238impl std::fmt::Display for TaskId {
239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240 f.write_str(&self.0)
241 }
242}
243
244#[derive(Clone, Default)]
253pub struct TaskRegistry {
254 inner: Arc<RwLock<TaskRegistryInner>>,
255}
256
257#[derive(Default)]
258struct TaskRegistryInner {
259 tasks: HashMap<TaskId, Arc<TaskState>>,
260 order: Vec<TaskId>,
263}
264
265impl TaskRegistry {
266 pub fn new() -> Self {
267 Self::default()
268 }
269
270 pub async fn register(&self, state: TaskState) -> Arc<TaskState> {
273 let id = state.id.clone();
274 let state = Arc::new(state);
275 let mut inner = self.inner.write().await;
276 inner.order.push(id.clone());
277 inner.tasks.insert(id, state.clone());
278 state
279 }
280
281 pub async fn get(&self, id: &TaskId) -> Option<Arc<TaskState>> {
283 self.inner.read().await.tasks.get(id).cloned()
284 }
285
286 pub async fn list(&self) -> Vec<Arc<TaskState>> {
288 let inner = self.inner.read().await;
289 inner
290 .order
291 .iter()
292 .filter_map(|id| inner.tasks.get(id).cloned())
293 .collect()
294 }
295
296 pub async fn stop(&self, id: &TaskId) -> bool {
299 match self.get(id).await {
300 Some(t) => t.stop().await,
301 None => false,
302 }
303 }
304
305 pub async fn drain_output(&self, id: &TaskId) -> usize {
307 match self.get(id).await {
308 Some(t) => t.drain_output().await,
309 None => 0,
310 }
311 }
312
313 pub async fn append_output(&self, id: &TaskId, line: String) -> bool {
315 match self.get(id).await {
316 Some(t) => {
317 t.append_output(line).await;
318 true
319 }
320 None => false,
321 }
322 }
323
324 pub async fn drain_all(&self) -> usize {
327 let tasks = self.list().await;
328 let mut total = 0;
329 for t in tasks {
330 total += t.drain_output().await;
331 }
332 total
333 }
334
335 pub async fn len(&self) -> usize {
337 self.inner.read().await.tasks.len()
338 }
339
340 pub async fn is_empty(&self) -> bool {
341 self.inner.read().await.tasks.is_empty()
342 }
343
344 pub async fn forget(&self, id: &TaskId) -> bool {
349 let mut inner = self.inner.write().await;
350 let removed = inner.tasks.remove(id).is_some();
351 inner.order.retain(|tid| tid != id);
352 removed
353 }
354}
355
356#[cfg(test)]
361mod tests {
362 use super::*;
363 use std::time::Duration;
364
365 #[test]
366 fn task_id_unique_and_format() {
367 let a = TaskId::new();
368 let b = TaskId::new();
369 assert_ne!(a, b);
370 assert!(a.0.starts_with("task-"));
371 assert!(a.to_string().starts_with("task-"));
372 }
373
374 #[test]
375 fn task_status_as_str_matches_serde() {
376 for s in [
377 TaskStatus::Running,
378 TaskStatus::Completed,
379 TaskStatus::Failed,
380 TaskStatus::Stopped,
381 ] {
382 let j = serde_json::to_string(&s).unwrap();
383 let back: TaskStatus = serde_json::from_str(&j).unwrap();
384 assert_eq!(s, back);
385 }
386 }
387
388 #[test]
389 fn task_status_is_terminal() {
390 assert!(!TaskStatus::Running.is_terminal());
391 assert!(TaskStatus::Completed.is_terminal());
392 assert!(TaskStatus::Failed.is_terminal());
393 assert!(TaskStatus::Stopped.is_terminal());
394 }
395
396 #[tokio::test]
397 async fn register_list_get() {
398 let reg = TaskRegistry::new();
399 let (state, id) = TaskState::new("test task", "alpha", "researcher");
400 let _ = reg.register(state).await;
401 assert_eq!(reg.len().await, 1);
402
403 let listed = reg.list().await;
404 assert_eq!(listed.len(), 1);
405 assert_eq!(listed[0].id, id);
406
407 let got = reg.get(&id).await.unwrap();
408 assert_eq!(got.description, "test task");
409 assert_eq!(got.team, "alpha");
410 assert_eq!(got.name, "researcher");
411 assert_eq!(got.status().await, TaskStatus::Running);
412 }
413
414 #[tokio::test]
415 async fn drain_output_collects_lines() {
416 let reg = TaskRegistry::new();
417 let (state, id) = TaskState::new("t", "", "");
418 let arc = reg.register(state).await;
419
420 arc.output_tx.send("line 1".into()).unwrap();
421 arc.output_tx.send("line 2".into()).unwrap();
422 drop(arc);
425
426 let drained = reg.drain_output(&id).await;
427 assert_eq!(drained, 2);
428 let got = reg.get(&id).await.unwrap();
429 assert_eq!(got.output_snapshot().await, vec!["line 1", "line 2"]);
430
431 assert_eq!(reg.drain_output(&id).await, 0);
433 }
434
435 #[tokio::test]
436 async fn append_output_via_registry() {
437 let reg = TaskRegistry::new();
438 let (state, id) = TaskState::new("t", "", "");
439 let _ = reg.register(state).await;
440 assert!(reg.append_output(&id, "x".into()).await);
441 assert!(!reg.append_output(&TaskId("ghost".into()), "y".into()).await);
442 let task = reg.get(&id).await.unwrap();
443 assert_eq!(task.output_snapshot().await, vec!["x"]);
444 }
445
446 #[tokio::test]
447 async fn mark_completed_and_failed() {
448 let reg = TaskRegistry::new();
449 let (state, id) = TaskState::new("t", "", "");
450 let _ = reg.register(state).await;
451
452 reg.get(&id)
453 .await
454 .unwrap()
455 .mark_completed("done".into())
456 .await;
457 let got = reg.get(&id).await.unwrap();
458 assert_eq!(got.status().await, TaskStatus::Completed);
459 let r = got.final_result.lock().await.clone();
460 assert_eq!(r, Some(Ok("done".into())));
461
462 reg.get(&id).await.unwrap().mark_failed("oops".into()).await;
463 let got = reg.get(&id).await.unwrap();
464 assert_eq!(got.status().await, TaskStatus::Failed);
465 let r = got.final_result.lock().await.clone();
466 assert_eq!(r, Some(Err("oops".into())));
467 }
468
469 #[tokio::test]
470 async fn stop_cancels_join_handle() {
471 let reg = TaskRegistry::new();
472 let (state, id) = TaskState::new("t", "", "");
473 let arc = reg.register(state).await;
474
475 let handle = tokio::spawn(async {
476 tokio::time::sleep(Duration::from_secs(60)).await;
477 });
478 arc.set_handle(handle).await;
479
480 assert!(reg.stop(&id).await);
481 let got = reg.get(&id).await.unwrap();
482 assert_eq!(got.status().await, TaskStatus::Stopped);
483
484 tokio::time::sleep(Duration::from_millis(10)).await;
485 }
486
487 #[tokio::test]
488 async fn completed_notify_wakes_on_mark_completed() {
489 let reg = TaskRegistry::new();
492 let (state, id) = TaskState::new("t", "", "");
493 let arc = reg.register(state).await;
494 let notify = arc.completed_notify();
495
496 let wait_handle = tokio::spawn(async move {
498 tokio::time::timeout(Duration::from_secs(2), notify.notified())
499 .await
500 .is_ok()
501 });
502
503 tokio::time::sleep(Duration::from_millis(20)).await;
505 arc.mark_completed("done".to_string()).await;
506
507 let woke = wait_handle.await.unwrap();
508 assert!(woke, "waiter should have been woken by notify_one()");
509 let got = reg.get(&id).await.unwrap();
510 assert_eq!(got.status().await, TaskStatus::Completed);
511 }
512
513 #[tokio::test]
514 async fn completed_notify_not_lost_when_fired_before_wait() {
515 let reg = TaskRegistry::new();
518 let (state, _id) = TaskState::new("t", "", "");
519 let arc = reg.register(state).await;
520 let notify = arc.completed_notify();
521
522 arc.mark_failed("boom".to_string()).await;
524 let woke = tokio::time::timeout(Duration::from_millis(100), notify.notified())
526 .await
527 .is_ok();
528 assert!(woke, "stored permit must wake a late waiter immediately");
529 }
530
531 #[tokio::test]
532 async fn forget_removes_from_lookup() {
533 let reg = TaskRegistry::new();
534 let (state, id) = TaskState::new("t", "", "");
535 let _ = reg.register(state).await;
536 assert!(reg.forget(&id).await);
537 assert!(reg.get(&id).await.is_none());
538 assert_eq!(reg.len().await, 0);
539 assert!(!reg.forget(&id).await);
540 }
541
542 #[test]
545 fn task_status_as_str_returns_correct_strings() {
546 assert_eq!(TaskStatus::Running.as_str(), "running");
547 assert_eq!(TaskStatus::Completed.as_str(), "completed");
548 assert_eq!(TaskStatus::Failed.as_str(), "failed");
549 assert_eq!(TaskStatus::Stopped.as_str(), "stopped");
550 }
551
552 #[test]
553 fn task_id_display_format() {
554 let id = TaskId("task-abc123".into());
555 assert_eq!(id.to_string(), "task-abc123");
556 assert_eq!(format!("{id}"), "task-abc123");
557 }
558
559 #[tokio::test]
560 async fn drain_output_returns_correct_count() {
561 let reg = TaskRegistry::new();
562 let (state, id) = TaskState::new("t", "", "");
563 let arc = reg.register(state).await;
564
565 arc.output_tx.send("a".into()).unwrap();
567 arc.output_tx.send("b".into()).unwrap();
568 arc.output_tx.send("c".into()).unwrap();
569 drop(arc);
570
571 let count = reg.drain_output(&id).await;
572 assert_eq!(
573 count, 3,
574 "drain must return the exact number of lines drained"
575 );
576
577 assert_eq!(reg.drain_output(&id).await, 0);
579 }
580
581 #[tokio::test]
582 async fn stop_without_handle_returns_false() {
583 let reg = TaskRegistry::new();
584 let (state, id) = TaskState::new("t", "", "");
585 let _ = reg.register(state).await;
586
587 let stopped = reg.stop(&id).await;
589 assert!(!stopped, "stop without handle must return false");
590 }
591
592 #[tokio::test]
593 async fn task_state_output_snapshot_empty_initially() {
594 let reg = TaskRegistry::new();
595 let (state, id) = TaskState::new("task", "team", "name");
596 let _ = reg.register(state).await;
597 let got = reg.get(&id).await.unwrap();
598 let snap = got.output_snapshot().await;
599 assert!(snap.is_empty(), "output must be empty before any drain");
600 }
601
602 #[tokio::test]
603 async fn registry_is_empty_initially_and_nonempty_after_register() {
604 let reg = TaskRegistry::new();
606 assert!(reg.is_empty().await, "new registry must be empty");
607 let (state, _id) = TaskState::new("t", "", "");
608 reg.register(state).await;
609 assert!(
610 !reg.is_empty().await,
611 "registry must not be empty after register"
612 );
613 }
614
615 #[tokio::test]
616 async fn drain_all_drains_multiple_tasks() {
617 let reg = TaskRegistry::new();
621 let (s1, _id1) = TaskState::new("t1", "", "");
622 let (s2, _id2) = TaskState::new("t2", "", "");
623 let tx1 = s1.output_tx.clone();
625 let tx2 = s2.output_tx.clone();
626 reg.register(s1).await;
627 reg.register(s2).await;
628 tx1.send("a".into()).unwrap();
629 tx2.send("b".into()).unwrap();
630 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
632 let total = reg.drain_all().await;
633 assert_eq!(
634 total, 2,
635 "drain_all must return total lines drained across tasks"
636 );
637 }
638
639 #[tokio::test]
640 async fn drain_output_returns_zero_for_missing_task() {
641 let reg = TaskRegistry::new();
643 let ghost_id = TaskId::new();
644 assert_eq!(
645 reg.drain_output(&ghost_id).await,
646 0,
647 "drain_output must return 0 for missing task"
648 );
649 }
650
651 #[tokio::test]
652 async fn append_output_returns_false_for_missing_task() {
653 let reg = TaskRegistry::new();
655 let ghost_id = TaskId::new();
656 let ok = reg.append_output(&ghost_id, "line".into()).await;
657 assert!(!ok, "append_output must return false for missing task");
658 }
659}