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

Why Is It Designed This Way?

1. Why are handlers ordinary async functions?

They already have the essential shape: arguments in, future out. TaskFn adapts that shape to Tower Service, FromRequest constructs typed context arguments, and IntoResponse normalizes results. The caller writes application logic without a macro, base class, or runtime-specific handler trait.

2. What does the user have to own, borrow, or clone?

The worker owns its backend and handler. A job payload is moved into the handler. Extractors borrow the Task only while constructing owned arguments. Shared application data must be cloneable and is commonly an Arc<T>; mutable shared state needs an explicit lock, channel, or pool. Retry additionally requires a cloneable request because it must own another attempt.

3. Why separate storage/backend concerns from worker execution?

Durable queues differ in encoding, IDs, locks, acknowledgement, polling, and maintenance. Execution differs in readiness, concurrency, middleware, tracking, events, and shutdown. Backend connects the two through associated types and poll methods, while Backend::Layer inserts storage-specific lifecycle policy. Either side can evolve without copying the other.

4. How does the builder make invalid configurations harder to create?

WorkerBuilder changes type as the backend and layers are added. build is available only when backend types, task types, handler extraction, and the final Tower service agree. Missing or incompatible pieces become trait-bound errors instead of latent runtime branches.

5. Which types and traits appear publicly?

The common surface is WorkerBuilder, Worker, Monitor, Backend, Task, WorkerContext, Data, FromRequest, IntoResponse, TaskSink, retry/error types, and Tower Service/Layer. Backend capability traits expose optional operations without forcing every backend into one enormous interface.

6. How are job errors, retries, shutdown, and middleware represented?

Errors are typed at their source and erased only at composition boundaries. Retry, limits, timeouts, tracing, panic catching, and acknowledgement are Tower layers. Shutdown is a cloneable future and admission state. Tracking and Backend::poll_close turn shutdown into a drain protocol rather than a boolean checked only at loop exit.

7. Where does Apalis use generics versus trait objects?

Generics describe the hot, homogeneous pipeline: backend, codec, task, handler, extractors, service, layer stack, executor, and policy. Trait objects appear where runtime heterogeneity is intentional: boxed errors, listeners, monitor worker factories/futures, and selected erased streams. Static by default, dynamic at collection and failure boundaries.

8. How does the simplest caller experience compare with the internals?

The caller sees an async function and a short builder chain. Internally that function becomes a generic Tower service whose input is decoded from a backend, augmented by typed extensions, guarded by readiness, executed among unordered in-flight futures, observed by policy layers, acknowledged durably, tracked through cancellation, and drained by a monitor.

That asymmetry is the design achievement. The complexity was not deleted; it was concentrated behind contracts that let the ordinary path remain ordinary.