Files
lmxopcua/scripts/validation/opcua-lifecycle-memory/SATURATION.md
T
Joseph Doherty 04d5513af2
v2-ci / unit-tests (pull_request) Has been cancelled
test(opcua): bind saturation artifacts to gateway 1.0 and module identities
2026-11-10 15:03:21 -05:00

7.0 KiB

Candidate saturation experiment — review before launch

This is a prospective diagnostic protocol, not an acceptance run or a replacement for the failed cold server soak. Production code, the cycle-3 resource baseline, and the +64 MiB managed / +128 MiB RSS / +32 descriptor caps remain unchanged. Only OTOPCUA_SDK2_SOAK_MODE=saturation selects this behavior; the existing diagnostic, profile, and full modes keep their existing stopping rules.

The fixed canonical phase performs exactly 384 lifecycle cycles, with the existing active-resource restart after every fourth cycle: 96 completed restarts. It then performs exactly one settling cycle, without another restart. The phase, including observer handshakes, must finish within 1,800 seconds. Initial fixture startup has a separate 120-second bound. There is no early completion, extension, RSS-selected endpoint, budget reset, or two-hour/full-pass flag. Cleanup remains bounded independently of the phase deadline.

All canonical operations remain in use: encrypted opc.tcp sessions, namespace URI binding, scalar/typed-array read/write, subscriptions, browse abandonment, history continuation churn/release/cross-session/close, surgical changes/rebuilds, key revocation, paused network reads/recovery, and same-PKI server restart. All ownership, continuation, PKI-descriptor, store and node inventory checks remain active. Cold budget rejection is latched at its original observation and rethrown, even if diagnostic readiness later succeeds. A rejected readiness receipt does not become a passing test or a new resource baseline.

Observer boundaries

A fresh task-owned control directory and UUID bind every request/receipt to this run and target PID. Files are published atomically; existing, malformed, foreign, or incomplete receipts fail closed. The observer is a separate diagnostic process using public DiagnosticsClient / EventPipe APIs. Product dependencies are unchanged. A separate short positive control first invokes a named non-SDK method with NoInlining/NoOptimization after attaching and requires its JIT-start count in the parsed receipt. This proves CLR provider delivery/classification independently of custom marker delivery. That probe is never invoked by the canonical experiment.

One continuous trace covers both fixed windows, including their boundary gap:

Marker Before/after operation Completed restarts
window1-start Before cycle 321 80
window1-end After cycle 352 and its restart 88
window2-start Before cycle 353 88
window2-end After cycle 384 and its restart 96

The target waits for the observer's active receipt and enabled marker provider before its first window operation. The parser requires exactly these four markers, the correct UUID and cycle/restart values, and strictly ordered timestamps. A continuous veto also covers events outside the marker ranges within the attached interval. Both windows and the entire attached interval require zero Opc.Ua.OpcUaExtensions JIT starts and method-load events, zero new Opc.Ua.* assembly-load events, and zero lost events. Other JIT counts and loaded native method-byte sums are reported for review; they do not establish that all runtime paths are warmed. Method-size sums do not equal resident memory.

Runtime capture is Verbose Microsoft-Windows-DotNETRuntime, keywords 0x18 (Loader + JIT), plus Informational OtOpcUa-Sdk2-Saturation markers, no rundown, and a prospectively fixed 16 MiB circular-buffer capacity. This is a new experiment and does not reinterpret the earlier 256 MiB-capacity traces.

After restart 96, the target requests stop and waits. The observer must stop its session, drain the event stream, close it, parse the entire trace, and atomically publish a PID/run-bound drained receipt. Its Stop/copy/dispose/parse stages have 10/10/5/10-second bounds. Target receipt waits are bounded at 45 seconds and remain inside the 1,800-second phase deadline. Observer faults propagate immediately. The target also verifies its marker provider is disabled before the settling cycle. Missing markers, no events, event loss, or malformed stream cannot prove readiness. A parser failure produces a failed observer receipt, not empty counts.

The saturation-result separates readiness, cold budget rejection, fixed operation completion and verified shutdown ownership. It always records acceptance=false. It is emitted before rethrowing the original cold assertion. The launcher retains nonzero test/container exits and records diagnostic readiness separately.

Prepare immutable Linux artifacts

Commit the reviewed source first. Preparation refuses a dirty checkout and publishes fresh, separate app/observer artifacts for linux-arm64, self-contained .NET 10.0.14, without trimming or single-file publishing. It never starts the experiment. The bundle records source revision, source hashes, exact publish command, fixed protocol, image digest and SHA-256 of every app/observer/tool file. It requires the landed HistorianGateway Client/Contracts 1.0.0 dependency graph and records assembly versions/module MVIDs by reading PE metadata without loading or executing product assemblies.

python3 scripts/validation/opcua-lifecycle-memory/prepare_saturation.py \
  --output /tmp/opcua-saturation-reviewed \
  --libraries "$diagnosticToolLibraries" \
  --image sha256:b04611ee9e1b7d36797012ed921962c57189cb4dbb3dff12afb8e158110f293b

diagnosticToolLibraries is the installed dotnet-trace 9.0.661903 tools/net8.0/any directory. No product package reference is added. Artifact preparation and review are prerequisites; root and Astra must review the actual code, control tests and receipt before launch. The coordinator owns the long execution.

python3 /tmp/opcua-saturation-reviewed/tools/launch_saturation.py \
  --bundle /tmp/opcua-saturation-reviewed --receipt-sha256 "$reviewedReceiptHash" \
  --output /tmp/opcua-saturation-run --verify-only
# After root/Astra review, root runs the same command without --verify-only.

The launcher rechecks every immutable file before and after the run. It creates a fresh offline task-owned container, writable output/PKI directories, and read-only app/observer/tool mounts. Descriptor controls run first in a separate process. TRX guards require two descriptor tests and one canonical test actually executed, with no skipped results. The launcher verifies the actual target and observer mapped their own self-contained libcoreclr.so; the older SDK-image controller runtime is not the target runtime. A 2,040-second outer timeout includes startup, phase and bounded cleanup; it does not extend the 1,800-second phase.

No long saturation run has been launched during implementation. Existing cold failures remain untouched. No original-SDK full-workload adapter, warmed acceptance baseline, additional two-hour server run, or production correction is part of this change. The separately demonstrated Mac native certificate-related inventory growth remains unresolved even if generated-factory compilation saturates.