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 let mut call = self.interceptors.begin(&call_ctx);
42 let result = async {
43 call.before().await?;
44 self.ensure_required_extensions(&call_ctx)?;
47
48 let task_id = TaskId::new(¶ms.id);
49 let task = self
50 .task_store
51 .get(&task_id)
52 .await?
53 .ok_or_else(|| ServerError::TaskNotFound(task_id.clone()))?;
54
55 if task.status.state.is_terminal() {
56 return Err(ServerError::TaskNotCancelable(task_id));
57 }
58
59 {
61 let tokens = self.cancellation_tokens.read().await;
62 if let Some(entry) = tokens.get(&task_id) {
63 entry.token.cancel();
64 }
65 }
66
67 let ctx = RequestContext::new(
69 a2a_protocol_types::message::Message {
70 id: a2a_protocol_types::message::MessageId::new(
71 uuid::Uuid::new_v4().to_string(),
72 ),
73 role: a2a_protocol_types::message::MessageRole::User,
74 parts: vec![],
75 task_id: Some(task_id.clone()),
76 context_id: Some(task.context_id.clone()),
77 reference_task_ids: None,
78 extensions: None,
79 metadata: None,
80 },
81 task_id.clone(),
82 task.context_id.0.clone(),
83 )
84 .with_call_context(call_ctx.clone());
87
88 let writer = self.event_queue_manager.writer_for_cancel(&task_id).await;
96 self.executor.cancel(&ctx, writer.as_ref()).await?;
97
98 let current = self
104 .task_store
105 .get(&task_id)
106 .await?
107 .ok_or_else(|| ServerError::TaskNotFound(task_id.clone()))?;
108 if current.status.state == a2a_protocol_types::task::TaskState::Canceled {
109 return Ok(current);
110 }
111 if current.status.state.is_terminal() {
112 return Err(ServerError::TaskNotCancelable(task_id));
113 }
114
115 let mut updated = current;
116 updated.status = TaskStatus::with_timestamp(TaskState::Canceled);
117 if let Err(e) = self.task_store.save(&updated).await {
123 return Err(match crate::store::TerminalStateConflict::from_error(&e) {
124 Some(_) => ServerError::TaskNotCancelable(task_id),
125 None => e.into(),
126 });
127 }
128 let final_task = self
130 .task_store
131 .get(&task_id)
132 .await?
133 .ok_or_else(|| ServerError::TaskNotFound(task_id.clone()))?;
134
135 Ok(final_task)
136 }
137 .await;
138 call.finish(result).await
139 })
140 .await;
141
142 let elapsed = start.elapsed();
143 match &result {
144 Ok(_) => {
145 self.metrics.on_response("CancelTask");
146 self.metrics.on_latency("CancelTask", elapsed);
147 }
148 Err(e) => {
149 self.metrics.on_error("CancelTask", e.metric_label());
150 self.metrics.on_latency("CancelTask", elapsed);
151 }
152 }
153 result
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use a2a_protocol_types::params::CancelTaskParams;
160 use a2a_protocol_types::task::{ContextId, Task, TaskId, TaskState, TaskStatus};
161
162 use crate::agent_executor;
163 use crate::builder::RequestHandlerBuilder;
164 use crate::error::ServerError;
165
166 struct DummyExecutor;
167 agent_executor!(DummyExecutor, |_ctx, _queue| async { Ok(()) });
168
169 struct CancelableExecutor;
170 agent_executor!(CancelableExecutor,
171 execute: |_ctx, _queue| async { Ok(()) },
172 cancel: |_ctx, _queue| async { Ok(()) }
173 );
174
175 fn make_completed_task(id: &str) -> Task {
176 Task {
177 id: TaskId::new(id),
178 context_id: ContextId::new("ctx-1"),
179 status: TaskStatus::new(TaskState::Completed),
180 history: None,
181 artifacts: None,
182 metadata: None,
183 }
184 }
185
186 fn make_submitted_task(id: &str) -> Task {
187 Task {
188 id: TaskId::new(id),
189 context_id: ContextId::new("ctx-1"),
190 status: TaskStatus::new(TaskState::Submitted),
191 history: None,
192 artifacts: None,
193 metadata: None,
194 }
195 }
196
197 #[tokio::test]
198 async fn cancel_task_not_found_returns_error() {
199 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
200 let params = CancelTaskParams {
201 tenant: None,
202 id: "nonexistent-task".to_owned(),
203 metadata: None,
204 };
205 let result = handler.on_cancel_task(params, None).await;
206 assert!(
207 matches!(result, Err(ServerError::TaskNotFound(_))),
208 "expected TaskNotFound for missing task, got: {result:?}"
209 );
210 }
211
212 #[tokio::test]
213 async fn cancel_task_terminal_state_returns_not_cancelable() {
214 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
215 let task = make_completed_task("t-cancel-terminal");
216 handler.task_store.save(&task).await.unwrap();
217
218 let params = CancelTaskParams {
219 tenant: None,
220 id: "t-cancel-terminal".to_owned(),
221 metadata: None,
222 };
223 let result = handler.on_cancel_task(params, None).await;
224 assert!(
225 matches!(result, Err(ServerError::TaskNotCancelable(_))),
226 "expected TaskNotCancelable for completed task, got: {result:?}"
227 );
228 }
229
230 #[tokio::test]
231 async fn cancel_task_non_terminal_succeeds() {
232 let handler = RequestHandlerBuilder::new(CancelableExecutor)
233 .build()
234 .unwrap();
235 let task = make_submitted_task("t-cancel-active");
236 handler.task_store.save(&task).await.unwrap();
237
238 let params = CancelTaskParams {
239 tenant: None,
240 id: "t-cancel-active".to_owned(),
241 metadata: None,
242 };
243 let result = handler.on_cancel_task(params, None).await;
244 assert!(
245 result.is_ok(),
246 "canceling a non-terminal task should succeed, got: {result:?}"
247 );
248 assert_eq!(
249 result.unwrap().status.state,
250 TaskState::Canceled,
251 "canceled task should have Canceled state"
252 );
253 }
254
255 #[tokio::test]
260 async fn cancel_task_does_not_leak_event_queue() {
261 let handler = RequestHandlerBuilder::new(CancelableExecutor)
262 .build()
263 .unwrap();
264 handler
266 .task_store
267 .save(&make_submitted_task("t-no-leak"))
268 .await
269 .unwrap();
270 assert_eq!(
271 handler.event_queue_manager.active_count().await,
272 0,
273 "precondition: no queue exists"
274 );
275
276 let params = CancelTaskParams {
277 tenant: None,
278 id: "t-no-leak".to_owned(),
279 metadata: None,
280 };
281 let result = handler.on_cancel_task(params, None).await;
282 assert!(result.is_ok(), "cancel should succeed, got {result:?}");
283 assert_eq!(result.unwrap().status.state, TaskState::Canceled);
284
285 assert_eq!(
286 handler.event_queue_manager.active_count().await,
287 0,
288 "cancel must not leave a leaked event queue behind"
289 );
290 }
291
292 #[tokio::test]
293 async fn cancel_task_error_path_records_metrics() {
294 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
296 let params = CancelTaskParams {
297 tenant: None,
298 id: "nonexistent-for-metrics".to_owned(),
299 metadata: None,
300 };
301 let result = handler.on_cancel_task(params, None).await;
302 assert!(
303 matches!(result, Err(ServerError::TaskNotFound(_))),
304 "expected TaskNotFound, got: {result:?}"
305 );
306 }
308
309 #[tokio::test]
314 async fn cancel_working_task_with_default_executor_succeeds() {
315 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
316 let mut task = make_submitted_task("cancel-default-1");
317 task.status = TaskStatus::new(TaskState::Working);
318 handler.task_store.save(&task).await.unwrap();
319
320 let result = handler
321 .on_cancel_task(
322 CancelTaskParams {
323 tenant: None,
324 id: "cancel-default-1".to_owned(),
325 metadata: None,
326 },
327 None,
328 )
329 .await
330 .expect("cancel of a WORKING task must succeed with the default executor");
331 assert_eq!(result.status.state, TaskState::Canceled);
332 }
333
334 type Fut<'a, T> = std::pin::Pin<
335 Box<dyn std::future::Future<Output = a2a_protocol_types::error::A2aResult<T>> + Send + 'a>,
336 >;
337
338 struct StaleReads(crate::store::InMemoryTaskStore);
342
343 impl crate::store::TaskStore for StaleReads {
344 fn save<'a>(&'a self, t: &'a Task) -> Fut<'a, ()> {
345 self.0.save(t)
346 }
347 fn get<'a>(&'a self, id: &'a TaskId) -> Fut<'a, Option<Task>> {
348 Box::pin(async move {
349 let mut task = self.0.get(id).await?;
350 if let Some(t) = task.as_mut() {
351 t.status = TaskStatus::new(TaskState::Working);
352 }
353 Ok(task)
354 })
355 }
356 fn list<'a>(
357 &'a self,
358 p: &'a a2a_protocol_types::params::ListTasksParams,
359 ) -> Fut<'a, a2a_protocol_types::responses::TaskListResponse> {
360 self.0.list(p)
361 }
362 fn insert_if_absent<'a>(&'a self, t: &'a Task) -> Fut<'a, bool> {
363 self.0.insert_if_absent(t)
364 }
365 fn delete<'a>(&'a self, id: &'a TaskId) -> Fut<'a, ()> {
366 self.0.delete(id)
367 }
368 }
369
370 #[tokio::test]
375 async fn a_task_that_finished_after_the_re_read_is_not_cancelable() {
376 use crate::store::TaskStore as _;
377 let inner = crate::store::InMemoryTaskStore::new();
378 inner
379 .save(&make_completed_task("t-late-finish"))
380 .await
381 .unwrap();
382 let handler = RequestHandlerBuilder::new(CancelableExecutor)
383 .with_task_store(StaleReads(inner))
384 .build()
385 .unwrap();
386
387 let result = handler
388 .on_cancel_task(
389 CancelTaskParams {
390 tenant: None,
391 id: "t-late-finish".to_owned(),
392 metadata: None,
393 },
394 None,
395 )
396 .await;
397 assert!(
398 matches!(result, Err(ServerError::TaskNotCancelable(_))),
399 "expected TaskNotCancelable, got {result:?}"
400 );
401 }
402}