docs(plans): design for WhileTrue conditional/expression trigger mode
This commit is contained in:
134
docs/plans/2026-05-18-whiletrue-trigger-mode-design.md
Normal file
134
docs/plans/2026-05-18-whiletrue-trigger-mode-design.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# WhileTrue Trigger Mode for Conditional & Expression Script Triggers — Design
|
||||
|
||||
**Date:** 2026-05-18
|
||||
**Status:** Approved (brainstorming) — implementation to follow
|
||||
|
||||
## Context
|
||||
|
||||
Template script triggers of type `Conditional` and `Expression` currently fire
|
||||
*once* when their condition first holds:
|
||||
|
||||
- **Expression** is edge-triggered — the script runs once per `false→true`
|
||||
transition of the boolean expression.
|
||||
- **Conditional** runs the script on each change of the monitored attribute for
|
||||
which the `{operator, threshold}` comparison is true.
|
||||
|
||||
There is no way to keep a script running *while* a condition remains true. This
|
||||
design adds a second mode, **WhileTrue**, alongside the existing behavior
|
||||
(referred to as **OnTrue**).
|
||||
|
||||
### Decisions taken during brainstorming
|
||||
|
||||
- **WhileTrue is a timer re-fire cadence.** On the `false→true` edge the script
|
||||
fires immediately; while the condition stays true it re-fires on a periodic
|
||||
timer; on the `true→false` edge the timer stops. It re-fires even if no
|
||||
attribute updates arrive (e.g. a static condition).
|
||||
- **The re-fire interval is the script's existing `MinTimeBetweenRuns`** — no
|
||||
new configuration field. One value serves as both the WhileTrue cadence and
|
||||
the general per-script throttle.
|
||||
- **OnTrue is unchanged.** A trigger config with no `mode` field parses as
|
||||
`OnTrue`, so every existing deployed template behaves identically.
|
||||
- **Alarms are out of scope** — alarm triggers are already level-based ("active
|
||||
while true").
|
||||
|
||||
## Design
|
||||
|
||||
### 1. Trigger model & storage
|
||||
|
||||
`Conditional` and `Expression` trigger `TriggerConfiguration` JSON gains an
|
||||
optional `mode` field:
|
||||
|
||||
| Trigger | JSON shape |
|
||||
|-------------|---------------------------------------------------------|
|
||||
| Conditional | `{ attributeName, operator, threshold, mode? }` |
|
||||
| Expression | `{ expression, mode? }` |
|
||||
|
||||
`mode` is `"OnTrue"` (default; absent ⇒ `OnTrue`) or `"WhileTrue"`. Entity
|
||||
types are unchanged — `TemplateScript.TriggerConfiguration` stays `string?`.
|
||||
|
||||
### 2. Runtime evaluation (`ScriptActor`)
|
||||
|
||||
The internal trigger-config records gain a `Mode`:
|
||||
|
||||
```csharp
|
||||
internal enum TriggerMode { OnTrue, WhileTrue }
|
||||
internal record ConditionalTriggerConfig(
|
||||
string AttributeName, string Operator, double Threshold, TriggerMode Mode) : ScriptTriggerConfig;
|
||||
internal record ExpressionTriggerConfig(string Expression, TriggerMode Mode) : ScriptTriggerConfig;
|
||||
```
|
||||
|
||||
**OnTrue** — current behavior, unchanged:
|
||||
- Conditional: fire on each monitored-attribute change for which the comparison
|
||||
is true.
|
||||
- Expression: fire once per `false→true` transition.
|
||||
|
||||
**WhileTrue** — the actor tracks the condition's current truth state:
|
||||
- **`false→true` edge:** fire once immediately (via `TrySpawnExecution`, which
|
||||
still respects `MinTimeBetweenRuns` against any prior run), then start a
|
||||
periodic Akka timer with both initial delay and interval set to
|
||||
`MinTimeBetweenRuns`.
|
||||
- **Each timer tick:** spawn an execution **directly** — the timer interval is
|
||||
itself the cadence, so ticks bypass the `MinTimeBetweenRuns` skip-check (which
|
||||
could otherwise drop a tick to sub-millisecond timing jitter).
|
||||
- **`true→false` edge:** cancel the timer.
|
||||
- The state transitions naturally re-arm: a later `true` after a `false` starts
|
||||
a fresh cycle.
|
||||
|
||||
Truth-state sources:
|
||||
- Conditional depends only on the monitored attribute, so it is re-evaluated on
|
||||
each change of that attribute (unchanged gate).
|
||||
- Expression is re-evaluated on every attribute change (unchanged).
|
||||
|
||||
A throwing/non-bool expression is treated as `false` (existing behavior); for
|
||||
WhileTrue that cleanly stops the timer.
|
||||
|
||||
**Edge case — WhileTrue with no `MinTimeBetweenRuns`:** a periodic timer has no
|
||||
interval to use, so the trigger degrades to a single edge fire and logs a
|
||||
warning. Deployment-time validation of this combination is a deliberate
|
||||
non-goal of this change.
|
||||
|
||||
New surface in `ScriptActor`: a `TriggerMode` on the two config records, one
|
||||
`WhileTrueTick` internal message, and start/stop timer helpers (the actor
|
||||
already implements `IWithTimers` for Interval triggers). Timer key
|
||||
`"whiletrue-trigger"`.
|
||||
|
||||
### 3. Editors & codec
|
||||
|
||||
- **`ScriptTriggerConfigCodec`:** `ScriptTriggerModel` gains a `Mode`
|
||||
(`ScriptTriggerMode` enum, default `OnTrue`). `Parse` reads `mode` for the
|
||||
Conditional and Expression kinds; `Serialize` writes it for those kinds.
|
||||
Absent/unrecognized `mode` ⇒ `OnTrue`.
|
||||
- **`ScriptTriggerEditor.razor`:** the Conditional and Expression panels gain a
|
||||
mode `<select>` — "When the condition becomes true — once" (`OnTrue`) vs
|
||||
"Repeatedly while true" (`WhileTrue`) — with a hint that WhileTrue re-fires at
|
||||
the script's *Min time between runs* interval. `BuildHint()` is updated to
|
||||
describe the selected mode.
|
||||
|
||||
### 4. Error handling
|
||||
|
||||
- Expression throws / returns non-bool → treated as `false` (logged as a script
|
||||
error, as today); for WhileTrue this stops the re-fire timer.
|
||||
- Malformed trigger JSON → trigger parses as inert (existing behavior).
|
||||
- WhileTrue with no `MinTimeBetweenRuns` → single fire + warning (see above).
|
||||
|
||||
### 5. Testing & verification
|
||||
|
||||
- **Runtime (`ScriptActorTests`)** — a script body that calls
|
||||
`Instance.SetAttribute(...)` is observable as a `SetStaticAttributeCommand` on
|
||||
the instance-actor `TestProbe`; with a short `MinTimeBetweenRuns`:
|
||||
- WhileTrue conditional & expression: fire on the `false→true` edge, re-fire
|
||||
on the timer, stop on `true→false`, re-arm on a later `true`.
|
||||
- OnTrue regression: conditional & expression fire exactly as before.
|
||||
- WhileTrue with no `MinTimeBetweenRuns`: a single fire, no repeats.
|
||||
- **Codec (`ScriptTriggerConfigCodecTests`, new)** — `mode` round-trips for
|
||||
Conditional and Expression; absent `mode` ⇒ `OnTrue`; `WhileTrue` serializes.
|
||||
|
||||
## Affected files
|
||||
|
||||
- `src/ScadaLink.SiteRuntime/Actors/ScriptActor.cs`
|
||||
- `src/ScadaLink.CentralUI/Components/Shared/ScriptTriggerConfigCodec.cs`
|
||||
- `src/ScadaLink.CentralUI/Components/Shared/ScriptTriggerEditor.razor`
|
||||
- `tests/ScadaLink.SiteRuntime.Tests/Actors/ScriptActorTests.cs`
|
||||
- `tests/ScadaLink.CentralUI.Tests/Shared/ScriptTriggerConfigCodecTests.cs` (new)
|
||||
- Docs: `docs/requirements/Component-SiteRuntime.md`,
|
||||
`docs/requirements/Component-TemplateEngine.md` (note the new mode).
|
||||
Reference in New Issue
Block a user