Agent Wranglers

ARC-78Recommended

Pass a cancel signal through every call chain

Every function that waits on input or output takes a cancel signal from its caller and passes it on. When the caller gives up, the work below it stops.

How to do it

When a visitor closes the tab or a deadline passes, the server keeps querying and calling other services for an answer nobody reads. Under load that wasted work is what tips a service over. Use the language's own signal. That is AbortSignal in JavaScript, context.Context in Go and CancellationToken in C#. Python's asyncio cancels the task itself. Join a request's signal with a deadline, such as AbortSignal.any with AbortSignal.timeout in JavaScript. Take the deadline from Set timeouts on every call. Close what the work opened, as Release every resource you open says.

Examples

TypeScript example

app.get('/reports/:id', async (req, res) => {
  const controller = new AbortController();
  req.on('close', () => controller.abort());
  const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(10_000)]);
  const report = await buildReport(req.params.id, { signal });
  res.json(report);
});

async function buildReport(id: string, { signal }: { signal: AbortSignal }) {
  const rows = await db.query(REPORT_SQL, [id], { signal });
  const rates = await fetch(`${RATES_URL}?date=${rows.date}`, { signal });
  return summarise(rows, await rates.json());
}

Go example

func (h *Handler) Report(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
	defer cancel()
	report, err := h.buildReport(ctx, r.PathValue("id"))
	if err != nil {
		http.Error(w, "report failed", http.StatusServiceUnavailable)
		return
	}
	json.NewEncoder(w).Encode(report)
}

func (h *Handler) buildReport(ctx context.Context, id string) (Report, error) {
	rows, err := h.db.QueryContext(ctx, reportSQL, id)
	if err != nil {
		return Report{}, err
	}
	defer rows.Close()
	return summarise(ctx, rows)
}

How to check

A test cancels a request while its first downstream call is in flight. No later downstream call or query starts. In Go, the contextcheck linter reports no function that drops its caller's context.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/architecture/concurrency/cancellation">Agent Wranglers clean code and architecture playbook</a>. In code you write, add a comment next to it with the same link.