jobs

package
v1.2.2 Latest Latest
Warning

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

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

Documentation

Overview

Package jobs is the single River composition root for e2a's background work. All durable background jobs — outbound sends, webhook delivery, HITL hold resolution, sender-identity provisioning, janitors — run on ONE shared river.Client against ONE river_job table, instead of a bespoke Postgres queue per subsystem. Each domain contributes its workers as a Registrar; queues are central (queues.go); business code enqueues through the Enqueuer interface so it stays testable and River lives at the edges.

This replaces the hand-rolled claim/lease/retry/sweep machinery: River's SKIP LOCKED claim, per-job MaxAttempts + retry policy, and built-in reaper do the plumbing, so domains only write job args + a Work() handler.

Index

Constants

View Source
const (
	// QueueOutbound carries outbound send jobs (API → SES).
	QueueOutbound = "outbound"
	// QueueInbound carries inbound message-processing jobs (accepted raw MIME →
	// parse/screen/persist/deliver). Isolated so an inbound spike (incl. the
	// per-message Gemini screen) can't starve outbound sends or webhook delivery.
	QueueInbound = "inbound"
	// QueueWebhook carries customer webhook-delivery jobs. Isolated from outbound
	// so a slow/failing endpoint's backlog never delays sends.
	QueueWebhook = "webhook"
	// QueueMaintenance carries low-urgency periodic/janitor work (reapers,
	// hold-TTL resolution, auto-disable sweeps).
	QueueMaintenance = "maintenance"
	// QueueNotify carries HITL approval-notification emails (the owner's review
	// alert when an outbound message enters pending_review). Small, isolated pool
	// so a burst of held sends notifying — or a stuck notification to a bad owner
	// address retrying — never competes with customer outbound delivery.
	QueueNotify = "notify"
	// QueueDefault is River's built-in default — anything not explicitly routed.
	QueueDefault = river.QueueDefault
)

Named queues. Jobs are assigned a queue at enqueue time (InsertOpts.Queue); the shared client works all of these. Separate queues give INDEPENDENT concurrency per lane, so a backlog in one (e.g. a slow customer webhook endpoint) can never starve another (e.g. outbound sends) — the isolation the hand-rolled queues couldn't express cleanly. Keep these names stable: they are persisted on river_job rows.

View Source
const DefaultReconcileBatch = 1000

DefaultReconcileBatch bounds one reconcile scan. In steady state the stranded set is ~empty; under a systemic enqueue failure it caps how many rows one pass re-drives (one tx each) so an unhealthy River isn't amplified by fanning the whole backlog every tick — the remainder is picked up on the next pass.

Variables

This section is empty.

Functions

func Migrate

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

Migrate applies River's own schema (river_job et al.) to the pool. River tracks its migrations in its own river_migration table, separate from e2a's schema_migrations, so this is idempotent and safe alongside identity migrations. Call once at startup, after the e2a migrations. (Generalized from the per-manager Migrate senderidentity used to own.)

func ReconcilePending

func ReconcilePending(ctx context.Context, pool *pgxpool.Pool, spec ReconcileSpec,
	enqueueTx func(ctx context.Context, tx pgx.Tx, id string) (int64, error)) (int, error)

ReconcilePending re-drives every row matching spec.Where whose spec.JobColumn IS NULL: it scans up to spec.Batch ids, then per id opens a tx, re-checks the job column under FOR UPDATE (skipping rows another process or a prior pass already enqueued), calls enqueueTx to insert the River job in that tx, and stamps the returned job id back onto the row — all atomically. Idempotent: the FOR UPDATE + IS NULL guard means a re-run (or a concurrent replica) never double-enqueues. A per-row failure is logged and skipped (the next pass retries it); the returned count is the rows enqueued.

This is the shared body behind every domain's startup cutover + live reconcile worker (outboundsend, inboundprocess, hitlnotify, webhookdelivery, webhookpub fan-out). Each domain supplies a ReconcileSpec + its own EnqueueXTx.

Types

type Client

type Client struct {
	*river.Client[pgx.Tx]
}

Client is the shared River client plus e2a's lifecycle helpers. It embeds *river.Client[pgx.Tx], so it IS an Enqueuer and exposes Start/Stop directly.

func New

func New(pool *pgxpool.Pool, cfg Config, registrars ...Registrar) (*Client, error)

New builds the one shared client from every domain's Registrar. Each registrar adds its workers and periodic jobs; queues are the central set (queues.go). The DB must already have River's schema (call Migrate first).

type Config

type Config struct {
	OutboundWorkers    int // QueueOutbound concurrency (default 8)
	InboundWorkers     int // QueueInbound concurrency (default 8)
	WebhookWorkers     int // QueueWebhook concurrency (default 16)
	MaintenanceWorkers int // QueueMaintenance concurrency (default 2)
	NotifyWorkers      int // QueueNotify concurrency (default 4)
	DefaultWorkers     int // QueueDefault concurrency (default 5)
}

Config sizes the per-queue worker pools. Zero values get sane defaults.

type Enqueuer

type Enqueuer interface {
	Insert(ctx context.Context, args river.JobArgs, opts *river.InsertOpts) (*rivertype.JobInsertResult, error)
	InsertTx(ctx context.Context, tx pgx.Tx, args river.JobArgs, opts *river.InsertOpts) (*rivertype.JobInsertResult, error)
}

Enqueuer is the narrow insert surface business code depends on, rather than the whole river.Client — keeps the store/agent layer testable with a fake and River swappable. Insert enqueues immediately; InsertTx enqueues WITHIN the caller's transaction (the outbox pattern: the job commits atomically with the business write and can never be lost). *river.Client[pgx.Tx] satisfies this directly.

type QueueStatsArgs added in v1.2.2

type QueueStatsArgs struct{}

QueueStatsArgs drives one gauge sampling pass.

func (QueueStatsArgs) Kind added in v1.2.2

func (QueueStatsArgs) Kind() string

type QueueStatsJobs added in v1.2.2

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

QueueStatsJobs contributes the periodic queue-gauge sampler to the shared client. Implements Registrar (same shape as sendramp.MaintenanceJobs).

func NewQueueStatsJobs added in v1.2.2

func NewQueueStatsJobs(pool *pgxpool.Pool, metrics QueueStatsMetrics) *QueueStatsJobs

NewQueueStatsJobs builds the sampler registrar. nil metrics degrades to a no-op sampler rather than a nil-panic.

func (*QueueStatsJobs) RegisterJobs added in v1.2.2

func (q *QueueStatsJobs) RegisterJobs(w *river.Workers) []*river.PeriodicJob

type QueueStatsMetrics added in v1.2.2

type QueueStatsMetrics interface {
	SetQueueDepth(queue, state string, n int)
	SetQueueOldestAge(queue string, seconds float64)
}

QueueStatsMetrics is the narrow gauge surface the sampler sets. Satisfied by telemetry.Metrics; injectable so tests assert with a fake.

type QueueStatsWorker added in v1.2.2

type QueueStatsWorker struct {
	river.WorkerDefaults[QueueStatsArgs]
	// contains filtered or unexported fields
}

QueueStatsWorker samples river_job depth and oldest-runnable-age gauges per queue. Read-only and bounded (two single-round-trip aggregates over the indexed state column), so River's default JobTimeout is ample.

func NewQueueStatsWorker added in v1.2.2

func NewQueueStatsWorker(pool *pgxpool.Pool, metrics QueueStatsMetrics) *QueueStatsWorker

NewQueueStatsWorker builds the sampler. nil metrics degrades to a no-op.

func (*QueueStatsWorker) Sample added in v1.2.2

func (w *QueueStatsWorker) Sample(ctx context.Context) error

Sample runs one gauge pass: depth per (queue, state) and the oldest RUNNABLE (available, scheduled_at due) job age per queue. Every known queue × state pair is set on every pass — pairs absent from the query zero-fill, so gauges drop to 0 when a queue drains. Rows for queues outside the known set are ignored: they cannot be zero-filled once they empty, so reporting them would leave stuck gauges.

func (*QueueStatsWorker) Work added in v1.2.2

type ReconcileSpec

type ReconcileSpec struct {
	// Table is the row table (e.g. "messages", "webhook_events").
	Table string
	// JobColumn is the nullable bigint column holding the enqueued River job id
	// (e.g. "send_job_id", "fanout_job_id"). A row is "stranded" when it matches
	// Where AND JobColumn IS NULL.
	JobColumn string
	// Where is the domain predicate identifying rows that SHOULD carry a job
	// (e.g. "direction='outbound' AND delivery_status='accepted'"). ANDed with
	// "<JobColumn> IS NULL"; pass no leading/trailing "AND".
	Where string
	// LogPrefix tags per-row enqueue-failure logs (e.g. "[outbound-reconcile]").
	LogPrefix string
	// Batch caps rows scanned per pass; 0 uses DefaultReconcileBatch.
	Batch int
}

ReconcileSpec describes one table's stranded-row reconcile.

SECURITY: Table, JobColumn, and Where are COMPILE-TIME CONSTANTS supplied by the calling package — never runtime or user input. They are interpolated directly into SQL (there is no parameter form for identifiers), so callers must pass only literal strings. The only runtime value (the row id) is always a bound parameter.

type Registrar

type Registrar interface {
	RegisterJobs(w *river.Workers) []*river.PeriodicJob
}

Registrar is what each domain implements to contribute its jobs to the shared client. RegisterJobs adds the domain's workers to the shared bundle (via river.AddWorker) and returns any periodic jobs it wants scheduled. Called once, at client construction — River requires all workers registered before Start.

Jump to

Keyboard shortcuts

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