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
- func EventID(c changefeed.Change) string
- func JobID(eventID, trigger string) string
- func WatchSet(reg *registry.Registry) changefeed.WatchSet
- type Admin
- type Dispatcher
- type DispatcherStatus
- type DocSource
- type ErrRescanNeedsConfirm
- type Gap
- type GapRecord
- type Job
- type Lease
- type Observer
- type Queue
- type RescanRequest
- type RescanResult
- type State
Constants ¶
const ( DefaultLeaseTTL = 15 * time.Second DefaultFlushEvery = 250 * time.Millisecond DefaultWatermarkEvery = time.Second DefaultBatchMax = 200 )
Defaults of a Dispatcher.
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.
const ( DropChain = "chain" DropBurst = "burst" )
Reasons an event that matched is not queued.
const BacklogCacheTTL = 5 * time.Second
BacklogCacheTTL is how long a realm's count of queued trigger jobs is reused.
const MaxGapKeys = 1000
MaxGapKeys is how many document ids one gap record lists.
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.
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.
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 ¶
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.