triggers

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package triggers compiles stored Astarte trigger definitions into fast matchers, renders the upstream-parity SimpleEvent JSON payloads, and executes HTTP webhook actions with retry (docs/ROADMAP.md §7.2 files 6.10–6.12, docs/DESIGN.md §1.1).

The accepted definition shape is upstream Realm Management's trigger JSON (astarte_core SimpleTriggerConfig, v1.2): a "simple_triggers" array of data_trigger / device_trigger conditions plus one "action". Conditions that are valid upstream but outside Astrate's v1 evaluation scope — device_empty_cache_received, interface_minor_updated, and group-scoped triggers — compile successfully (so installs round-trip) but never match; they are reported in Trigger.Unsupported so callers can log them.

The change-derived data conditions (value_change, value_change_applied, path_created, path_removed, value_stored) follow upstream data_handler.ex semantics; the engine captures the previous-value snapshot pre-write and emits the events post-commit (internal/engine trackPrevious/fireData).

Index

Constants

View Source
const (
	// DefaultWorkers is the default delivery worker count.
	DefaultWorkers = 4
	// DefaultQueueSize is the default delivery queue capacity.
	DefaultQueueSize = 256
	// DefaultMaxAttempts bounds delivery attempts per event.
	DefaultMaxAttempts = 5
	// DefaultBackoffStart is the first retry delay.
	DefaultBackoffStart = 250 * time.Millisecond
	// DefaultBackoffCap bounds the exponential retry delay.
	DefaultBackoffCap = 5 * time.Second
	// DefaultRequestTimeout bounds one webhook request.
	DefaultRequestTimeout = 10 * time.Second
)

Executor defaults.

View Source
const (
	// OnIncomingData fires on every accepted device data publish.
	OnIncomingData = "incoming_data"
	// OnValueChange fires when a value differs from the previous one
	// (upstream execute_pre_change_triggers; Astrate evaluates it
	// post-commit against the accept-time previous-value snapshot).
	OnValueChange = "value_change"
	// OnValueChangeApplied is value_change after persistence (upstream
	// execute_post_change_triggers).
	OnValueChangeApplied = "value_change_applied"
	// OnPathCreated fires on the first value of a path (previous missing,
	// new value present — upstream Core.Trigger post-change semantics).
	OnPathCreated = "path_created"
	// OnPathRemoved fires when an existing property is unset (previous
	// present, new value absent).
	OnPathRemoved = "path_removed"
	// OnValueStored fires after an accepted individual-datastream insert.
	OnValueStored = "value_stored"
	// OnDeviceRegistered fires when a device is registered via the Pairing API.
	OnDeviceRegistered = "device_registered"
	// OnDeviceConnected fires when a device session is established.
	OnDeviceConnected = "device_connected"
	// OnDeviceDisconnected fires when a device connection ends.
	OnDeviceDisconnected = "device_disconnected"
	// OnDeviceDeletionStarted fires immediately before a device is deleted
	// (upstream: start of async deletion; Astrate: before the sync delete).
	OnDeviceDeletionStarted = "device_deletion_started"
	// OnDeviceDeletionFinished fires immediately after a device is deleted
	// (upstream: end of async deletion; Astrate: after the sync delete).
	OnDeviceDeletionFinished = "device_deletion_finished"
	// OnDeviceEmptyCacheReceived fires on control/emptyCache (accepted, not
	// evaluated in v1: upstream defines no SimpleEvent variant for it).
	OnDeviceEmptyCacheReceived = "device_empty_cache_received"
	// OnDeviceError fires when the validation pipeline rejects a message.
	OnDeviceError = "device_error"
	// OnIncomingIntrospection fires on every introspection publish.
	OnIncomingIntrospection = "incoming_introspection"
	// OnInterfaceAdded fires when an introspection declares a new
	// name:major pair.
	OnInterfaceAdded = "interface_added"
	// OnInterfaceRemoved fires when an introspection drops a name:major pair.
	OnInterfaceRemoved = "interface_removed"
	// OnInterfaceMinorUpdated fires on a minor bump (accepted, not evaluated
	// in v1).
	OnInterfaceMinorUpdated = "interface_minor_updated"
)

Trigger condition names (upstream SimpleTriggerConfig "on" values).

View Source
const EventTimeLayout = "2006-01-02T15:04:05.000Z"

EventTimeLayout renders envelope timestamps: UTC, millisecond precision — what upstream's DateTime.from_unix(ms) |> Jason.encode produces. Exported because consumers outside the executor (the Channels socket) render the same instant into the same upstream shape.

View Source
const StatusTransport = 0

StatusTransport is the pseudo-status passed to Decide when the request never produced a response at all.

Variables

This section is empty.

Functions

func UpstreamErrorName added in v0.2.0

func UpstreamErrorName(reason string) string

UpstreamErrorName maps one of Astrate's reject-reason labels to the upstream error_name enum value. If the input is already an upstream enum value it is returned unchanged. Any other input maps to interface_loading_failed.

func UpstreamErrorNames added in v0.2.0

func UpstreamErrorNames() []string

UpstreamErrorNames returns a copy of the closed set, so a caller cannot mutate the package's own slice.

Types

type Action

type Action struct {
	// Method is the upper-cased HTTP method.
	Method string
	// URL is the webhook endpoint.
	URL string
	// StaticHeaders are the action's extra request headers.
	StaticHeaders map[string]string
	// IgnoreSSLErrors disables server-certificate verification for this
	// action's requests (upstream "ignore_ssl_errors").
	IgnoreSSLErrors bool
	// Custom is the raw action object of a non-HTTP action, delivered
	// through the Forwarder extension point; nil for HTTP actions.
	Custom json.RawMessage
	// Template is the Mustache body template (upstream "template"), set
	// only when TemplateType == "mustache".
	Template string
	// TemplateType is the upstream "template_type" field. Only "mustache"
	// is rendered; any other non-empty value is accepted but ignored (the
	// default JSON envelope is sent), same as before this field existed.
	TemplateType string
}

Action is a trigger's parsed delivery action (docs/ROADMAP.md §7.2 file 6.12): an HTTP webhook (upstream "http_url"+"http_method", or the legacy "http_post_url"), or a custom action routed to the Forwarder extension point. Upstream AMQP-shaped actions ("amqp_exchange") are rejected at parse time (#64); docs/DESIGN.md §1.1 records the original seam.

type DataEvent

type DataEvent struct {
	// DeviceID is the encoded publishing device ID.
	DeviceID string
	// On is the condition name being evaluated.
	On string
	// Interface and Major identify the interface the publish validated
	// against.
	Interface string
	Major     int
	// Path is the concrete data path.
	Path string
	// Value is the decoded payload value (payload.Value closed set; nil for
	// unset).
	Value any
}

DataEvent is the match input for an accepted data publish: the typed decoded value rides along for value conditions (nil for property unset). On selects which condition set evaluates: one of the On* data constants (incoming_data, value_change, value_change_applied, path_created, path_removed, value_stored) — a matcher only reacts to its own condition.

type Decision added in v0.2.0

type Decision struct {
	Strategy Strategy
	Reason   string
}

Decision is what a policy prescribes, plus why — the reason is logged by the executor so an operator can see which rule governed a delivery.

type Delivery

type Delivery struct {
	// Realm is the tenant (rides in the Astarte-Realm header).
	Realm string
	// Trigger is the matched trigger.
	Trigger *Trigger
	// Event is the rendered envelope.
	Event SimpleEvent
	// contains filtered or unexported fields
}

Delivery is one matched (trigger, event) pair queued for execution.

type DeviceConnectedEvent

type DeviceConnectedEvent struct {
	// Type is always "device_connected".
	Type string `json:"type"`
	// DeviceIPAddress is the peer address of the new connection.
	DeviceIPAddress string `json:"device_ip_address"`
}

DeviceConnectedEvent is the device_connected event body.

func NewDeviceConnectedEvent

func NewDeviceConnectedEvent(ip string) DeviceConnectedEvent

NewDeviceConnectedEvent builds a device_connected event body.

type DeviceDeletionFinishedEvent added in v0.2.0

type DeviceDeletionFinishedEvent struct {
	// Type is always "device_deletion_finished".
	Type string `json:"type"`
}

DeviceDeletionFinishedEvent is the device_deletion_finished event body.

func NewDeviceDeletionFinishedEvent added in v0.2.0

func NewDeviceDeletionFinishedEvent() DeviceDeletionFinishedEvent

NewDeviceDeletionFinishedEvent builds a device_deletion_finished event body.

type DeviceDeletionStartedEvent added in v0.2.0

type DeviceDeletionStartedEvent struct {
	// Type is always "device_deletion_started".
	Type string `json:"type"`
}

DeviceDeletionStartedEvent is the device_deletion_started event body.

func NewDeviceDeletionStartedEvent added in v0.2.0

func NewDeviceDeletionStartedEvent() DeviceDeletionStartedEvent

NewDeviceDeletionStartedEvent builds a device_deletion_started event body.

type DeviceDisconnectedEvent

type DeviceDisconnectedEvent struct {
	// Type is always "device_disconnected".
	Type string `json:"type"`
}

DeviceDisconnectedEvent is the device_disconnected event body.

func NewDeviceDisconnectedEvent

func NewDeviceDisconnectedEvent() DeviceDisconnectedEvent

NewDeviceDisconnectedEvent builds a device_disconnected event body.

type DeviceErrorEvent

type DeviceErrorEvent struct {
	// Type is always "device_error".
	Type string `json:"type"`
	// ErrorName is the rejection reason (Astrate's §2.6 reject-reason
	// labels feed it).
	ErrorName string `json:"error_name"`
	// Metadata carries free-form diagnostic strings.
	Metadata map[string]string `json:"metadata"`
}

DeviceErrorEvent is the device_error event body.

func NewDeviceErrorEvent

func NewDeviceErrorEvent(errorName string, metadata map[string]string) DeviceErrorEvent

NewDeviceErrorEvent builds a device_error event body from one of Astrate's own reject-reason labels (§2.6). The reason is translated to the upstream error_name enum by UpstreamErrorName, because consumers validate error_name against that closed set and drop any event carrying an unknown name — the Dashboard's Device Live Events card raises "Unrecognised event received". The untranslated reason is preserved under metadata["astrate_reason"] whenever translation changed it, so no diagnostic detail is lost. A nil metadata map renders as {} (upstream always emits the map).

type DeviceEvent

type DeviceEvent struct {
	// DeviceID is the encoded device ID.
	DeviceID string
	// On is the condition name (OnDeviceConnected, ...).
	On string
	// Interface and Major carry the interface filter input for
	// interface_added / interface_removed events; empty otherwise.
	Interface string
	Major     int
}

DeviceEvent is the match input for a device-scoped event.

type DeviceRegisteredEvent added in v0.2.0

type DeviceRegisteredEvent struct {
	// Type is always "device_registered".
	Type string `json:"type"`
}

DeviceRegisteredEvent is the device_registered event body.

func NewDeviceRegisteredEvent added in v0.2.0

func NewDeviceRegisteredEvent() DeviceRegisteredEvent

NewDeviceRegisteredEvent builds a device_registered event body.

type Executor

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

Executor runs trigger actions asynchronously: a bounded queue feeding worker goroutines, HTTP webhooks with exponential-backoff retry, and delivery-outcome metrics (docs/ROADMAP.md §7.2 file 6.12).

func NewExecutor

func NewExecutor(cfg ExecutorConfig) *Executor

NewExecutor builds and starts an executor.

func (*Executor) Close

func (x *Executor) Close(ctx context.Context) error

Close stops accepting deliveries, lets the workers drain the queue, and waits for them bounded by ctx. In-flight retry sleeps abort immediately.

func (*Executor) Enqueue

func (x *Executor) Enqueue(d Delivery)

Enqueue queues one delivery without blocking; a full queue drops it with a metric and a log line.

The read lock spans the closing check and the send: without it a caller that passed the check could still be holding an unsent delivery when Close closes the channel underneath it, and the send would panic.

type ExecutorConfig

type ExecutorConfig struct {
	// Workers is the number of delivery goroutines.
	Workers int
	// QueueSize is the bounded delivery queue capacity; a full queue drops
	// the event with a metric (triggers must never backpressure ingestion,
	// docs/DESIGN.md §1.4).
	QueueSize int
	// MaxAttempts bounds attempts per delivery (1 = no retries).
	MaxAttempts int
	// BackoffStart and BackoffCap shape the exponential retry delay.
	BackoffStart time.Duration
	BackoffCap   time.Duration
	// RequestTimeout bounds each webhook request.
	RequestTimeout time.Duration
	// Forwarder handles non-HTTP actions; nil logs and skips them.
	Forwarder Forwarder
	// Registerer receives the executor's collectors; nil leaves them
	// unregistered (they still work, which tests rely on).
	Registerer prometheus.Registerer
	// Logger receives delivery logs (default slog.Default()).
	Logger *slog.Logger
}

ExecutorConfig carries the executor's knobs; zero values select the defaults above.

type FieldErrors added in v0.2.0

type FieldErrors struct {
	Part   string // e.g. "action"
	Fields map[string][]string
}

FieldErrors carries an upstream-shaped field→messages map for one part of a trigger definition. errors.Is/As-friendly so the realm layer can render the nested changeset envelope without string parsing.

func (*FieldErrors) Error added in v0.2.0

func (e *FieldErrors) Error() string

Error renders a stable human form ("action: field=message, ..." with fields sorted) so logs and plain-error consumers stay deterministic.

type Forwarder

type Forwarder interface {
	// Forward delivers one rendered event for a custom action.
	Forward(ctx context.Context, realm, trigger string, action json.RawMessage, event []byte) error
}

Forwarder is the extension point for non-HTTP trigger actions (docs/DESIGN.md §1.1: "AMQP action replaced by optional NATS/HTTP forwarding"). The executor hands it every matched event whose action is not an HTTP webhook. internal/engine/forward provides the HTTP and NATS (build-tag "nats") implementations, wired from [triggers.forward] by internal/config and cmd/astrate. The default (nil) logs the event and counts it as skipped, which is the designed behaviour, not a gap in this code path.

type IncomingDataEvent

type IncomingDataEvent struct {
	// Type is always "incoming_data".
	Type string `json:"type"`
	// Interface is the interface name.
	Interface string `json:"interface"`
	// Path is the concrete data path.
	Path string `json:"path"`
	// Value is the published value (null for property unset).
	Value any `json:"value"`
}

IncomingDataEvent is the incoming_data event body. Value must already be JSON-friendly (the engine renders payload values through its canonical jsonb encoding before constructing events); nil renders as null (property unset, matching upstream's empty-bson-value handling).

func NewIncomingDataEvent

func NewIncomingDataEvent(iface, path string, value any) IncomingDataEvent

NewIncomingDataEvent builds an incoming_data event body.

type IncomingIntrospectionEvent

type IncomingIntrospectionEvent struct {
	// Type is always "incoming_introspection".
	Type string `json:"type"`
	// Introspection is the raw `;`-separated introspection payload.
	Introspection string `json:"introspection"`
}

IncomingIntrospectionEvent is the incoming_introspection event body, carrying the raw introspection string (upstream's string form).

func NewIncomingIntrospectionEvent

func NewIncomingIntrospectionEvent(introspection string) IncomingIntrospectionEvent

NewIncomingIntrospectionEvent builds an incoming_introspection event body.

type InterfaceAddedEvent

type InterfaceAddedEvent struct {
	// Type is always "interface_added".
	Type string `json:"type"`
	// Interface is the added interface name.
	Interface string `json:"interface"`
	// MajorVersion and MinorVersion are the declared version.
	MajorVersion int `json:"major_version"`
	MinorVersion int `json:"minor_version"`
}

InterfaceAddedEvent is the interface_added event body.

func NewInterfaceAddedEvent

func NewInterfaceAddedEvent(iface string, major, minor int) InterfaceAddedEvent

NewInterfaceAddedEvent builds an interface_added event body.

type InterfaceRemovedEvent

type InterfaceRemovedEvent struct {
	// Type is always "interface_removed".
	Type string `json:"type"`
	// Interface is the removed interface name.
	Interface string `json:"interface"`
	// MajorVersion is the removed major.
	MajorVersion int `json:"major_version"`
}

InterfaceRemovedEvent is the interface_removed event body.

func NewInterfaceRemovedEvent

func NewInterfaceRemovedEvent(iface string, major int) InterfaceRemovedEvent

NewInterfaceRemovedEvent builds an interface_removed event body.

type PathCreatedEvent added in v0.2.0

type PathCreatedEvent struct {
	// Type is always "path_created".
	Type string `json:"type"`
	// Interface is the interface name.
	Interface string `json:"interface"`
	// Path is the concrete data path.
	Path string `json:"path"`
	// Value is the first published value.
	Value any `json:"value"`
}

PathCreatedEvent is the path_created event body: first accepted value of a path (upstream SimpleEvents.PathCreatedEvent).

func NewPathCreatedEvent added in v0.2.0

func NewPathCreatedEvent(iface, path string, value any) PathCreatedEvent

NewPathCreatedEvent builds a path_created event body.

type PathRemovedEvent added in v0.2.0

type PathRemovedEvent struct {
	// Type is always "path_removed".
	Type string `json:"type"`
	// Interface is the interface name.
	Interface string `json:"interface"`
	// Path is the concrete data path.
	Path string `json:"path"`
}

PathRemovedEvent is the path_removed event body: an existing property was unset. It carries no value (upstream SimpleEvents.PathRemovedEvent).

func NewPathRemovedEvent added in v0.2.0

func NewPathRemovedEvent(iface, path string) PathRemovedEvent

NewPathRemovedEvent builds a path_removed event body.

type Policy added in v0.2.0

type Policy struct {
	Name            string
	MaximumCapacity int
	PrefetchCount   int
	RetryTimes      int           // 0 when no handler retries
	EventTTL        time.Duration // 0 when unset
	// contains filtered or unexported fields
}

Policy is a compiled trigger-delivery policy that decides whether to retry or discard a failed delivery attempt.

func CompilePolicy added in v0.2.0

func CompilePolicy(def []byte) (*Policy, error)

CompilePolicy parses and validates one stored policy definition.

func (*Policy) Decide added in v0.2.0

func (p *Policy) Decide(status int) Decision

Decide returns the decision for a finished attempt. status is the HTTP status code, or StatusTransport when the request never produced a response.

type SimpleEvent

type SimpleEvent struct {
	// Timestamp is the event instant.
	Timestamp time.Time
	// DeviceID is the encoded device ID.
	DeviceID string
	// TriggerName is the matched trigger's name (the envelope carries it).
	TriggerName string
	// Event is the typed event body: one of the *Event structs below.
	Event any
}

SimpleEvent is one trigger event envelope, ready for delivery.

func (SimpleEvent) MarshalJSON

func (s SimpleEvent) MarshalJSON() ([]byte, error)

MarshalJSON renders the upstream envelope shape.

type Strategy added in v0.2.0

type Strategy int

Strategy is what a policy prescribes for one failed delivery attempt.

const (
	// StrategyDiscard discards the event after a failed attempt.
	StrategyDiscard Strategy = iota
	// StrategyRetry re-delivers the event after a failed attempt.
	StrategyRetry
)

type Trigger

type Trigger struct {
	// Name is the trigger's installed name.
	Name string
	// Action is the parsed delivery action.
	Action *Action
	// Unsupported lists upstream-valid features this version accepts but
	// does not evaluate (logged by the engine's trigger cache).
	Unsupported []string
	// PolicyName is the delivery policy named by the trigger definition.
	// An empty value means the implicit @default behaviour.
	PolicyName string
	// contains filtered or unexported fields
}

Trigger is one compiled trigger: matchers plus the parsed action.

func Compile

func Compile(name string, def []byte) (*Trigger, error)

Compile parses and validates one stored trigger definition. Validation follows upstream SimpleTriggerConfig: M7's Realm Management reuses it at install time, so an error here maps to an upstream-shaped 422.

Field-scoped violations accumulate into a *TriggerErrors (action plus an index-aligned simple_triggers array, upstream changeset semantics); errors that have no field shape (does-not-parse, no simple_triggers, unknown trigger type) stay plain wrapped errors.

func CompileCondition added in v0.2.0

func CompileCondition(name string, raw json.RawMessage) (*Trigger, error)

CompileCondition compiles a single simple_triggers condition into a matcher-only Trigger. A transient trigger is compiled per subscription for live-event watching: it is never stored, never delivered, and its Action is deliberately nil.

func (*Trigger) AttachPolicy added in v0.2.0

func (t *Trigger) AttachPolicy(p *Policy)

AttachPolicy sets the resolved delivery policy on the trigger.

func (*Trigger) MatchesData

func (t *Trigger) MatchesData(ev DataEvent) bool

MatchesData reports whether any data_trigger condition matches the event.

func (*Trigger) MatchesDevice

func (t *Trigger) MatchesDevice(ev DeviceEvent) bool

MatchesDevice reports whether any device_trigger condition matches the event.

func (*Trigger) Policy added in v0.2.0

func (t *Trigger) Policy() *Policy

Policy returns the resolved delivery policy, nil when none is attached. Safe on a nil receiver.

func (*Trigger) TracksChanges added in v0.2.0

func (t *Trigger) TracksChanges(ev DataEvent) bool

TracksChanges reports whether any evaluated data condition of this trigger reacts to one of the previous-value-derived conditions (value_change*, path_created/removed) for this event's device, interface, and path — the gate that keeps the engine's pre-write previous-value lookup off messages nobody is watching (upstream get_value_change_triggers).

type TriggerErrors added in v0.2.0

type TriggerErrors struct {
	Action         map[string][]string
	SimpleTriggers []map[string][]string // nil entries = no errors at that index
}

TriggerErrors carries upstream-shaped field errors across the parts of a trigger definition; rendered as {"errors":{"action":{...}}} and/or {"errors":{"simple_triggers":[...]}} (index-aligned, {} for clean entries).

func (*TriggerErrors) Error added in v0.2.0

func (e *TriggerErrors) Error() string

Error renders a stable human form: one "action: ..." segment plus one "simple_triggers[i]: ..." segment per offending condition (fields sorted inside each), joined by "; ".

type ValueChangeAppliedEvent added in v0.2.0

type ValueChangeAppliedEvent struct {
	// Type is always "value_change_applied".
	Type string `json:"type"`
	// Interface is the interface name.
	Interface string `json:"interface"`
	// Path is the concrete data path.
	Path string `json:"path"`
	// OldValue is the previous value (null when the path had none).
	OldValue any `json:"old_value"`
	// NewValue is the persisted value.
	NewValue any `json:"new_value"`
}

ValueChangeAppliedEvent is the value_change_applied event body — value_change's post-persistence twin (upstream SimpleEvents.ValueChangeAppliedEvent). Astrate evaluates both after the commit, so the pair carries identical payloads.

func NewValueChangeAppliedEvent added in v0.2.0

func NewValueChangeAppliedEvent(iface, path string, oldValue, newValue any) ValueChangeAppliedEvent

NewValueChangeAppliedEvent builds a value_change_applied event body.

type ValueChangeEvent added in v0.2.0

type ValueChangeEvent struct {
	// Type is always "value_change".
	Type string `json:"type"`
	// Interface is the interface name.
	Interface string `json:"interface"`
	// Path is the concrete data path.
	Path string `json:"path"`
	// OldValue is the previous value (null when the path had none).
	OldValue any `json:"old_value"`
	// NewValue is the published value.
	NewValue any `json:"new_value"`
}

ValueChangeEvent is the value_change event body (upstream SimpleEvents.ValueChangeEvent): old and new value of one changed path. The engine emits it post-commit against the accept-time previous-value snapshot; a missing previous renders as null.

func NewValueChangeEvent added in v0.2.0

func NewValueChangeEvent(iface, path string, oldValue, newValue any) ValueChangeEvent

NewValueChangeEvent builds a value_change event body.

type ValueStoredEvent added in v0.2.0

type ValueStoredEvent struct {
	// Type is always "value_stored".
	Type string `json:"type"`
	// Interface is the interface name.
	Interface string `json:"interface"`
	// Path is the concrete data path.
	Path string `json:"path"`
	// Value is the stored value.
	Value any `json:"value"`
}

ValueStoredEvent is the value_stored event body: one accepted individual-datastream insert (upstream SimpleEvents.ValueStoredEvent).

func NewValueStoredEvent added in v0.2.0

func NewValueStoredEvent(iface, path string, value any) ValueStoredEvent

NewValueStoredEvent builds a value_stored event body.

Jump to

Keyboard shortcuts

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