Help us shape the future of Go! Take the 2026 Go Developer Survey

postgres

package
v0.0.0-...-e3c07e6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package postgres is the Postgres-backed queue backend. It is wire- and schema-compatible with existing deployments, so a Go router can be dropped in and drain the SAME queue_messages table that the existing producers write to (and vice versa).

Schema (created by InitSchema; matches the pre-existing layout):

CREATE TABLE queue_messages (
    id               TEXT NOT NULL,
    queue_name       TEXT NOT NULL,
    message_group_id TEXT,
    receipt_handle   TEXT,
    visible_at       BIGINT NOT NULL,   -- unix epoch seconds
    payload          TEXT NOT NULL,     -- JSON-encoded common.Message
    created_at       BIGINT NOT NULL,   -- unix epoch seconds
    receive_count    INTEGER DEFAULT 0,
    PRIMARY KEY (queue_name, id)
);
CREATE INDEX idx_queue_visible
    ON queue_messages (queue_name, visible_at, message_group_id);

Semantics:

  • Claim is keyed on visible_at, NOT on receipt_handle being NULL. A claimed message becomes eligible again once its visibility window lapses, so a crashed consumer's messages are redelivered (at-least-once) without an explicit NACK.
  • FIFO per message group: only the earliest visible message of each group (COALESCE(message_group_id, id)) is eligible at a time, so a NULL group behaves as a singleton keyed by the message id.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func InitSchema

func InitSchema(ctx context.Context, pool *pgxpool.Pool) error

InitSchema creates the queue tables on an existing pool. Exported for the party that OWNS the database the queue lives on — the platform, whose dispatch scheduler publishes there. A router process must not call it against the platform's database: the platform owns that schema, and running DDL from a consumer build fails wherever the shared pool cannot hand out a connection.

func InsertBatch

func InsertBatch(ctx context.Context, pool *pgxpool.Pool, rows []Row) error

InsertBatch writes one row per Row in a single statement, so a failure never partially applies. Each row may name a different queue.

Exported so the dispatch scheduler's Postgres publisher writes through the same column mapping this backend's own publisher uses: two mappings for one table is how a producer and its consumer drift apart. ON CONFLICT DO NOTHING keeps at-least-once publish semantics — re-publishing a message still sitting on the queue is a no-op rather than a duplicate.

Types

type Queue

type Queue struct {
	// contains filtered or unexported fields
}

Queue is the Postgres-backed queue (both consumer + publisher).

func (*Queue) Ack

func (q *Queue) Ack(ctx context.Context, receipt string, _ string) error

Ack deletes the message permanently. brokerMessageID is unused: a Postgres receipt handle embeds the row id and the row is deleted outright, so there is no at-least-once redelivery to guard against.

func (*Queue) Counters

func (q *Queue) Counters() *queue.Metrics

Counters returns process-local counters only.

func (*Queue) Defer

func (q *Queue) Defer(ctx context.Context, receipt string, delaySeconds *uint32) error

Defer restores visibility after delay; not counted as a failure.

func (*Queue) Healthy

func (q *Queue) Healthy() bool

Healthy reports whether we can talk to Postgres.

func (*Queue) HonoursDelayedReturn

func (q *Queue) HonoursDelayedReturn() bool

HonoursDelayedReturn is true (R5, docs/spec/router-deferral-handback.md): Nack's visible_at update really does hold the row back for delay, and the claim query's second NOT EXISTS clause (see Poll's doc comment) blocks a nacked-with-delay group head's successors from claiming ahead of it.

func (*Queue) Identifier

func (q *Queue) Identifier() string

Identifier returns the queue name.

func (*Queue) InitSchema

func (q *Queue) InitSchema(ctx context.Context) error

InitSchema creates the queue table and index (idempotent). The DDL matches the pre-existing layout exactly so it is a no-op when the table was already provisioned by the existing system.

func (*Queue) Metrics

func (q *Queue) Metrics(ctx context.Context) (*queue.Metrics, error)

Metrics returns broker-side metrics. Reads counts from the table.

func (*Queue) Nack

func (q *Queue) Nack(ctx context.Context, receipt string, delaySeconds *uint32) error

Nack restores visibility after delay; counted as a failure.

func (*Queue) Poll

func (q *Queue) Poll(ctx context.Context, maxMessages uint32) ([]common.QueuedMessage, error)

Poll claims up to maxMessages eligible messages from this queue.

Eligibility = visible_at <= now AND the message is the earliest ELIGIBLE message in its group (COALESCE(message_group_id, id)). Each claimed row gets a unique receipt handle (<pollUUID>:<id>) and its visibility window is pushed out by the configured timeout. We use a correlated NOT EXISTS rather than a windowed CTE because FOR UPDATE SKIP LOCKED cannot be applied over a ROW_NUMBER() result under Postgres CTE inlining; the resulting claim set is equivalent.

Two NOT EXISTS clauses gate "earliest eligible in its group", not one:

  • an earlier row that is CLAIMED (receipt_handle IS NOT NULL) does NOT block — an in-flight head's cross-poll ordering is still not enforced by the broker, same as always;
  • an earlier row that was NACKED WITH A DELAY (R4, owner ruling 2026-09-17, docs/spec/router-deferral-handback.md: receipt_handle IS NULL AND visible_at > now) DOES block. Before R4 this state was treated the same as "claimed" — not visible, so not blocking — which let a returned group head's own successors overtake it on the very next poll, exactly the ordering violation an ordered group exists to prevent. A delay-bearing MediationDeferred hand-back (R1) sits in precisely this state, which is what made the gap load-bearing rather than academic.

func (*Queue) Publish

func (q *Queue) Publish(ctx context.Context, m common.Message) (string, error)

Publish writes a single message. Uses ON CONFLICT DO NOTHING so a duplicate id is a no-op (at-least-once publish semantics).

func (*Queue) PublishBatch

func (q *Queue) PublishBatch(ctx context.Context, msgs []common.Message) ([]string, error)

PublishBatch writes a batch of messages, all bound for this queue, in one statement.

func (*Queue) Stop

func (q *Queue) Stop()

Stop closes the connection pool.

type Row

type Row struct {
	QueueName string
	Message   common.Message
}

Row is one message bound for one named queue — the unit InsertBatch writes, so a single statement can span several destination queues. The dispatch scheduler publishes a claimed batch that way: one row-queue per (tenant, priority), all in this one table.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL