Zizq.Router (Zizq v0.6.1)

Copy Markdown View Source

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

entry()

@type entry() :: module() | {String.t(), route()}

route()

@type route() :: (term() -> term()) | (term(), Zizq.Job.t() -> term())

t()

@type t() :: %Zizq.Router{
  fallback: (Zizq.Job.t() -> term()) | nil,
  routes: %{optional(String.t()) => (term(), Zizq.Job.t() -> term())}
}

Functions

build(router)

@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).

fallback(router, fun)

@spec fallback(t(), (Zizq.Job.t() -> term())) :: t()

Handle jobs matching no route with fun, instead of raising.

Replaces any fallback already registered.

new(entries \\ [], opts \\ [])

@spec new(
  [entry()],
  keyword()
) :: t()

Build a router from a list of job modules.

Options

  • :fallback — a one-argument function called with the Zizq.Job when no route matches, instead of raising.

route(router, module)

@spec route(t(), module()) :: t()

Register a module that uses Zizq.JobKind, under its own type.

route(router, type, fun)

@spec route(t(), String.t(), route()) :: t()

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.