Files
scadalink-design/docs/plans/2026-05-18-whiletrue-trigger-mode-design.md

135 lines
6.2 KiB
Markdown

# 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).