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

webhookdelivery

package
v1.1.0 Latest Latest
Warning

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

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

Documentation

Overview

Package webhookdelivery is Layer 3 of the webhook pipeline (docs/design/webhook-delivery-river-migration.md): the River execution stage that POSTs a webhook_subscriber_deliveries (Layer 2) row to the customer endpoint and retries it. It replaces the hand-rolled SKIP-LOCKED claim + retry worker (internal/webhook/subscriber_retry.go) with a River Worker on the shared `webhook` queue; River owns claim/lease/retry/backoff, and Layer 2 becomes pure delivery state written here for the history API.

Delivery is at-least-once (the industry standard — Stripe/Svix/Postmark): River re-drives a crashed job, so an endpoint may receive a duplicate if the POST succeeds but the worker crashes before writing `delivered`. Consumers dedup on the event id in the signed payload.

Index

Constants

View Source
const MaxDeliveryAttempts = 8

MaxDeliveryAttempts is River's MaxAttempts for a delivery job — after this many failed attempts River discards and the worker marks Layer 2 'failed'.

Variables

This section is empty.

Functions

This section is empty.

Types

type DeliverWorker

type DeliverWorker struct {
	river.WorkerDefaults[WebhookDeliverArgs]
	// contains filtered or unexported fields
}

DeliverWorker delivers one Layer 2 row. Mirrors the legacy processOne: re-fetch the webhook per attempt (disabled → snooze, deleted → cancel), POST, write Layer 2 state. River owns retry/backoff via NextRetry below.

func NewDeliverWorker

func NewDeliverWorker(subStore *webhook.SubscriberStore, deliverer Deliverer, webhooks WebhookReader) *DeliverWorker

NewDeliverWorker constructs the worker. Used by the Registrar and by tests.

func (*DeliverWorker) NextRetry

func (w *DeliverWorker) NextRetry(job *river.Job[WebhookDeliverArgs]) time.Time

NextRetry overrides River's client-wide policy for webhook jobs only, returning the exact 29h21m envelope. job.Attempt is the just-failed attempt (1-based); River discards at MaxAttempts so this is called for attempts 1..MaxDeliveryAttempts-1.

func (*DeliverWorker) Work

Work intentionally has no Timeout() override — a single HTTP POST (bounded by the deliverer's own client timeout) fits River's 60s default JobTimeout.

type Deliverer

type Deliverer interface {
	Deliver(ctx context.Context, url string, body []byte, secret, secretPrev, eventType, schemaVersion string) webhook.DeliveryOutcome
}

Deliverer is the POST surface. *webhook.SubscriberDeliverer satisfies it.

type Jobs

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

Jobs is the webhook-delivery integration on the shared River client: a jobs.Registrar (contributes DeliverWorker + the reconcile periodic) plus the transactional enqueue entry point the outbox drain + redelivery API call. The shared client is injected via SetEnqueuer after jobs.New builds it (two-phase wiring, same as senderidentity).

func NewJobs

func NewJobs(subStore *webhook.SubscriberStore, deliverer Deliverer, webhooks WebhookReader, pool *pgxpool.Pool) *Jobs

NewJobs builds the integration with its dependencies (no client yet). pool backs the periodic reconciler's scan.

func (*Jobs) EnqueueDelivery

func (j *Jobs) EnqueueDelivery(ctx context.Context, pool *pgxpool.Pool, deliveryID string) error

EnqueueDelivery enqueues a River delivery job for an ALREADY-INSERTED pending Layer 2 row, in its own transaction, and stamps job_id. This is for the direct- insert API surfaces that bypass the outbox drain — the /test webhook endpoint and the redelivery API — which create a subscriber_deliveries row targeting a single webhook. Without this, those rows have no River job and (post SubscriberRetryWorker deletion) would never deliver. Idempotent per row via the job_id IS NULL guard under a row lock.

func (*Jobs) EnqueueDeliveryTx

func (j *Jobs) EnqueueDeliveryTx(ctx context.Context, tx pgx.Tx, deliveryID string) (int64, error)

EnqueueDeliveryTx enqueues a delivery job WITHIN the caller's transaction — the outbox pattern: the Layer 2 row insert and this job commit together, so a delivery record can never exist without a job (or vice versa). The caller only calls this when the Layer 2 insert actually inserted a row (dedup ON CONFLICT returned an id), so a deduped event enqueues nothing. Returns the river_job id for the caller to stamp on the Layer 2 row's job_id.

func (*Jobs) ReconcilePending

func (j *Jobs) ReconcilePending(ctx context.Context, pool *pgxpool.Pool) (int, error)

ReconcilePending enqueues a River delivery job for every pending Layer 2 row that has no job yet (job_id IS NULL). It runs BOTH at startup (the one-shot cutover from the legacy queue) AND on a live schedule (ReconcileWorker) so a stranded row — from the separate-tx /test/redelivery enqueue paths or an outbox-drain crash window — is re-driven within reconcileInterval rather than only on the next restart. Idempotent: the per-row FOR UPDATE + job_id IS NULL guard means a re-run (or a concurrent replica) never double-enqueues. Returns the number of rows enqueued.

func (*Jobs) RegisterJobs

func (j *Jobs) RegisterJobs(w *river.Workers) []*river.PeriodicJob

RegisterJobs adds the DeliverWorker + the reconcile worker, and returns the reconcile periodic. Implements jobs.Registrar.

func (*Jobs) SetEnqueuer

func (j *Jobs) SetEnqueuer(e jobs.Enqueuer)

SetEnqueuer injects the shared client so EnqueueDeliveryTx can insert jobs.

type MaintenanceJobs

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

MaintenanceJobs is the jobs.Registrar for the webhook janitor: it contributes the MaintenanceWorker and a periodic that fires it on QueueMaintenance. No enqueuer needed — the schedule is the only trigger.

func NewMaintenanceJobs

func NewMaintenanceJobs(sweeper Sweeper) *MaintenanceJobs

NewMaintenanceJobs builds the registrar around a Sweeper (the AutoDisableWorker).

func (*MaintenanceJobs) RegisterJobs

func (m *MaintenanceJobs) RegisterJobs(w *river.Workers) []*river.PeriodicJob

RegisterJobs adds the maintenance worker + its periodic schedule. Mirrors senderidentity's reaper: routed to QueueMaintenance, no UniqueOpts (River's periodic scheduler already inserts at most one per interval and a completed run must not dedup-block the next), RunOnStart:false (first sweep after one interval).

type MaintenanceWorker

type MaintenanceWorker struct {
	river.WorkerDefaults[WebhookMaintenanceArgs]
	// contains filtered or unexported fields
}

MaintenanceWorker runs the janitor passes once per scheduled job. Errors are swallowed inside Tick (logged there); Work returns nil so a transient DB blip never spins River's retry machinery for a best-effort idempotent sweep — the next interval picks it up.

func NewMaintenanceWorker

func NewMaintenanceWorker(sweeper Sweeper) *MaintenanceWorker

NewMaintenanceWorker builds the worker around a Sweeper. Exported so tests can drive Work directly (RegisterJobs builds an identical one for the client).

func (*MaintenanceWorker) Work

type ReconcileWorker

type ReconcileWorker struct {
	river.WorkerDefaults[WebhookReconcileArgs]
	// contains filtered or unexported fields
}

ReconcileWorker re-enqueues any pending delivery row with no River job. It is the LIVE backstop for the separate-tx enqueue paths (/test, redelivery) and any outbox-drain crash window — turning "recovered only on restart" into "recovered within reconcileInterval". Idempotent (ReconcilePending's job_id IS NULL guard).

func (*ReconcileWorker) Work

type Sweeper

type Sweeper interface {
	Tick(ctx context.Context)
}

Sweeper runs the webhook maintenance passes (auto-disable chronically-failing webhooks + clear expired signing_secret_prev rows). Satisfied by *webhook.AutoDisableWorker — the River worker just drives its Tick on a schedule instead of a hand-rolled time.Ticker.

type WebhookDeliverArgs

type WebhookDeliverArgs struct {
	DeliveryID string `json:"delivery_id"`
}

WebhookDeliverArgs carries only the Layer 2 delivery id — the worker reads the payload + webhook from the DB (keeps river_job rows tiny; Layer 2 is source of truth).

func (WebhookDeliverArgs) Kind

func (WebhookDeliverArgs) Kind() string

type WebhookMaintenanceArgs

type WebhookMaintenanceArgs struct{}

WebhookMaintenanceArgs drives the periodic webhook janitor. No fields — the worker sweeps the whole table each run.

func (WebhookMaintenanceArgs) Kind

type WebhookReader

type WebhookReader interface {
	GetWebhookByIDInternal(ctx context.Context, webhookID string) (*identity.Webhook, error)
}

WebhookReader is the narrow webhook-lookup surface the worker needs. *identity.Store satisfies it.

type WebhookReconcileArgs

type WebhookReconcileArgs struct{}

WebhookReconcileArgs drives the periodic stranded-delivery reconciler.

func (WebhookReconcileArgs) Kind

Jump to

Keyboard shortcuts

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