Skip to content

Queueing

The pubsub package defines the interfaces for message queues, and channelpubsub and dbpubsub provide two implementations: an in-memory channel backend and a durable database-backed queue.

The interfaces

type PubSub interface {
	Topic(name string) Topic
}

type Topic interface {
	Enqueue(ctx context.Context, data []byte) error
	Dequeue(ctx context.Context) (Message, error)
	Close() error
}

type Message interface {
	ID() string
	Data() []byte
	Ack(ctx context.Context) error
	Nack(ctx context.Context) error
}

A PubSub hands out named Topics. Dequeue blocks until a message is available or the context is canceled. Message.Ack marks a message as processed (it can be deleted or marked finished); Message.Nack signals failure and re-queues it.

RegisterTopic(ctx) registers a pubsub.Topic dependency whose name comes from the inject tag, backed by whatever pubsub.PubSub is registered:

type Handler struct {
	Queue pubsub.Topic `inject:"orders"`
}

channelpubsub: in-memory

channelpubsub.New() returns a PubSub backed by Go channels per topic. It is the default in the generated template:

channelpubsub.Register(ctx)

Messages are held in an unbuffered-per-topic channel with a capacity of 10; Nack puts the message back on the same channel. Because everything lives in process, enqueued messages are lost if the server restarts and only apps in the same process can share a topic.

dbpubsub: database-backed

dbpubsub.New(update) persists messages to a table. Register is the bootstrap consumer:

di.RegisterLazySingletonWith(ctx, func(u database.Update) (pubsub.PubSub, error) {
	return New(u), nil
})

Each message is an Event row with a topic, run_at, status, data, and retries count. Enqueue inserts a row with status pending; Dequeue claims the oldest pending row for the topic with FOR UPDATE SKIP LOCKED, flipping it to processing, and polls once a second until one is ready. Ack marks the row finished, and Nack returns it to pending and schedules it for a retry using an exponential falloff delay.

Because the queue lives in the database, messages survive restarts and topics are shared by every process connected to the same database — including multiple application servers. The events table is created by the migrations in this package.

Injecting topics

Whichever backend is registered, pubsub.RegisterTopic wires up a named Topic for injection. The event package builds on this: event.Register registers a Dispatch over the injected topic, and event.Service consumes a topic, so events and jobs can be routed through the channel or database queue by changing a single bootstrap call.