fix(scripting): review minors: configured limit in summaries, dispose gate/CTS, validator, docs (fix round 1)
- ScriptLogRateLimiter.LimitPerWindow. The ScriptLogTopicSink and VirtualTagActor summaries now print the configured limit, not DefaultLimitPerWindow. Tests assert "limit N/window" for a non-default N. - CalculationDriver.Dispose cancels, then takes _evalGate (bounded 2 s) and disposes the gate and _evalCts. The pump and timer ticks enter the gate through TryEnterEvalGateAsync, which stops cleanly once the driver is disposed. Test: Dispose with a 6 s runaway run in flight returns promptly, disposes both, and a late trigger is harmless. - ScriptedAlarmEngine captures _shutdownToken once and disposes _shutdownCts in Dispose when every drained task has completed. A task stuck past the grace leaves the CTS to the GC. Lifecycle tests assert the disposal. - DraftValidator: CalculationNoEffectiveTrigger is skipped for a change-triggered tag whose scriptId does not resolve. That case is already CalculationScriptMissing or CalculationScriptNotFound, and there are no reads to count. Both existing tests now assert the absence (red before the change). - docs/VirtualTags.md: System.Threading is denied as a whole namespace, with an allow-list; the type list is now accurate (Unsafe, RuntimeHelpers, and the ForbiddenMembers on System.Type); the resource limits section covers the run guard and the allocation budget. docs/ScriptEditor.md: accurate deny-list and a new "Runtime safety" section covering timeout, refusal, quarantine after 3 timeouts, allocation budget, lifting, and rate-limit summaries that appear only on the source's next entry after a burst then quiet. Refs #734 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
+40
-2
@@ -162,11 +162,49 @@ user's fault.
|
||||
`ForbiddenTypeAnalyzer.Analyze(compilation)` walks the wrapped syntax tree and
|
||||
resolves every referenced symbol against the sandbox deny-list (namespace
|
||||
prefixes `System.IO`, `System.Net`, `System.Diagnostics`, `System.Reflection`,
|
||||
`System.Threading.Tasks`, etc., plus type-granular denials like
|
||||
`System.Environment` and `System.Threading.Thread`). Each violation is mapped
|
||||
`System.Threading` — the whole namespace, with only `CancellationToken`,
|
||||
`Interlocked` and `Volatile` allowed back in — `System.Threading.Tasks`,
|
||||
`System.Runtime.InteropServices`, `System.Runtime.Loader`, `Microsoft.Win32`;
|
||||
type-granular denials `System.Environment`, `System.AppDomain`, `System.GC`,
|
||||
`System.Activator`, `System.Runtime.CompilerServices.Unsafe` /
|
||||
`RuntimeHelpers`; and member-granular denials on `System.Type`'s by-name
|
||||
reflection entry points — `GetType(string)`, `GetMethod(s)`, `InvokeMember`,
|
||||
and the `GetProperty`/`GetField`/`GetMember`/`GetConstructor`/`GetEvent`
|
||||
families). The runtime publish gate uses the same analyzer, so a deny-list
|
||||
change needs a `DiagnoseTests` case to keep editor ⇔ publish parity. Each violation is mapped
|
||||
via `tree.GetMappedLineSpan(span)` to user-source coordinates and emitted as an
|
||||
error marker with code `OTSCRIPT_FORBIDDEN`.
|
||||
|
||||
### Runtime safety (what happens after publish)
|
||||
|
||||
The editor cannot catch a script that compiles cleanly but never returns or
|
||||
allocates without bound, so every runtime seam (virtual tags, Calculation tags,
|
||||
scripted-alarm predicates) runs scripts through the shared `ScriptRunGuard`:
|
||||
|
||||
- **Timeout + one orphan.** A run that exceeds its budget is abandoned (it cannot
|
||||
be pre-empted in-process) and the tag/alarm reads Bad. While that abandoned run
|
||||
is still executing, every further trigger is **refused** — Bad
|
||||
`"previous evaluation still running"`, counted on `otopcua.script.run_refused` —
|
||||
so a `while(true)` costs one pool thread, not one per trigger.
|
||||
- **Quarantine after 3 consecutive timeouts** (`Scripting:QuarantineAfterConsecutiveTimeouts`).
|
||||
Only a timeout is a strike: a refusal neither adds to nor resets the streak, and
|
||||
a run that returns (value or throw) resets it. A quarantined script is never
|
||||
dispatched again; it reads Bad `"script quarantined: …"`, logs one Error and
|
||||
increments `otopcua_script_quarantined`. An infinite loop therefore usually shows
|
||||
as *refused* (one timeout, then refusals for as long as the orphan spins) rather
|
||||
than *quarantined*.
|
||||
- **Allocation budget.** A single run allocating more than
|
||||
`Scripting:MaxAllocBytesPerRun` (64 MB) is quarantined. This is sampled after the
|
||||
run returns, so a run that OOMs the process first is not stopped — that needs an
|
||||
out-of-process runner.
|
||||
- **Lifting.** A quarantine lifts when the script source changes or on redeploy,
|
||||
never by itself.
|
||||
- **Script-log rate limit.** The cluster-wide script-log fan-out admits 20 entries
|
||||
per source per minute; the rest are counted and replaced by one "N suppressed"
|
||||
summary. The summary is emitted on that source's **next** entry after the window
|
||||
rolls, so a source that bursts and then goes quiet shows its summary only when
|
||||
it logs again (the full stream is always in `scripts-*.log`).
|
||||
|
||||
### 3. DependencyExtractor
|
||||
|
||||
`DependencyExtractor.Extract(code).Rejections` enumerates every `ctx.GetTag` /
|
||||
|
||||
Reference in New Issue
Block a user