03686eb5d7
- ShadowScriptRunner (Core.Scripting): IScriptShadow.Mirror copies the inputs and queues the shadow run on the
pool; compares value (structural), CLR type, status and failure class (compile / budget / threw+message /
unsupported_type); runner-fault shadow outcomes (Unavailable, worker busy) are not compared. Bounded:
Scripting:Shadow:MaxInFlight (256) and one per script; drops counted (behind | key_busy).
- Metrics otopcua.script.shadow.{compared,mismatch{script},dropped,inflight}; one rate-limited Warning per
script and reason with a diff summary (Scripting:Shadow:MismatchLogsPerMinute, re-armed at the seam's deploy).
- The VirtualTag, Calculation and scripted-alarm seams recognise the shadow and keep their in-process path
byte-for-byte (no coalescer, one-at-a-time pump, in-process engine), mirroring after each compiled run.
- ScriptRunnerSelection builds the shadow over an OutOfProcessScriptRunner whose lines carry
ScriptRunnerRole=Shadow; Status() is null under Shadow so readiness never sees the shadow pool; the
script-runner health entry stays Healthy and reports the shadow counters as data.
- docker-dev/docker-compose.script-runner-shadow.yml; docs/VirtualTags.md "Shadow mode".
- Tests: comparison table, per-script metric + once-per-reason log, rate limit, hung/blocking/throwing shadow
never reaches the caller, MaxInFlight drop accounting, input copy; real-worker divergent + unsupported-type
scripts counted against exactly those scripts, a worker-only runaway killed/quarantined there with every
published value intact, alarm transitions identical to in process across a kill -9 of the shadow worker.
Mutation-proved (16 mutants, 15 red; the one survivor added an assertion).
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
28 lines
1.6 KiB
YAML
28 lines
1.6 KiB
YAML
# docker-dev/docker-compose.script-runner-shadow.yml — optional overlay: run the central pair in SHADOW mode
|
|
# (lmxopcua#780 P2, Scripting:Runner=Shadow). Every compiled VirtualTag / Calculation / scripted-alarm evaluation runs
|
|
# IN PROCESS — that is what is published — and is mirrored to the ScriptWorker pool, whose result is only compared.
|
|
# Shadow doubles script CPU and keeps the in-process risk: a time-boxed diagnostic, never a production mode.
|
|
#
|
|
# Up (the image must be built from a branch that has the shadow runner):
|
|
# docker compose -f docker-dev/docker-compose.yml -f docker-dev/docker-compose.script-runner-shadow.yml up -d --no-deps central-1 central-2
|
|
# Back to InProcess: re-run `up -d --no-deps central-1 central-2` with the base file alone.
|
|
#
|
|
# Watch it: `/metrics` on :9000 carries otopcua_script_shadow_compared_total{kind,result},
|
|
# otopcua_script_shadow_mismatch_total{kind,reason,script}, otopcua_script_shadow_dropped_total{kind,reason} and
|
|
# otopcua_script_shadow_inflight; each script's first mismatch per reason logs one Warning "Script shadow mismatch: …"
|
|
# with a diff summary; `/health/ready`'s `script-runner` entry stays Healthy and carries the shadow counters as data.
|
|
x-script-runner-shadow-env: &script-runner-shadow-env
|
|
Scripting__Runner: "Shadow"
|
|
Scripting__Runner__Seams__0: "VirtualTag"
|
|
Scripting__Runner__Seams__1: "Calculation"
|
|
Scripting__Runner__Seams__2: "ScriptedAlarm"
|
|
Scripting__Worker__PeCacheDir: "/tmp/otopcua-pe-cache"
|
|
|
|
services:
|
|
central-1:
|
|
environment:
|
|
<<: *script-runner-shadow-env
|
|
central-2:
|
|
environment:
|
|
<<: *script-runner-shadow-env
|