a2a_protocol_server/handler/lifecycle/
cancel_task.rs1use std::collections::HashMap;
9use std::time::Instant;
10
11use a2a_protocol_types::params::CancelTaskParams;
12use a2a_protocol_types::task::{Task, TaskId, TaskState, TaskStatus};
13
14use crate::error::{ServerError, ServerResult};
15use crate::request_context::RequestContext;
16
17use super::super::RequestHandler;
18use super::super::helpers::build_call_context;
19
20impl RequestHandler {
21 #[allow(clippy::too_many_lines)]
27 pub async fn on_cancel_task(
28 &self,
29 params: CancelTaskParams,
30 headers: Option<&HashMap<String, String>>,
31 ) -> ServerResult<Task> {
32 let start = Instant::now();
33 trace_info!(method = "CancelTask", task_id = %params.id, "handling cancel task");
34 self.metrics.on_request("CancelTask");
35
36 let tenant = self
37 .resolve_tenant("CancelTask", headers, params.tenant.as_deref())
38 .await?;
39 let result: ServerResult<_> = crate::store::tenant::TenantContext::scope(tenant, async {
40 let call_ctx = build_call_context("CancelTask", headers, self.inbound_trace_policy);
41 self.interceptors.run_before(&call_ctx).await?;
42 self.ensure_required_extensions(&call_ctx)?;
45
46 let task_id = TaskId::new(¶ms.id);
47 let task = self
48 .task_store
49 .get(&task_id)
50 .await?
51 .ok_or_else(|| ServerError::TaskNotFound(task_id.clone()))?;
52
53 if task.status.state.is_terminal() {
54 return Err(ServerError::TaskNotCancelable(task_id));
55 }
56
57 {
59 let tokens = self.cancellation_tokens.read().await;
60 if let Some(entry) = tokens.get(&task_id) {
61 entry.token.cancel();
62 }
63 }
64
65 let ctx = RequestContext::new(
67 a2a_protocol_types::message::Message {
68 id: a2a_protocol_types::message::MessageId::new(
69 uuid::Uuid::new_v4().to_string(),
70 ),
71 role: a2a_protocol_types::message::MessageRole::User,
72 parts: vec![],
73 task_id: Some(task_id.clone()),
74 context_id: Some(task.context_id.clone()),
75 reference_task_ids: None,
76 extensions: None,
77 metadata: None,
78 },
79 task_id.clone(),
80 task.context_id.0.clone(),
81 )
82 .with_call_context(call_ctx.clone());
85
86 let writer = self.event_queue_manager.writer_for_cancel(&task_id).await;
94 self.executor.cancel(&ctx, writer.as_ref()).await?;
95
96 let current = self
102 .task_store
103 .get(&task_id)
104 .await?
105 .ok_or_else(|| ServerError::TaskNotFound(task_id.clone()))?;
106 if current.status.state == a2a_protocol_types::task::TaskState::Canceled {
107 self.interceptors.run_after(&call_ctx).await?;
108 return Ok(current);
109 }
110 if current.status.state.is_terminal() {
111 return Err(ServerError::TaskNotCancelable(task_id));
112 }
113
114 let mut updated = current;
115 updated.status = TaskStatus::with_timestamp(TaskState::Canceled);
116 self.task_store.save(&updated).await?;
117 let final_task = self
119 .task_store
120 .get(&task_id)
121 .await?
122 .ok_or_else(|| ServerError::TaskNotFound(task_id.clone()))?;
123
124 self.interceptors.run_after(&call_ctx).await?;
125 Ok(final_task)
126 })
127 .await;
128
129 let elapsed = start.elapsed();
130 match &result {
131 Ok(_) => {
132 self.metrics.on_response("CancelTask");
133 self.metrics.on_latency("CancelTask", elapsed);
134 }
135 Err(e) => {
136 self.metrics.on_error("CancelTask", e.metric_label());
137 self.metrics.on_latency("CancelTask", elapsed);
138 }
139 }
140 result
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use a2a_protocol_types::params::CancelTaskParams;
147 use a2a_protocol_types::task::{ContextId, Task, TaskId, TaskState, TaskStatus};
148
149 use crate::agent_executor;
150 use crate::builder::RequestHandlerBuilder;
151 use crate::error::ServerError;
152
153 struct DummyExecutor;
154 agent_executor!(DummyExecutor, |_ctx, _queue| async { Ok(()) });
155
156 struct CancelableExecutor;
157 agent_executor!(CancelableExecutor,
158 execute: |_ctx, _queue| async { Ok(()) },
159 cancel: |_ctx, _queue| async { Ok(()) }
160 );
161
162 fn make_completed_task(id: &str) -> Task {
163 Task {
164 id: TaskId::new(id),
165 context_id: ContextId::new("ctx-1"),
166 status: TaskStatus::new(TaskState::Completed),
167 history: None,
168 artifacts: None,
169 metadata: None,
170 }
171 }
172
173 fn make_submitted_task(id: &str) -> Task {
174 Task {
175 id: TaskId::new(id),
176 context_id: ContextId::new("ctx-1"),
177 status: TaskStatus::new(TaskState::Submitted),
178 history: None,
179 artifacts: None,
180 metadata: None,
181 }
182 }
183
184 #[tokio::test]
185 async fn cancel_task_not_found_returns_error() {
186 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
187 let params = CancelTaskParams {
188 tenant: None,
189 id: "nonexistent-task".to_owned(),
190 metadata: None,
191 };
192 let result = handler.on_cancel_task(params, None).await;
193 assert!(
194 matches!(result, Err(ServerError::TaskNotFound(_))),
195 "expected TaskNotFound for missing task, got: {result:?}"
196 );
197 }
198
199 #[tokio::test]
200 async fn cancel_task_terminal_state_returns_not_cancelable() {
201 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
202 let task = make_completed_task("t-cancel-terminal");
203 handler.task_store.save(&task).await.unwrap();
204
205 let params = CancelTaskParams {
206 tenant: None,
207 id: "t-cancel-terminal".to_owned(),
208 metadata: None,
209 };
210 let result = handler.on_cancel_task(params, None).await;
211 assert!(
212 matches!(result, Err(ServerError::TaskNotCancelable(_))),
213 "expected TaskNotCancelable for completed task, got: {result:?}"
214 );
215 }
216
217 #[tokio::test]
218 async fn cancel_task_non_terminal_succeeds() {
219 let handler = RequestHandlerBuilder::new(CancelableExecutor)
220 .build()
221 .unwrap();
222 let task = make_submitted_task("t-cancel-active");
223 handler.task_store.save(&task).await.unwrap();
224
225 let params = CancelTaskParams {
226 tenant: None,
227 id: "t-cancel-active".to_owned(),
228 metadata: None,
229 };
230 let result = handler.on_cancel_task(params, None).await;
231 assert!(
232 result.is_ok(),
233 "canceling a non-terminal task should succeed, got: {result:?}"
234 );
235 assert_eq!(
236 result.unwrap().status.state,
237 TaskState::Canceled,
238 "canceled task should have Canceled state"
239 );
240 }
241
242 #[tokio::test]
247 async fn cancel_task_does_not_leak_event_queue() {
248 let handler = RequestHandlerBuilder::new(CancelableExecutor)
249 .build()
250 .unwrap();
251 handler
253 .task_store
254 .save(&make_submitted_task("t-no-leak"))
255 .await
256 .unwrap();
257 assert_eq!(
258 handler.event_queue_manager.active_count().await,
259 0,
260 "precondition: no queue exists"
261 );
262
263 let params = CancelTaskParams {
264 tenant: None,
265 id: "t-no-leak".to_owned(),
266 metadata: None,
267 };
268 let result = handler.on_cancel_task(params, None).await;
269 assert!(result.is_ok(), "cancel should succeed, got {result:?}");
270 assert_eq!(result.unwrap().status.state, TaskState::Canceled);
271
272 assert_eq!(
273 handler.event_queue_manager.active_count().await,
274 0,
275 "cancel must not leave a leaked event queue behind"
276 );
277 }
278
279 #[tokio::test]
280 async fn cancel_task_error_path_records_metrics() {
281 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
283 let params = CancelTaskParams {
284 tenant: None,
285 id: "nonexistent-for-metrics".to_owned(),
286 metadata: None,
287 };
288 let result = handler.on_cancel_task(params, None).await;
289 assert!(
290 matches!(result, Err(ServerError::TaskNotFound(_))),
291 "expected TaskNotFound, got: {result:?}"
292 );
293 }
295
296 #[tokio::test]
301 async fn cancel_working_task_with_default_executor_succeeds() {
302 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
303 let mut task = make_submitted_task("cancel-default-1");
304 task.status = TaskStatus::new(TaskState::Working);
305 handler.task_store.save(&task).await.unwrap();
306
307 let result = handler
308 .on_cancel_task(
309 CancelTaskParams {
310 tenant: None,
311 id: "cancel-default-1".to_owned(),
312 metadata: None,
313 },
314 None,
315 )
316 .await
317 .expect("cancel of a WORKING task must succeed with the default executor");
318 assert_eq!(result.status.state, TaskState::Canceled);
319 }
320}