Dispatches jobs to the module that defines them, by type.
Pass one straight to a worker as its :handler:
{Zizq.Worker,
client: MyApp.Zizq,
queues: ["emails", "reports"],
handler: Zizq.Router.new([MyApp.SendEmail, MyApp.GenerateReport])}Each module is asked for its type/0 as it is registered, so the
routing table is built once at startup rather than per job. Two
modules in one list claiming the same type raises ArgumentError.
Plain functions can be registered too:
Zizq.Router.new([
MyApp.SendEmail,
{"ping", fn _payload -> :ok end},
{"audit", fn payload, job -> MyApp.Audit.record(payload, job.id) end}
])Building a route at a time
route/2 and route/3 add to a router, so routes can be assembled
conditionally rather than declared all at once:
Zizq.Router.new()
|> Zizq.Router.route(MyApp.SendEmail)
|> Zizq.Router.route("charge_card", &MyApp.Billing.charge/2)
|> Zizq.Router.fallback(&MyApp.unknown_job/1)Unlike a list — where a repeated type is a mistake and raises —
route/2 and route/3 replace any route already registered for
that type. That is what makes a router of shared defaults useful as a
starting point:
base_router() |> Zizq.Router.route("charge_card", &test_double/1)Unrecognised types
A job whose type is in no route raises Zizq.Router.UnknownJobType,
which the worker reports as a failure like any other exception, so
the job retries, and dies once its retry limit is spent. Retrying is
usually right: during a rolling deploy a job enqueued by new code can
reach a worker running old code, and the retry lands on a worker that
knows the type. Or occasionally a bug is introduced and detected in
your error tracker so you have time to rectify it.
A fallback handles those instead of raising. It is optional, and
receives the whole Zizq.Job, since a payload alone says little
about a job you did not expect:
Zizq.Router.new(modules) |> Zizq.Router.fallback(&MyApp.unknown/1)A fallback takes the same argument a handler does, so one router can defer to another:
Zizq.Router.fallback(specific, Zizq.Router.build(general))Why types are registered, never resolved
Turning a wire type into a module at runtime would mean
String.to_existing_atom/1 on data from the queue. Building a table
from modules the application named itself keeps job data as something
looked up, never something that selects code to run.
Summary
Functions
Compile a router into a one-argument function.
Handle jobs matching no route with fun, instead of raising.
Build a router from a list of job modules.
Register a module that uses Zizq.JobKind, under its own type.
Register a handler for type.
Types
@type route() :: (term() -> term()) | (term(), Zizq.Job.t() -> term())
@type t() :: %Zizq.Router{ fallback: (Zizq.Job.t() -> term()) | nil, routes: %{optional(String.t()) => (term(), Zizq.Job.t() -> term())} }
Functions
@spec build(t()) :: (Zizq.Job.t() -> term())
Compile a router into a one-argument function.
Zizq.Worker does this once when it starts, so a router can be given
to it directly. Call it yourself to hold a plain function (e.g. to use
as another router's fallback, or to dispatch without a worker).
@spec fallback(t(), (Zizq.Job.t() -> term())) :: t()
Handle jobs matching no route with fun, instead of raising.
Replaces any fallback already registered.
Build a router from a list of job modules.
Options
:fallback— a one-argument function called with theZizq.Jobwhen no route matches, instead of raising.
Register a module that uses Zizq.JobKind, under its own type.
Register a handler for type.
The handler takes the payload, or the payload and the Zizq.Job.
Replaces any route already registered for that type.