scheduled

package
v0.2.44 Latest Latest
Warning

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

Go to latest
Published: Oct 6, 2026 License: MPL-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package scheduled is the durable backing store for the chassis `scheduled` inlet and the txco://schedule op: a table of future events ("run this payload, not before schedule_at") that the scheduled personality polls, claims, and fires.

Storage is dialect-aware (registry.Dialect, the same seam the auth registry uses); the bundled backend is a SQLite file. The CLAIM is the coordination — a node flips a due row pending→claimed with one conditional UPDATE and treats rows-affected==1 as "I won", so a single claimer fires each event even when several pollers share one table.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewEventID added in v0.2.30

func NewEventID() string

NewEventID mints a scheduled_events row id (`sched_<hxid>`), for a caller that needs the id before EnqueueTx.

func Register

func Register(name string, c Constructor)

Register adds a backend constructor. Called from a backend package's init().

Types

type Claimed

type Claimed struct {
	ID             string
	Tenant         string
	IdempotencyKey string
	Payload        json.RawMessage
}

Claimed is a row this node won at ClaimDue: the bits the firing path needs to build the `_scheduled/0` envelope.

type Config

type Config struct {
	// DBPath is the bundled SQLite backend's file path (--scheduled-db-path).
	DBPath string
}

Config carries backend-selecting options resolved from chassis config. The bundled "sqlite" backend uses DBPath. Adding a field here doesn't affect existing backends (same posture as cron.Config / vector.Config).

type Constructor

type Constructor func(Config) (*Store, error)

Constructor builds a Store from resolved config. It is expected to open its backing DB and call Store.EnsureSchema before returning.

type Store

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

Store is the thin façade over the scheduled_events table. It carries the dialect (for `?`→`$n` rebinding + Postgres-only tweaks) and a clock seam for tests, mirroring secrets.Store.

func NewStore

func NewStore(db *sql.DB, d registry.Dialect) *Store

NewStore builds a Store over the opened scheduled DB and its dialect. A nil dialect defaults to SQLite (the in-tree default).

func Open

func Open(name string, cfg Config) (*Store, error)

Open constructs the named backend. Unknown name is a startup error listing what is available (so a misconfigured --scheduled-store fails loudly).

func (*Store) Cancel

func (s *Store) Cancel(ctx context.Context, tenant, idempotencyKey string) (bool, error)

Cancel deletes a still-PENDING event for (tenant, idempotency_key). Returns true if a row was removed; false (no error) if it was already fired or never existed. A claimed/done/failed event is immutable and is left untouched.

func (*Store) ClaimDue

func (s *Store) ClaimDue(ctx context.Context, node string, limit int) ([]Claimed, error)

ClaimDue claims up to limit due pending rows (schedule_at <= now) in ONE statement and returns the rows this call won. The subselect picks the due set, the UPDATE flips it, RETURNING hands back the claimed rows.

The previous shape — SELECT candidates, then one conditional UPDATE per row — was correct but paid one network round trip per candidate on a shared Postgres store (~5s at 200 due rows, all before the first event fired), and competing pollers each burned the full round-trip count racing for the same rows. Now: on Postgres the subselect takes FOR UPDATE SKIP LOCKED, so concurrent pollers partition the due set without waiting; on SQLite the clause is empty and the single statement is atomic under the single writer. The redundant status guard on the outer UPDATE is a belt for any plan shape — a row can never be claimed twice.

func (*Store) Close

func (s *Store) Close() error

Close releases the underlying DB handle.

func (*Store) Enqueue

func (s *Store) Enqueue(ctx context.Context, tenant, idempotencyKey string, at time.Time, payload json.RawMessage) (string, error)

Enqueue inserts a pending event, or — if a PENDING row already exists for (tenant, idempotency_key) — reschedules it (new schedule_at + payload). A row that has already left 'pending' (claimed/done/failed) is immutable: the ON CONFLICT update no-ops via the status guard, so a spent key can't be resurrected. Returns the row id (the proposed id on a no-op conflict).

func (*Store) EnqueueTx added in v0.2.30

func (s *Store) EnqueueTx(ctx context.Context, tx *sql.Tx, id, tenant, idempotencyKey string, at time.Time, payload json.RawMessage) error

EnqueueTx is Enqueue inside the caller's transaction: the row is written on tx and lands (or not) with the caller's commit. id is the row id the caller minted (NewEventID). Same validation and ON CONFLICT semantics as Enqueue.

func (*Store) EnsureSchema

func (s *Store) EnsureSchema(ctx context.Context) error

EnsureSchema creates the scheduled_events table + due index if absent. The DDL is portable (TEXT timestamps RFC3339, JSON as TEXT, native partial index); a backend calls it once at construction.

func (*Store) MarkDone

func (s *Store) MarkDone(ctx context.Context, id string) error

MarkDone moves a claimed row to the terminal 'done' state.

func (*Store) MarkFailed

func (s *Store) MarkFailed(ctx context.Context, id string) error

MarkFailed moves a claimed row to the terminal 'failed' state. Used when a dispatched event's pipeline errored or its response timed out — an at-most-once bias (it likely ran; don't risk a double-fire by retrying). A true crash never reaches here: the row stays 'claimed' for ReclaimStale.

func (*Store) Purge

func (s *Store) Purge(ctx context.Context, retention time.Duration) (int64, error)

Purge deletes terminal (done/failed) rows older than retention so the table doesn't grow without bound. Returns the deleted count.

func (*Store) ReclaimStale

func (s *Store) ReclaimStale(ctx context.Context, staleAfter time.Duration) (int64, error)

ReclaimStale resets rows stuck in 'claimed' past staleAfter back to 'pending' so another node retries — crash recovery for a node that died after claiming but before MarkDone/MarkFailed. Returns the reset count. (RFC3339 UTC timestamps compare lexicographically === chronologically.)

Jump to

Keyboard shortcuts

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