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
- func UpstreamErrorName(reason string) string
- func UpstreamErrorNames() []string
- type Action
- type DataEvent
- type Decision
- type Delivery
- type DeviceConnectedEvent
- type DeviceDeletionFinishedEvent
- type DeviceDeletionStartedEvent
- type DeviceDisconnectedEvent
- type DeviceErrorEvent
- type DeviceEvent
- type DeviceRegisteredEvent
- type Executor
- type ExecutorConfig
- type FieldErrors
- type Forwarder
- type IncomingDataEvent
- type IncomingIntrospectionEvent
- type InterfaceAddedEvent
- type InterfaceRemovedEvent
- type PathCreatedEvent
- type PathRemovedEvent
- type Policy
- type SimpleEvent
- type Strategy
- type Trigger
- type TriggerErrors
- type ValueChangeAppliedEvent
- type ValueChangeEvent
- type ValueStoredEvent
Constants ¶
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.
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).
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.
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
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
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 ¶
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 ¶
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
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
CompilePolicy parses and validates one stored policy definition.
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.
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 ¶
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
AttachPolicy sets the resolved delivery policy on the trigger.
func (*Trigger) MatchesData ¶
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
Policy returns the resolved delivery policy, nil when none is attached. Safe on a nil receiver.
func (*Trigger) TracksChanges ¶ added in v0.2.0
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.