1use std::{
21 collections::HashMap,
22 pin::Pin,
23 sync::{Arc, Mutex},
24 time::Instant,
25};
26
27use futures::Future;
28use tokio::sync::oneshot;
29
30use crate::{
31 error::ErrorData as McpError,
32 model::{
33 CallToolResult, DetailedTask, InputRequest, InputRequests, JsonObject, Task, TaskPayload,
34 TaskStatus,
35 },
36};
37
38pub const DEFAULT_TASK_TTL_MS: u64 = 300_000;
40
41pub const DEFAULT_POLL_INTERVAL_MS: u64 = 1_000;
43
44pub fn current_timestamp() -> String {
46 chrono::Utc::now().to_rfc3339()
47}
48
49#[derive(Clone)]
53pub struct TaskContext {
54 task_id: String,
55 inner: Arc<Mutex<TaskManagerInner>>,
56}
57
58impl TaskContext {
59 pub fn task_id(&self) -> &str {
61 &self.task_id
62 }
63
64 pub async fn request_input(
72 &self,
73 key: impl Into<String>,
74 request: InputRequest,
75 ) -> Result<serde_json::Value, TaskExit> {
76 let key = key.into();
77 let (tx, rx) = oneshot::channel();
78 {
79 let mut inner = self.inner.lock().expect("task manager lock poisoned");
80 let entry = inner.tasks.get_mut(&self.task_id).ok_or_else(|| {
81 TaskExit::Error(McpError::internal_error(
82 "task no longer exists".to_string(),
83 None,
84 ))
85 })?;
86 if !entry.used_input_keys.insert(key.clone()) {
87 return Err(TaskExit::Error(McpError::internal_error(
88 format!("inputRequests key {key:?} was already used for this task"),
89 None,
90 )));
91 }
92 entry.pending_inputs.insert(key.clone(), (request, tx));
93 entry.touch();
94 }
95 rx.await.map_err(|_| TaskExit::Cancelled)
97 }
98
99 pub fn set_status_message(&self, message: impl Into<String>) {
101 let mut inner = self.inner.lock().expect("task manager lock poisoned");
102 if let Some(entry) = inner.tasks.get_mut(&self.task_id) {
103 entry.task.status_message = Some(message.into());
104 entry.touch();
105 }
106 }
107
108 pub fn is_cancel_requested(&self) -> bool {
111 let inner = self.inner.lock().expect("task manager lock poisoned");
112 inner
113 .tasks
114 .get(&self.task_id)
115 .is_some_and(|e| e.cancel_requested)
116 }
117
118 pub async fn cancelled(&self) {
129 let mut rx = {
130 let inner = self.inner.lock().expect("task manager lock poisoned");
131 let Some(entry) = inner.tasks.get(&self.task_id) else {
132 return;
133 };
134 if entry.cancel_requested {
135 return;
136 }
137 entry.cancel_signal.subscribe()
138 };
139 while !*rx.borrow_and_update() {
142 if rx.changed().await.is_err() {
143 return;
144 }
145 }
146 }
147}
148
149#[expect(
151 clippy::exhaustive_enums,
152 reason = "error variant for task exit may only be due to error or cancellation"
153)]
154#[derive(Debug)]
155pub enum TaskExit {
156 Cancelled,
159 Error(McpError),
162}
163
164impl From<McpError> for TaskExit {
165 fn from(error: McpError) -> Self {
166 TaskExit::Error(error)
167 }
168}
169
170pub type TaskFuture = Pin<Box<dyn Future<Output = Result<CallToolResult, TaskExit>> + Send>>;
172
173struct TaskEntry {
174 task: Task,
175 terminal: Option<TaskPayload>,
177 terminal_at: Option<Instant>,
179 pending_inputs: HashMap<String, (InputRequest, oneshot::Sender<serde_json::Value>)>,
181 used_input_keys: std::collections::HashSet<String>,
183 cancel_requested: bool,
184 cancel_signal: tokio::sync::watch::Sender<bool>,
188 created: Instant,
189 join_handle: Option<tokio::task::JoinHandle<()>>,
190}
191
192impl TaskEntry {
193 fn touch(&mut self) {
194 self.task.last_updated_at = current_timestamp();
195 }
196
197 fn current_status(&self) -> TaskStatus {
198 match &self.terminal {
199 Some(payload) => payload.status(),
200 None if !self.pending_inputs.is_empty() => TaskStatus::InputRequired,
201 None => TaskStatus::Working,
202 }
203 }
204
205 fn detailed(&self) -> DetailedTask {
206 let payload = match &self.terminal {
207 Some(p) => p.clone(),
208 None if !self.pending_inputs.is_empty() => TaskPayload::InputRequired {
209 input_requests: self
210 .pending_inputs
211 .iter()
212 .map(|(k, (req, _))| (k.clone(), req.clone()))
213 .collect::<InputRequests>(),
214 },
215 None => TaskPayload::Working,
216 };
217 DetailedTask::new(self.task.clone(), payload)
218 }
219}
220
221#[derive(Default)]
222struct TaskManagerInner {
223 tasks: HashMap<String, TaskEntry>,
224}
225
226#[derive(Debug, Clone)]
228#[non_exhaustive]
229pub struct TaskOptions {
230 pub ttl_ms: Option<u64>,
232 pub poll_interval_ms: Option<u64>,
234 pub status_message: Option<String>,
236}
237
238impl Default for TaskOptions {
239 fn default() -> Self {
240 Self {
241 ttl_ms: Some(DEFAULT_TASK_TTL_MS),
242 poll_interval_ms: Some(DEFAULT_POLL_INTERVAL_MS),
243 status_message: None,
244 }
245 }
246}
247
248impl TaskOptions {
249 pub fn new() -> Self {
250 Self::default()
251 }
252
253 pub fn with_ttl_ms(mut self, ttl_ms: impl Into<Option<u64>>) -> Self {
255 self.ttl_ms = ttl_ms.into();
256 self
257 }
258
259 pub fn with_poll_interval_ms(mut self, poll_interval_ms: u64) -> Self {
261 self.poll_interval_ms = Some(poll_interval_ms);
262 self
263 }
264
265 pub fn with_status_message(mut self, message: impl Into<String>) -> Self {
267 self.status_message = Some(message.into());
268 self
269 }
270}
271
272#[derive(Clone, Default)]
300pub struct TaskManager {
301 inner: Arc<Mutex<TaskManagerInner>>,
302}
303
304impl TaskManager {
305 pub fn new() -> Self {
306 Self::default()
307 }
308
309 pub fn spawn<F>(&self, options: TaskOptions, make_future: F) -> Task
316 where
317 F: FnOnce(TaskContext) -> TaskFuture,
318 {
319 let task_id = uuid::Uuid::new_v4().to_string();
320 let now = current_timestamp();
321 let mut task = Task::new(task_id.clone(), TaskStatus::Working, now.clone(), now);
322 task.ttl_ms = options.ttl_ms;
323 task.poll_interval_ms = options.poll_interval_ms;
324 task.status_message = options.status_message;
325
326 let entry = TaskEntry {
327 task: task.clone(),
328 terminal: None,
329 terminal_at: None,
330 pending_inputs: HashMap::new(),
331 used_input_keys: std::collections::HashSet::new(),
332 cancel_requested: false,
333 cancel_signal: tokio::sync::watch::channel(false).0,
334 created: Instant::now(),
335 join_handle: None,
336 };
337 {
338 let mut inner = self.inner.lock().expect("task manager lock poisoned");
339 Self::sweep_expired(&mut inner);
342 inner.tasks.insert(task_id.clone(), entry);
343 }
344
345 let context = TaskContext {
346 task_id: task_id.clone(),
347 inner: self.inner.clone(),
348 };
349 let future = make_future(context);
350 let inner = self.inner.clone();
351 let id_for_task = task_id.clone();
352 let originating_request = crate::service::ORIGINATING_REQUEST
353 .try_with(|id| id.clone())
354 .ok();
355 let handle = tokio::spawn(async move {
356 let result = run_task_operation(originating_request, future).await;
357 let mut inner = inner.lock().expect("task manager lock poisoned");
358 if let Some(entry) = inner.tasks.get_mut(&id_for_task) {
359 if entry.terminal.is_none() {
360 entry.terminal = Some(match result {
361 Ok(result) => TaskPayload::Completed {
362 result: result_to_object(&result),
363 },
364 Err(TaskExit::Cancelled) => TaskPayload::Cancelled,
365 Err(TaskExit::Error(error)) => TaskPayload::Failed {
366 error: error_to_object(&error),
367 },
368 });
369 entry.terminal_at = Some(Instant::now());
370 entry.pending_inputs.clear();
371 entry.touch();
372 entry.task.status = entry.current_status();
373 }
374 entry.join_handle = None;
377 }
378 });
379 match self
380 .inner
381 .lock()
382 .expect("task manager lock poisoned")
383 .tasks
384 .get_mut(&task_id)
385 {
386 Some(entry) => {
390 if entry.terminal.is_none() {
391 entry.join_handle = Some(handle);
392 }
393 }
394 None => handle.abort(),
397 }
398 task
399 }
400
401 pub fn get_task(&self, task_id: &str) -> Result<DetailedTask, McpError> {
403 let mut inner = self.inner.lock().expect("task manager lock poisoned");
404 Self::sweep_expired(&mut inner);
405 let entry = inner
406 .tasks
407 .get_mut(task_id)
408 .ok_or_else(|| unknown_task(task_id))?;
409 entry.task.status = entry.current_status();
410 Ok(entry.detailed())
411 }
412
413 pub fn update_task(
417 &self,
418 task_id: &str,
419 input_responses: impl IntoIterator<Item = (String, serde_json::Value)>,
420 ) -> Result<(), McpError> {
421 let mut inner = self.inner.lock().expect("task manager lock poisoned");
422 Self::sweep_expired(&mut inner);
423 let entry = inner
424 .tasks
425 .get_mut(task_id)
426 .ok_or_else(|| unknown_task(task_id))?;
427 for (key, value) in input_responses {
428 if let Some((_, tx)) = entry.pending_inputs.remove(&key) {
429 let _ = tx.send(value);
431 }
432 }
433 entry.touch();
434 entry.task.status = entry.current_status();
435 Ok(())
436 }
437
438 pub fn cancel_task(&self, task_id: &str) -> Result<(), McpError> {
454 let mut inner = self.inner.lock().expect("task manager lock poisoned");
455 Self::sweep_expired(&mut inner);
456 let entry = inner
457 .tasks
458 .get_mut(task_id)
459 .ok_or_else(|| unknown_task(task_id))?;
460 entry.cancel_requested = true;
461 let _ = entry.cancel_signal.send(true);
462 if entry.terminal.is_none() {
463 entry.pending_inputs.clear();
468 entry.touch();
469 entry.task.status = entry.current_status();
470 }
471 Ok(())
472 }
473
474 pub fn running_task_count(&self) -> usize {
476 let mut inner = self.inner.lock().expect("task manager lock poisoned");
477 Self::sweep_expired(&mut inner);
478 inner
479 .tasks
480 .values()
481 .filter(|e| e.terminal.is_none())
482 .count()
483 }
484
485 pub fn shutdown(&self) {
487 let mut inner = self.inner.lock().expect("task manager lock poisoned");
488 for (_, mut entry) in inner.tasks.drain() {
489 if let Some(handle) = entry.join_handle.take() {
490 handle.abort();
491 }
492 }
493 }
494
495 fn sweep_expired(inner: &mut TaskManagerInner) {
509 for entry in inner.tasks.values_mut() {
511 if entry.terminal.is_none()
512 && let Some(ttl_ms) = entry.task.ttl_ms
513 && entry.created.elapsed().as_millis() >= u128::from(ttl_ms)
514 {
515 if let Some(handle) = entry.join_handle.take() {
516 handle.abort();
517 }
518 entry.terminal = Some(TaskPayload::Failed {
519 error: error_to_object(&McpError::internal_error(
520 "task expired: TTL elapsed before completion".to_string(),
521 None,
522 )),
523 });
524 entry.terminal_at = Some(Instant::now());
525 entry.pending_inputs.clear();
526 entry.touch();
527 entry.task.status = TaskStatus::Failed;
528 }
529 }
530 inner.tasks.retain(|_, entry| {
532 let (Some(ttl_ms), Some(terminal_at)) = (entry.task.ttl_ms, entry.terminal_at) else {
533 return true;
534 };
535 terminal_at.elapsed().as_millis() < u128::from(ttl_ms)
536 });
537 }
538}
539
540fn unknown_task(task_id: &str) -> McpError {
541 McpError::invalid_params(format!("unknown task: {task_id}"), None)
542}
543
544async fn run_task_operation(
545 originating_request: Option<crate::model::RequestId>,
546 future: TaskFuture,
547) -> Result<CallToolResult, TaskExit> {
548 match originating_request {
549 Some(id) => crate::service::ORIGINATING_REQUEST.scope(id, future).await,
550 None => future.await,
551 }
552}
553
554fn result_to_object(result: &CallToolResult) -> JsonObject {
555 match serde_json::to_value(result) {
556 Ok(serde_json::Value::Object(map)) => map,
557 _ => JsonObject::new(),
558 }
559}
560
561fn error_to_object(error: &McpError) -> JsonObject {
562 match serde_json::to_value(error) {
563 Ok(serde_json::Value::Object(map)) => map,
564 _ => JsonObject::new(),
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571 use crate::model::ContentBlock;
572
573 fn ok_result(text: &str) -> CallToolResult {
574 CallToolResult::success(vec![ContentBlock::text(text.to_string())])
575 }
576
577 #[tokio::test]
578 async fn task_completes_and_result_is_inlined() {
579 let manager = TaskManager::new();
580 let task = manager.spawn(TaskOptions::default(), |_ctx| {
581 Box::pin(async { Ok(ok_result("42")) })
582 });
583 assert_eq!(task.status, TaskStatus::Working);
584
585 manager.get_task(&task.task_id).unwrap();
587
588 for _ in 0..100 {
590 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
591 let detailed = manager.get_task(&task.task_id).unwrap();
592 if detailed.status() == TaskStatus::Completed {
593 match detailed.payload {
594 TaskPayload::Completed { result } => {
595 assert!(result.contains_key("content"));
596 return;
597 }
598 other => panic!("unexpected payload: {other:?}"),
599 }
600 }
601 }
602 panic!("task did not complete");
603 }
604
605 #[tokio::test]
606 async fn cancel_settles_to_cancelled_when_operation_honors_it() {
607 let manager = TaskManager::new();
608 let task = manager.spawn(TaskOptions::default(), |ctx| {
609 Box::pin(async move {
610 tokio::select! {
611 _ = ctx.cancelled() => Err(TaskExit::Cancelled),
612 _ = tokio::time::sleep(std::time::Duration::from_secs(60)) => {
613 Ok(ok_result("never"))
614 }
615 }
616 })
617 });
618 manager.cancel_task(&task.task_id).unwrap();
619
620 for _ in 0..100 {
623 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
624 let detailed = manager.get_task(&task.task_id).unwrap();
625 if detailed.status().is_terminal() {
626 assert_eq!(detailed.status(), TaskStatus::Cancelled);
627 return;
628 }
629 }
630 panic!("task did not settle after cancel");
631 }
632
633 #[tokio::test]
634 async fn post_cancel_unrelated_error_settles_as_failed() {
635 let manager = TaskManager::new();
636 let task = manager.spawn(TaskOptions::default(), |ctx| {
637 Box::pin(async move {
638 ctx.cancelled().await;
641 Err(TaskExit::Error(McpError::internal_error(
642 "database write failed",
643 None,
644 )))
645 })
646 });
647 manager.cancel_task(&task.task_id).unwrap();
648
649 for _ in 0..100 {
650 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
651 let detailed = manager.get_task(&task.task_id).unwrap();
652 if detailed.status().is_terminal() {
653 assert_eq!(detailed.status(), TaskStatus::Failed);
654 match detailed.payload {
655 TaskPayload::Failed { error } => {
656 assert!(
657 error.get("message").is_some_and(|m| m
658 .as_str()
659 .is_some_and(|s| s.contains("database write failed"))),
660 "error payload should be preserved: {error:?}"
661 );
662 }
663 other => panic!("unexpected payload: {other:?}"),
664 }
665 return;
666 }
667 }
668 panic!("task did not settle after cancel");
669 }
670
671 #[tokio::test]
672 async fn cancel_is_cooperative_and_lets_the_operation_clean_up() {
673 let manager = TaskManager::new();
674 let (cleanup_tx, cleanup_rx) = oneshot::channel::<&'static str>();
675 let task = manager.spawn(TaskOptions::default(), |ctx| {
676 Box::pin(async move {
677 ctx.cancelled().await;
681 let _ = cleanup_tx.send("cleaned up");
682 Ok(ok_result("finished despite cancel"))
683 })
684 });
685
686 manager.cancel_task(&task.task_id).unwrap();
687
688 let detailed = manager.get_task(&task.task_id).unwrap();
690 assert!(
691 !detailed.status().is_terminal(),
692 "cancel must not force terminal state"
693 );
694
695 let cleanup = tokio::time::timeout(std::time::Duration::from_secs(5), cleanup_rx)
697 .await
698 .expect("cleanup should not time out")
699 .expect("cleanup channel should not be dropped");
700 assert_eq!(cleanup, "cleaned up");
701
702 for _ in 0..100 {
704 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
705 let detailed = manager.get_task(&task.task_id).unwrap();
706 if detailed.status().is_terminal() {
707 assert_eq!(detailed.status(), TaskStatus::Completed);
708 return;
709 }
710 }
711 panic!("task did not settle after cancel");
712 }
713
714 #[tokio::test]
715 async fn cancel_wakes_parked_input_requests() {
716 let manager = TaskManager::new();
717 let (exit_tx, exit_rx) = oneshot::channel::<&'static str>();
718 let task = manager.spawn(TaskOptions::default(), |ctx| {
719 Box::pin(async move {
720 let request: InputRequest = serde_json::from_value(serde_json::json!({
721 "method": "elicitation/create",
722 "params": {
723 "message": "Waiting forever",
724 "requestedSchema": {"type": "object", "properties": {}}
725 }
726 }))
727 .map_err(|e| McpError::internal_error(e.to_string(), None))?;
728 let err = ctx.request_input("k1", request).await.unwrap_err();
730 let _ = exit_tx.send("woken");
731 Err(err)
732 })
733 });
734
735 for _ in 0..100 {
737 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
738 if manager.get_task(&task.task_id).unwrap().status() == TaskStatus::InputRequired {
739 break;
740 }
741 }
742
743 manager.cancel_task(&task.task_id).unwrap();
744 let woken = tokio::time::timeout(std::time::Duration::from_secs(5), exit_rx)
745 .await
746 .expect("parked operation should be woken by cancel")
747 .expect("exit channel should not be dropped");
748 assert_eq!(woken, "woken");
749 assert_eq!(
750 manager.get_task(&task.task_id).unwrap().status(),
751 TaskStatus::Cancelled
752 );
753 }
754
755 #[tokio::test]
756 async fn unknown_task_is_invalid_params() {
757 let manager = TaskManager::new();
760 for err in [
761 manager.get_task("nope").unwrap_err(),
762 manager.cancel_task("nope").unwrap_err(),
763 manager.update_task("nope", []).unwrap_err(),
764 ] {
765 assert_eq!(err.code, crate::model::ErrorCode::INVALID_PARAMS);
766 }
767 }
768
769 #[tokio::test]
770 async fn terminal_tasks_are_evicted_after_retention_window() {
771 let manager = TaskManager::new();
772 let task = manager.spawn(TaskOptions::new().with_ttl_ms(50), |_ctx| {
773 Box::pin(async { Ok(ok_result("fast")) })
774 });
775
776 let mut completed = false;
779 for _ in 0..100 {
780 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
781 if manager.get_task(&task.task_id).unwrap().status() == TaskStatus::Completed {
782 completed = true;
783 break;
784 }
785 }
786 assert!(completed, "task should have completed");
787
788 tokio::time::sleep(std::time::Duration::from_millis(120)).await;
791 let err = manager.get_task(&task.task_id).unwrap_err();
792 assert_eq!(err.code, crate::model::ErrorCode::INVALID_PARAMS);
793 assert_eq!(manager.running_task_count(), 0);
794 }
795
796 #[tokio::test]
797 async fn abandoned_tasks_are_swept_by_other_entry_points() {
798 let manager = TaskManager::new();
801 let abandoned = manager.spawn(TaskOptions::new().with_ttl_ms(10), |_ctx| {
802 Box::pin(async {
803 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
804 Ok(ok_result("never"))
805 })
806 });
807
808 tokio::time::sleep(std::time::Duration::from_millis(40)).await;
811 let _ = manager.spawn(TaskOptions::default(), |_ctx| {
812 Box::pin(async { Ok(ok_result("other")) })
813 });
814 tokio::time::sleep(std::time::Duration::from_millis(40)).await;
815 let _ = manager.spawn(TaskOptions::default(), |_ctx| {
816 Box::pin(async { Ok(ok_result("other2")) })
817 });
818
819 let err = manager.get_task(&abandoned.task_id).unwrap_err();
820 assert_eq!(err.code, crate::model::ErrorCode::INVALID_PARAMS);
821 }
822
823 #[tokio::test]
824 async fn running_task_count_sweeps_expired_tasks() {
825 let manager = TaskManager::new();
826 let _task = manager.spawn(TaskOptions::new().with_ttl_ms(10), |_ctx| {
827 Box::pin(async {
828 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
829 Ok(ok_result("never"))
830 })
831 });
832 assert_eq!(manager.running_task_count(), 1);
833 tokio::time::sleep(std::time::Duration::from_millis(30)).await;
834 assert_eq!(manager.running_task_count(), 0);
837 }
838
839 #[tokio::test]
840 async fn unlimited_ttl_tasks_are_retained() {
841 let manager = TaskManager::new();
842 let task = manager.spawn(TaskOptions::new().with_ttl_ms(None), |_ctx| {
843 Box::pin(async { Ok(ok_result("kept")) })
844 });
845 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
846 let _ = manager.spawn(TaskOptions::default(), |_ctx| {
848 Box::pin(async { Ok(ok_result("other")) })
849 });
850 assert_eq!(
851 manager.get_task(&task.task_id).unwrap().status(),
852 TaskStatus::Completed
853 );
854 }
855
856 #[tokio::test]
857 async fn ttl_expiry_fails_task() {
858 let manager = TaskManager::new();
859 let task = manager.spawn(
860 TaskOptions {
861 ttl_ms: Some(10),
862 ..Default::default()
863 },
864 |_ctx| {
865 Box::pin(async {
866 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
867 Ok(ok_result("never"))
868 })
869 },
870 );
871 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
872 let detailed = manager.get_task(&task.task_id).unwrap();
873 assert_eq!(detailed.status(), TaskStatus::Failed);
874 }
875
876 #[tokio::test]
877 async fn input_required_roundtrip() {
878 let manager = TaskManager::new();
879 let task = manager.spawn(TaskOptions::default(), |ctx| {
880 Box::pin(async move {
881 let request: InputRequest = serde_json::from_value(serde_json::json!({
882 "method": "elicitation/create",
883 "params": {
884 "message": "What is your name?",
885 "requestedSchema": {"type": "object", "properties": {}}
886 }
887 }))
888 .map_err(|e| McpError::internal_error(e.to_string(), None))?;
889 let response = ctx.request_input("name-1", request).await?;
890 let name = response
891 .get("content")
892 .and_then(|c| c.get("name"))
893 .and_then(|v| v.as_str())
894 .unwrap_or("unknown")
895 .to_string();
896 Ok(ok_result(&format!("hello {name}")))
897 })
898 });
899
900 let mut saw_input_required = false;
902 for _ in 0..100 {
903 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
904 let detailed = manager.get_task(&task.task_id).unwrap();
905 if let TaskPayload::InputRequired { input_requests } = &detailed.payload {
906 assert!(input_requests.contains_key("name-1"));
907 saw_input_required = true;
908 break;
909 }
910 }
911 assert!(saw_input_required, "task never reached input_required");
912
913 manager
915 .update_task(
916 &task.task_id,
917 [(
918 "name-1".to_string(),
919 serde_json::json!({"action": "accept", "content": {"name": "Ada"}}),
920 )],
921 )
922 .unwrap();
923
924 for _ in 0..100 {
925 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
926 let detailed = manager.get_task(&task.task_id).unwrap();
927 if detailed.status() == TaskStatus::Completed {
928 return;
929 }
930 }
931 panic!("task did not complete after input response");
932 }
933
934 #[tokio::test]
935 async fn task_operation_reestablishes_request_association_scope() {
936 use crate::{
937 model::RequestId,
938 service::{ORIGINATING_REQUEST, in_request_handler_scope},
939 };
940
941 let manager = TaskManager::new();
942 let observed = Arc::new(Mutex::new(None::<bool>));
943 let observed_in_task = observed.clone();
944
945 ORIGINATING_REQUEST
946 .scope(RequestId::Number(7), async {
947 manager.spawn(TaskOptions::default(), move |_ctx| {
948 let observed_in_task = observed_in_task.clone();
949 Box::pin(async move {
950 *observed_in_task.lock().unwrap() = Some(in_request_handler_scope());
951 Ok(ok_result("done"))
952 })
953 })
954 })
955 .await;
956
957 for _ in 0..100 {
958 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
959 if let Some(scoped) = *observed.lock().unwrap() {
960 assert!(
961 scoped,
962 "task operation must run inside the originating request's association scope"
963 );
964 return;
965 }
966 }
967 panic!("task operation did not run");
968 }
969
970 #[tokio::test]
971 async fn task_operation_without_originating_request_is_unscoped() {
972 use crate::service::in_request_handler_scope;
973
974 let manager = TaskManager::new();
975 let observed = Arc::new(Mutex::new(None::<bool>));
976 let observed_in_task = observed.clone();
977
978 manager.spawn(TaskOptions::default(), move |_ctx| {
979 let observed_in_task = observed_in_task.clone();
980 Box::pin(async move {
981 *observed_in_task.lock().unwrap() = Some(in_request_handler_scope());
982 Ok(ok_result("done"))
983 })
984 });
985
986 for _ in 0..100 {
987 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
988 if let Some(scoped) = *observed.lock().unwrap() {
989 assert!(
990 !scoped,
991 "task operation started without an originating request must remain unscoped"
992 );
993 return;
994 }
995 }
996 panic!("task operation did not run");
997 }
998}