ergon_pgmq_consumer (ergon v0.5.0)
View SourceStreams messages off a pgmq queue, one batch at a time.
Replaces the Broadway pipeline. What it keeps is the delivery contract; what it drops is the demand protocol, which existed to move backpressure between stages that no longer exist.
The cycle
Read a batch, hand every message to the executor pool, wait for all of them, then archive the ones that succeeded in a single call. Repeat.
That shape is the backpressure, which is why there is no equivalent of
ergon_worker's in-flight counter here: exactly one batch is ever outstanding,
and nothing is read until the last one is accounted for. Do not add a counter by
analogy with the job side. It would be measuring something that cannot exceed one
batch.
A full batch means there is probably more waiting, so the next cycle starts immediately rather than sleeping out the poll interval. A short batch means the queue is drained, and the consumer goes back to waiting.
Archive means acknowledge
A message is archived only after its handler returned ok. A failure is reported
by not archiving: the visibility timeout expires and pgmq redelivers. There is
no negative acknowledgement, and nothing here deletes a message outright.
That is what makes delivery survive a BEAM crash mid-batch. Whatever was in flight was never archived, so it comes back. The cost is that redelivery is at-least-once, so handlers must tolerate seeing a message twice.
Two consequences of batching worth knowing:
- One slow message delays its siblings' archive, since the cycle waits for the
whole batch. Bounded by
handler_timeout, which defaults to the visibility timeout. - A message whose handler always fails is redelivered forever. pgmq has no
dead-letter queue;
read_ctcounts deliveries and is the only signal a handler has to give up on one.
Waking up
The poll is always underneath. On top of it, optionally, either a LISTEN
subscription through the node's shared ergon_listener, or a server-side long
poll that blocks until a message arrives. See ergon_pgmq_queue for which to
pick.
Summary
Functions
Child spec for a host that supervises its own pipelines.
Types
-type args() :: #{queue := pgmq_queue(), pool := atom(), read_opts := query_options()}.
-type pgmq_queue() :: #{name := binary(), poll_interval := pos_integer(), batch_size := pos_integer(), visibility_timeout := pos_integer(), concurrency := pos_integer(), handler_timeout := timeout() | undefined, notify_channel := binary() | undefined, read_strategy := pgmq_read_strategy()}.
-type pgmq_read_strategy() :: plain | grouped | grouped_head | grouped_rr | {long_poll, MaxSeconds :: pos_integer(), IntervalMs :: pos_integer()} | {long_poll, plain | grouped | grouped_head | grouped_rr, pos_integer(), pos_integer()}.
Functions
-spec child_spec(args()) -> supervisor:child_spec().
Child spec for a host that supervises its own pipelines.
ergon:start_consumer/2 is the usual way in, but a consumer is an ordinary
gen_server and a host that already owns a supervision tree may prefer to place
it there. Note this spec covers the consumer alone: started this way, it has no
executor pool of its own, so it must be given one.