Runbook de Cancelacion de Async Tasks
Runbook para cancelar de forma segura long-running async tasks en Python, JavaScript, Go y Java: cancellation tokens, context propagation, resource cleanup, timeout strategies y graceful shutdown procedures con ejemplos de codigo.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Overview
Este runbook cubre procedures para cancelar de forma segura long-running async tasks across Python, JavaScript, Go y Java. Improper cancellation lleva a resource leaks, partial writes, orphaned connections y inconsistent state. Este documento cubre cancellation tokens, context propagation, resource cleanup, timeout strategies y graceful shutdown.
1. Cancellation Patterns
1.1 Pattern Comparison
Language | Cancellation mechanism | Propagation method
────────────┼────────────────────────────┼──────────────────────────
Python | asyncio.CancelledError | task.cancel() + try/finally
JavaScript | AbortSignal / AbortController | signal.aborted + listeners
Go | context.Context | ctx.Done() channel
Java | CompletableFuture.cancel() | future.cancel(true) + interrupt
1.2 Cancellation Token Concept
Un cancellation token es un signal object que:
- Propagates cancellation de caller a callee
- Puede ser checked en safe points en el code
- Supporta cleanup via finally blocks o defer
- Puede tener un timeout (auto-cancel despues de N seconds)
- Puede tener un deadline (cancela a specific time)
Safe points para check cancellation:
- Antes de I/O operations (DB, HTTP, file)
- Dentro de loops que processean batches
- Antes de acquiring locks
- Al start de cada pipeline stage
2. Python (asyncio)
2.1 Basic Cancellation
import asyncio
async def long_running_task(task_id: str):
try:
for i in range(1000):
# Checkea cancellation en safe points
await asyncio.sleep(0.1)
print(f"Task {task_id}: processing item {i}")
except asyncio.CancelledError:
print(f"Task {task_id}: cancelled, cleaning up...")
# Performea cleanup aqui
await cleanup_resources(task_id)
raise # Re-raise para propagate cancellation
async def cleanup_resources(task_id: str):
print(f"Task {task_id}: closing connections...")
await asyncio.sleep(0.05) # Simulatea cleanup
print(f"Task {task_id}: cleanup complete")
async def main():
task = asyncio.create_task(long_running_task("worker-1"))
# Cancela despues de 2 seconds
await asyncio.sleep(2)
task.cancel()
try:
await task
except asyncio.CancelledError:
print("Task was cancelled successfully")
asyncio.run(main())
2.2 Timeout-Based Cancellation
import asyncio
async def fetch_with_timeout(url: str, timeout: float = 5.0):
try:
result = await asyncio.wait_for(
fetch_data(url),
timeout=timeout,
)
return result
except asyncio.TimeoutError:
print(f"Request to {url} timed out after {timeout}s")
return None
async def fetch_data(url: str):
reader, writer = await asyncio.open_connection(url, 80)
try:
writer.write(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
await writer.drain()
data = await reader.read(4096)
return data
finally:
writer.close()
await writer.wait_closed()
2.3 Graceful Shutdown con Signal Handling
import asyncio
import signal
class GracefulShutdown:
def __init__(self):
self.shutdown_event = asyncio.Event()
self.tasks: set[asyncio.Task] = set()
def setup_signal_handlers(self):
loop = asyncio.get_running_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, self._signal_handler)
def _signal_handler(self):
print("\nShutdown signal received, cancelling tasks...")
self.shutdown_event.set()
async def run_with_shutdown(self, worker_func, num_workers: int = 4):
self.setup_signal_handlers()
# Startea workers
for i in range(num_workers):
task = asyncio.create_task(worker_func(i, self.shutdown_event))
self.tasks.add(task)
task.add_done_callback(self.tasks.discard)
# Wait por shutdown signal
await self.shutdown_event.wait()
# Cancela all tasks
for task in self.tasks:
task.cancel()
# Wait por all tasks para finish cleanup
await asyncio.gather(*self.tasks, return_exceptions=True)
print("All tasks shut down gracefully")
async def worker(worker_id: int, shutdown: asyncio.Event):
try:
while not shutdown.is_set():
print(f"Worker {worker_id}: processing...")
await asyncio.sleep(1)
except asyncio.CancelledError:
print(f"Worker {worker_id}: cleaning up...")
await asyncio.sleep(0.1)
print(f"Worker {worker_id}: done")
raise
asyncio.run(GracefulShutdown().run_with_shutdown(worker))
3. JavaScript (Node.js)
3.1 AbortController Cancellation
async function longRunningTask(signal, taskId) {
try {
for (let i = 0; i < 1000; i++) {
// Checkea cancellation en safe points
if (signal.aborted) {
throw new DOMException('Task cancelled', 'AbortError');
}
await sleep(100, signal);
console.log(`Task ${taskId}: processing item ${i}`);
}
} catch (err) {
if (err.name === 'AbortError') {
console.log(`Task ${taskId}: cancelled, cleaning up...`);
await cleanupResources(taskId);
throw err;
}
throw err;
}
}
function sleep(ms, signal) {
return new Promise((resolve, reject) => {
if (signal.aborted) {
reject(new DOMException('Aborted', 'AbortError'));
return;
}
const timer = setTimeout(() => {
signal.removeEventListener('abort', onAbort);
resolve();
}, ms);
const onAbort = () => {
clearTimeout(timer);
reject(new DOMException('Aborted', 'AbortError'));
};
signal.addEventListener('abort', onAbort, { once: true });
});
}
async function cleanupResources(taskId) {
console.log(`Task ${taskId}: closing connections...`);
await sleep(50);
console.log(`Task ${taskId}: cleanup complete`);
}
// Usage
const controller = new AbortController();
const task = longRunningTask(controller.signal, 'worker-1');
setTimeout(() => controller.abort(), 2000);
try {
await task;
} catch (err) {
if (err.name === 'AbortError') {
console.log('Task cancelled successfully');
} else {
throw err;
}
}
3.2 Fetch con Cancellation
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
return await response.json();
} catch (err) {
if (err.name === 'AbortError') {
console.error(`Request to ${url} timed out after ${timeout}ms`);
return null;
}
throw err;
} finally {
clearTimeout(timeoutId);
}
}
3.3 Graceful Shutdown en Express
const express = require('express');
const app = express();
const server = app.listen(3000);
let connections = new Set();
server.on('connection', (conn) => {
connections.add(conn);
conn.on('close', () => connections.delete(conn));
});
function gracefulShutdown(signal) {
console.log(`\n${signal} received, shutting down...`);
server.close(() => {
console.log('HTTP server closed');
// Force-close idle connections despues de 10s
setTimeout(() => {
connections.forEach((conn) => conn.destroy());
process.exit(0);
}, 10000);
});
// Destroy idle connections immediately
connections.forEach((conn) => {
if (!conn.writableEnded) return;
conn.destroy();
});
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
4. Go (context.Context)
4.1 Basic Cancellation
package main
import (
"context"
"fmt"
"time"
)
func longRunningTask(ctx context.Context, taskID string) error {
for i := 0; i < 1000; i++ {
// Checkea cancellation en safe points
select {
case <-ctx.Done():
fmt.Printf("Task %s: cancelled, cleaning up...\n", taskID)
cleanupResources(taskID)
return ctx.Err()
default:
}
time.Sleep(100 * time.Millisecond)
fmt.Printf("Task %s: processing item %d\n", taskID, i)
}
return nil
}
func cleanupResources(taskID string) {
fmt.Printf("Task %s: closing connections...\n", taskID)
time.Sleep(50 * time.Millisecond)
fmt.Printf("Task %s: cleanup complete\n", taskID)
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
err := longRunningTask(ctx, "worker-1")
if err != nil {
fmt.Printf("Task ended: %v\n", err)
}
}
4.2 Propagating Context Through HTTP Handlers
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
select {
case <-ctx.Done():
// Client disconnected
fmt.Println("Client cancelled request")
return
default:
}
result, err := processWithTimeout(ctx, r.URL.Query().Get("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(result)
}
func processWithTimeout(ctx context.Context, id string) (Result, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
type result struct {
data Result
err error
}
ch := make(chan result, 1)
go func() {
data, err := slowDatabaseQuery(id)
ch <- result{data, err}
}()
select {
case <-ctx.Done():
return Result{}, ctx.Err()
case res := <-ch:
return res.data, res.err
}
}
5. Java (CompletableFuture)
5.1 Basic Cancellation
import java.util.concurrent.*;
public class TaskCancellation {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
for (int i = 0; i < 1000; i++) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("Task cancelled, cleaning up...");
cleanupResources();
throw new InterruptedException("Task cancelled");
}
Thread.sleep(100);
System.out.println("Processing item " + i);
}
return "done";
});
Thread.sleep(2000);
future.cancel(true);
try {
future.get();
} catch (CancellationException e) {
System.out.println("Task was cancelled");
}
executor.shutdown();
}
static void cleanupResources() {
System.out.println("Closing connections...");
try { Thread.sleep(50); } catch (InterruptedException ignored) {}
System.out.println("Cleanup complete");
}
}
5.2 CompletableFuture con Timeout
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(10000);
return "result";
} catch (InterruptedException e) {
throw new CompletionException(e);
}
});
try {
String result = future.orTimeout(5, TimeUnit.SECONDS).get();
System.out.println("Result: " + result);
} catch (ExecutionException e) {
if (e.getCause() instanceof TimeoutException) {
System.out.println("Task timed out after 5 seconds");
future.cancel(true);
}
}
Preguntas Frecuentes
¿Qué es un cancellation token y por qué lo necesito?
Un cancellation token es un object que signalea cuando un task deberia stop. Sin el, no puedes safely stopper long-running operations — killeando threads abruptly puede leavear resources open, data inconsistent y connections leaked. Un token le permite al task checkear cancellation en safe points y performear cleanup en finally blocks o defer statements. Tambien supporta timeouts y deadlines, para que tasks auto-canceleen si corren too long.
¿Cómo handleo cancellation en un pipeline de async stages?
Pasea el cancellation token through every stage del pipeline. Cada stage checkea el token antes de starting work y propagatea cancellation a downstream stages. En Python, usa asyncio.Task objects y cancela el root task. En Go, pasea context.Context a every function. En JavaScript, pasea AbortSignal a cada stage. Cuando cualquier stage se cancela, all upstream y downstream stages deberian stop y clean up.
¿Qué pasa si no propago cancellation?
El task continuea running en el background, consumiendo CPU, memory y connections. Esto se llama “leaked task” o “orphaned goroutine.” Over time, leaked tasks exhaustan resources y causan que el process hanguee o crashee. Siempre propaga cancellation — si un parent se cancela, cancela all children. Usa structured concurrency patterns (TaskGroups en Python, errgroup en Go) para ensure automatic cancellation propagation.
¿Cuánto deberia tomar cleanup despues de cancellation?
Cleanup deberia completear dentro de 5-10 seconds. Setea un hard timeout en cleanup — si toma longer, force-closea resources. En Python, usa asyncio.wait_for(cleanup(), timeout=5). En Go, usa context.WithTimeout para cleanup. En JavaScript, usa Promise.race([cleanup(), timeout(5000)]). Si cleanup excede el timeout, loggea el issue y force-exit. Nunca dejes que cleanup blockee indefinitely — puede prevenir que el process shut down.
¿Deberia usar cancellation o simplemente killear el process?
Usa cancellation para graceful shutdown — permite a tasks finishear current work, flushear buffers, closear connections y writear final state. Kill el process solo como last resort cuando cancellation no funciona dentro de un timeout. En Kubernetes, setea terminationGracePeriodSeconds a 30 (default) — el pod recibe SIGTERM, tu code deberia cancelar tasks y cleanup, y si no exit dentro de 30s, Kubernetes manda SIGKILL. Siempre handlea SIGTERM para graceful shutdown.
See Also
Recursos Relacionados
Thread Pool Sizing Template
Template for documenting thread pool configuration per service: pool type selection, sizing formulas, CPU vs I/O bound tuning, queue strategies, rejection policies, monitoring metrics, and tuning examples for Java, Python, Go, and Node.js.
DocRace Condition Debugging Checklist
Checklist for identifying and fixing race conditions in concurrent code: symptom identification, reproduction strategies, debugging tools, common patterns, fixes using locks, atomics, channels, and prevention techniques with code examples.
GuideComplete Guide to Python Asyncio in Production
Run Python asyncio in production with confidence. Covers event loops, task management, debugging, cancellation, timeouts, backpressure, and patterns for high-concurrency async applications.
GuideComplete Guide to Python Asyncio
Master asynchronous Python programming with asyncio. Covers coroutines, tasks, event loops, async/await, gather, semaphores, queues, HTTP clients, websockets, and debugging async code.
GuideConcurrency Patterns Guide
A guide to common concurrency patterns and what works for writing safe, efficient concurrent code.