Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Build a Smaller Apalis

Rebuild the architectural center, not the spelling of the builder API.

What are we preserving?

  • an ordinary async handler;
  • an owned task payload plus typed shared data;
  • a backend independent from execution;
  • readiness before dequeue;
  • bounded unordered execution;
  • result-aware acknowledgement;
  • retry as composable policy;
  • shutdown that stops admission and drains ownership.

Stage 1: one task and one handler

Define Task<A, Id> and adapt FnMut(A) -> Future into a tiny service with ready and call. Move A into the future. Do not add persistence yet.

Stage 2: a backend contract

Define associated types for payload, compact form, ID, error, and codec. Add poll_ready, poll_next, and poll_close. Implement an in-memory backend with VecDeque and a waker.

Stage 3: the admission loop

Use FuturesUnordered. Poll completed jobs, then backend readiness, then service readiness, and only then dequeue one task. Add a test proving the backend is not polled while the service has no permit.

Stage 4: typed extraction

Add a task extension map keyed by TypeId, a Data<T> extractor, and a two-argument function adapter. Observe why extraction returns owned values rather than references held across .await.

Stage 5: failure and acknowledgement

Return Result from handlers. Wrap execution with an acknowledgement service that records success or failure only after the inner future resolves. Add an abort error and a retry policy that must clone the task.

Stage 6: bounded concurrency and executor injection

Implement readiness with a semaphore permit. Separately accept a generic spawn function. Demonstrate that admission limits and task scheduling solve different problems.

Stage 7: graceful drain

Signal shutdown, make readiness pending for new jobs, drain in-flight handlers and acknowledgements, then close the backend. Add a cancellation test proving a dropped tracked future decrements the count.

Compare with production Apalis

Your reconstruction now contains the same architectural argument. Production Apalis adds many backends and codecs, generated extractor arities, Tower interoperability, richer retry/backoff, worker events and restart policies, status metadata, observability, panic containment, feature gating, and backend maintenance capabilities.

The final exercise is diagnostic: replace the in-memory backend without editing the worker loop, then add middleware without editing the handler. If both work, you preserved the boundaries rather than merely imitating the surface syntax.