triggers

package
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package triggers dispatches the changes of a database to the functions collections declare (design/triggers-and-realtime.md §9): one dispatcher per deployment holds a lease, reads the change stream from a durable position, evaluates each trigger's condition and queues one async job per match. It is driver-neutral: MongoDB lives behind State, Queue and a changefeed.Source.

Index

Constants

View Source
const (
	DefaultLeaseTTL       = 15 * time.Second
	DefaultFlushEvery     = 250 * time.Millisecond
	DefaultWatermarkEvery = time.Second
	DefaultBatchMax       = 200
)

Defaults of a Dispatcher.

View Source
const (
	// GapWindow: the dispatcher was away longer than the change history reaches.
	GapWindow = "window"
	// GapPreImage: a change arrived without the image its condition needs.
	GapPreImage = "preimage_missing"
	// GapBacklog: the realm had more queued trigger jobs than its ceiling allows.
	GapBacklog = "backlog"
)

Gap reasons.

View Source
const (
	DropChain = "chain"
	DropBurst = "burst"
)

Reasons an event that matched is not queued.

View Source
const BacklogCacheTTL = 5 * time.Second

BacklogCacheTTL is how long a realm's count of queued trigger jobs is reused.

View Source
const MaxGapKeys = 1000

MaxGapKeys is how many document ids one gap record lists.

View Source
const MaxInputBytes = 256 << 10

MaxInputBytes caps the input of a trigger job. A job keeps its input for as long as the realm keeps jobs, and a larger one would make every retry and every listing carry it: over the cap the document bodies are left out and `truncated` says so, the function reads the documents itself.

View Source
const RescanConfirmOver = 10000

RescanConfirmOver is how many documents a rescan may read before it asks to be confirmed.

Variables

This section is empty.

Functions

func EventID

func EventID(c changefeed.Change) string

EventID identifies one change of one document, the same wherever and however often it is read. It does not use the resume token (token bytes are not a stable identity) and does not rely on (id, version) alone: a document deleted and created again with the same id repeats its first version.

func JobID

func JobID(eventID, trigger string) string

JobID is the job a trigger makes of an event. One change can match several triggers, so the event's id alone would collapse their jobs; the trigger's name is part of it. The id has the shape of any job id, and is the unique key that makes a replay a no-op.

func WatchSet

func WatchSet(reg *registry.Registry) changefeed.WatchSet

WatchSet is what the dispatcher reads: the collections of every realm that declare triggers.

Types

type Admin

type Admin interface {
	Status(ctx context.Context) (DispatcherStatus, error)
	// Gaps lists the gaps of a realm (those of every realm included: a window gap covers all),
	// newest first; resolved ones only when all is true.
	Gaps(ctx context.Context, realm string, all bool, limit int) ([]GapRecord, error)
	// ResolveGap closes an open gap of the realm; ok is false when there is none by that id.
	ResolveGap(ctx context.Context, realm, id string, at time.Time) (ok bool, err error)
}

Admin reads and closes what the dispatcher recorded; MongoDB's TriggerState implements it.

type Dispatcher

type Dispatcher struct {
	// Source opens the change streams; State and Queue are the durable parts.
	Source changefeed.Source
	State  State
	Queue  Queue
	// Holder names this instance in the lease; it must be unique per process.
	Holder string
	// Config returns a realm's app configuration, for conditions that use appConfig; may be
	// nil. reg is the configuration the dispatcher is running.
	Config func(ctx context.Context, reg *registry.Registry, realm string) map[string]any
	// Limits returns a realm's trigger limits (the instance's, lowered by the realm's); nil
	// is registry.DefaultTriggerLimits for every realm.
	Limits   func(reg *registry.Registry, realm string) registry.TriggerLimits
	Observer Observer
	Log      *slog.Logger
	Now      func() time.Time

	LeaseTTL       time.Duration
	FlushEvery     time.Duration
	WatermarkEvery time.Duration
	BatchMax       int
	// Backoff is the wait between attempts to queue a batch that failed; nil is 100ms
	// doubling to 5s.
	Backoff func(attempt int) time.Duration
	// contains filtered or unexported fields
}

Dispatcher turns changes into jobs (design §9). Run it on every instance that may dispatch (BACKD_TRIGGERS=true): they compete for one lease and only the holder reads.

func (*Dispatcher) Run

func (d *Dispatcher) Run(ctx context.Context) error

Run competes for the lease and dispatches while it holds it, until ctx is done.

func (*Dispatcher) SetRegistry

func (d *Dispatcher) SetRegistry(reg *registry.Registry)

SetRegistry gives the dispatcher the configuration in force. Called again on a reload, it applies to the next change onward: nothing is replayed.

type DispatcherStatus

type DispatcherStatus struct {
	// Found is false before any instance has ever dispatched.
	Found bool
	// Held is whether some instance holds the lease at the moment.
	Held         bool
	Epoch        int64
	Holder       string
	LeaseExpires time.Time
	// WatermarkAt is when the position was last stored: its age is what to watch.
	WatermarkAt time.Time
}

DispatcherStatus is what the deployment's dispatcher state says right now.

type DocSource

type DocSource interface {
	// Count says how many documents were updated in [since, until).
	Count(ctx context.Context, mongoDatabase, collection string, since, until time.Time) (int64, error)
	// Updated calls fn for each document updated in [since, until), oldest first, in API shape.
	Updated(ctx context.Context, mongoDatabase, collection string, since, until time.Time, fn func(doc map[string]any) error) error
}

DocSource reads the documents of a collection by when they were last written.

type ErrRescanNeedsConfirm

type ErrRescanNeedsConfirm struct{ Documents int64 }

ErrRescanNeedsConfirm means the range holds more documents than a rescan reads unconfirmed.

func (*ErrRescanNeedsConfirm) Error

func (e *ErrRescanNeedsConfirm) Error() string

type Gap

type Gap struct {
	Realm      string
	Collection string // "" for a window gap: it covers every collection
	Trigger    string // "" when it is not about one trigger
	Reason     string
	From, To   time.Time
	Count      int
	Keys       []string // document ids, at most MaxGapKeys
}

Gap is a stretch of changes the dispatcher could not turn into jobs.

type GapRecord

type GapRecord struct {
	Gap
	ID         string
	RecordedAt time.Time
	// ResolvedAt is zero while the gap is open.
	ResolvedAt time.Time
}

GapRecord is a stored gap.

type Job

type Job struct {
	// ID is derived from the change and the trigger, so a replay of the change produces the
	// same job and the queue collapses it.
	ID        string
	Realm     string
	Database  string // the function's database
	Function  string // the function's name
	Input     json.RawMessage
	Origin    string // trigger:<database>/<collection>/<trigger>
	Timeout   time.Duration
	RequestID string
	EventID   string
	// ChainRoot and ChainDepth place the run in a chain of writes caused by trigger runs: its
	// own writes are stamped with them.
	ChainRoot  string
	ChainDepth int
	// SerialKey and SerialOrder make an `ordered` trigger's jobs for one document run one at a
	// time, in the order the changes were made (the cluster time of the change).
	SerialKey   string
	SerialOrder int64
}

Job is one queued run of a trigger's function.

type Lease

type Lease struct {
	Holder string
	Epoch  int64
}

Lease is the right to dispatch. Epoch grows with every acquisition, so a holder that lost the lease (a pause, a partition) is fenced: its writes fail on the epoch.

type Observer

type Observer interface {
	// Event counts a change a trigger considered, by outcome: matched, no_match, missing_preimage or error.
	Event(realm, collection, trigger, outcome string)
	// Enqueued counts the jobs queued and the replays the queue collapsed.
	Enqueued(realm string, created, duplicates int)
	// Lag is the time from a change to the jobs it made being queued.
	Lag(d time.Duration)
	// Watermark records when the position was last stored.
	Watermark(at time.Time)
	// Lease records whether this instance holds the lease, and its epoch.
	Lease(held bool, epoch int64)
	// Gap counts a gap recorded.
	Gap(realm, reason string)
	// Dropped counts an event that matched and was not queued, by reason: chain (too deep) or
	// burst (a chained loop above its rate).
	Dropped(realm, collection, trigger, reason string)
	// Backlog records a realm's queued trigger jobs.
	Backlog(realm string, n int)
}

Observer receives the dispatcher's metrics. internal/metrics implements it.

type Queue

type Queue interface {
	// Enqueue inserts the jobs of one realm. A job whose ID exists already is a replay and
	// is not an error: created counts the new ones.
	Enqueue(ctx context.Context, realm string, jobs []Job) (created int, err error)
	// Backlog counts the realm's trigger jobs that are queued or running: what the
	// dispatcher keeps below the realm's ceiling.
	Backlog(ctx context.Context, realm string) (int, error)
}

Queue stores jobs for workers to claim.

type RescanRequest

type RescanRequest struct {
	Realm, Database, Collection string
	// Trigger limits it to one trigger; "" is every one that can be rescanned.
	Trigger      string
	Since, Until time.Time
	// Confirm allows a scan over RescanConfirmOver documents.
	Confirm bool
	// Run identifies this rescan in the job ids, so its jobs don't collapse with the originals.
	Run string
}

RescanRequest says what to re-emit.

type RescanResult

type RescanResult struct {
	Scanned    int
	Queued     int
	Duplicates int
	// NotRescannable names the triggers left out and why.
	NotRescannable map[string]string
}

RescanResult is what a rescan did.

func Rescan

func Rescan(ctx context.Context, reg *registry.Registry, req RescanRequest, src DocSource, q Queue, config func(context.Context, string) map[string]any) (RescanResult, error)

Rescan re-emits a collection's changes as synthetic `updated` events, from `_meta.updated_at`, for what the stream could not deliver (a gap). It is not a replay of history: a document that changed twice in the range is read once, as it is now; deletions can't be found; and a trigger whose condition needs `before` or `changed()` can't be evaluated from a document alone, so it is listed as not rescannable.

type State

type State interface {
	// AcquireLease takes the lease when nobody holds it or the holder's has expired, with
	// the database's clock. ok is false when someone else holds it.
	AcquireLease(ctx context.Context, holder string, ttl time.Duration) (l Lease, ok bool, err error)
	// RenewLease extends it; ok is false when it is no longer this lease.
	RenewLease(ctx context.Context, l Lease, ttl time.Duration) (ok bool, err error)
	// ReleaseLease gives it up so another instance may take it at once.
	ReleaseLease(ctx context.Context, l Lease) error
	// Watermark returns the stored position and when it was stored; found is false before
	// the first.
	Watermark(ctx context.Context) (pos changefeed.Position, at time.Time, found bool, err error)
	// SetWatermark stores the position, only while l is still the lease; ok is false when
	// it is not (the caller must stop).
	SetWatermark(ctx context.Context, l Lease, pos changefeed.Position, at time.Time) (ok bool, err error)
	// RecordGap stores a gap for an operator to see.
	RecordGap(ctx context.Context, g Gap) error
}

State is the dispatcher's durable state: the lease, the position it has reached and the gaps it recorded. The position lives with the lease so that only the current holder can move it.

Jump to

Keyboard shortcuts

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