Files

168 lines
10 KiB
Markdown

# Original close semantics and bounded retention controls
These are separately reviewed short controls against the unchanged 83ed880e
publication, SDK 1.5.378.182. They do not replace the historical `(1 browse,
6 history)` held-list port comparison, change an oracle, approve a memory
baseline, or claim an unbounded leak. The operation deadline is 120 seconds;
normal shutdown/private-directory cleanup has independent 20/20/5/5-second
bounds, and the semantic runner has a final 180-second process bound.
## What was tested
A fresh fixture creates the original production host, encrypted opc.tcp client,
one abandoned browse point and six event-history points. After normal client
CloseAsync, the control verifies removal from the same running SessionManager.
The old closed object still exposes `(1,6)` while explicitly held, as previously
recorded; that observation is not treated as live protocol ownership.
A fresh valid session supplies the same open raw transport for all following
requests. `ITransportChannel.SendRequestAsync` receives explicit request headers;
no closed Session client guard or ClientBase header substitution can satisfy the
negative checks. Each response must carry the exact sent RequestHandle.
| Request | Verified outcome |
|---|---|
| Fresh raw read before old-token request | Good service/item status |
| Read with old authentication token | Correlated ServiceFault, BadSessionIdInvalid (2149908480) |
| Fresh raw read after the fault, same transport | Good service/item status |
| Fresh browse point creation | Good, nonempty point |
| Old browse point from the fresh session | BadContinuationPointInvalid (2152333312) |
| Fresh own browse point resume and release | Good; resume returns references and another point |
| Fresh history page | Good; nonempty point; backend dispatched |
| Old history point from the fresh session | BadContinuationPointInvalid; backend counter remains 7 to 7 |
| Fresh own history point resume and release | Good; resume dispatches backend and returns another point |
All 11 raw responses were correlated. Generic transport failures/timeouts are not
accepted as token denial. The exact .182 ClientBase independently preserves a
nonnull AuthenticationToken in UpdateRequestHeader; the raw path bypasses that
method entirely. The protected token getter is read through an exact-type-checked
diagnostic accessor before close; tokens and payloads are never logged.
## Two distinct weak-reference boundaries
A NoInlining synchronous helper completes the async closed-session scope from a
thread-pool context, returning only weak references and primitive/type metadata.
No Session, point, response, token array or backend is returned as a strong
reference. The first GC phase runs while the SAME original host/configuration
remain alive. Their weak targets are explicitly expected alive. A second
NoInlining fixture boundary completes normal host shutdown, private-directory
cleanup and the complete async fixture scope before the outer GC phase.
Each phase records round 0 followed by exactly five forced blocking/compacting
collections with fixed 50/100/250/500/1000 ms delays. Each GC/finalizer worker is
bounded at five seconds. Observers use WeakReference.IsAlive rather than keeping
TryGetTarget values across collections. A deliberately retained positive object
must stay alive and an isolated collectible negative must disappear. No delay is
extended to obtain collection.
In the ordinary run all **16 old client/server Session, browse point/Data,
history-slot wrapper and history-value targets** disappeared during the first
live-host forced collection. This separates the held-list implementation detail
from those objects' actual reclamation in this fixture. After normal stop and
all fixture boundaries, five server-graph targets remained across all five
observations: server, node manager, backend, server validator and server cached
store. Rig/client configuration/client application/client validator/client cached
store targets disappeared. Positive and negative controls behaved correctly.
PKI descriptors were zero in live-host and stopped-but-harness-held observations.
The ordinary run preserves **exit 1**, with unsupported SDK2 manager-disposal
observables still explicit.
For this single fixture, live-host final managed bytes were 94,550,728 and timers
10; after-stop final managed bytes were 7,774,200 and timers 7. This drop does not
establish that the earlier multi-restart managed/timer growth disappears, nor is
it a usable performance denominator. Full process/framework parity remains open.
## Exact point-resource ownership
Runtime inspection identifies browse `ContinuationPoint.Data` as **Opc.Ua.NodeBrowser**
from the original Opc.Ua.Types binary. Its exact Dispose(bool) implementation is
empty. Its declared storage is managed IReference/List/HashSet/index state plus
managed context/view properties, with no owned IDisposable field. The containing
ContinuationPoint.Dispose delegates to Data, so omission of that call on this
exact browser does not skip a direct cleanup action. This is not a statement
about every INodeBrowser implementation or arbitrary driver browser resources.
The six history values are the original event HistoryContinuationState records;
the original store saves each record directly in the SDK slot. They have scalar,
string/date and managed paging/set state, no IDisposable contract or backend/store
handle ownership. This event fixture has no pending raw end-bound resource. Both
the SDK slot wrapper and its Value are weak-tracked; all collected while the
original host remained running.
## Stop is distinct from terminal Dispose
Read-only inspection of the pinned original product/SDK establishes:
1. Archived OtOpcUa OpcUaApplicationHost.DisposeAsync awaits only
ApplicationInstance.StopAsync.
2. Original ApplicationInstance.StopAsync delegates to Server.StopAsync.
3. ServerBase.StopAsync stops listeners/service hosts and invokes the standard
stopping hook. StandardServer's stopping hook DOES shut down subscriptions,
sessions and node managers and dispose internal server data.
4. Terminal ServerBase.Dispose separately disposes its RequestQueue.
RequestQueue owns a ServerBase reference and starts minimum worker tasks;
those workers await SemaphoreSlim with a 15,000 ms timeout and cancellation
token. At the minimum worker count they continue waiting. Queue.Dispose
cancels that token, releases/disposes its signal and terminates its ownership.
5. StandardServer.Dispose also unregisters its CertificateUpdate callback and
disposes remaining terminal resources. The SDK2 ApplicationInstance.DisposeAsync
explicitly performs StopAsync THEN Server.Dispose; the archived original host
never calls that terminal API.
This is a source-backed original application lifecycle gap and a plausible
retention path consistent with the observed server graph. It is not a measured
heap dominator/root chain or attribution of any particular timer. No original
production code was patched, and no extra disposal was added to ordinary mode.
A separate, gated `semantic-terminal-dispose` attribution mode first records the
ordinary result. Only afterward does a NoInlining helper briefly acquire the
known weak original server target, invoke its PUBLIC terminal Dispose once,
drop that strong reference, then repeat the same fixed GC schedule. No private
timer disposal, cache clearing, oracle adjustment or delay extension is used.
The original ordinary exit is returned even if terminal disposal clears all
observed targets. Its results are recorded separately in `semantic-evidence.json`.
It cannot turn the archived original behavior or failed RSS gates into a pass.
That separately approved run reproduced the same normal-stop five survivors and
returned **exit 1**. After the single public Dispose, all five disappeared at the
first next forced collection and remained absent through round 5. At the final
observations, timers changed from 7 to 2 and managed bytes from 7,692,144 to
4,513,760. Positive/negative GC controls remained valid. This controlled intervention
supports missing terminal server disposal as the ownership distinction; it does
not identify which queue task, timer or event subscription was the actual GC root.
It does not establish an unbounded leak from one fixture, eliminate unrelated
native retention, or make the original process a performance denominator.
Both immutable adapter builds completed with zero warnings/errors, using the
same 208 reference hashes. The ordinary run uses build 3 and the separate
counterfactual uses build 4; their exact source, binary, build-log and raw-result
hashes are retained in `semantic-evidence.json`. Raw evidence remains in private
external directories. No additional control was run to adjust these findings.
The exact original test seams are public virtual
`ServerBase.StopAsync(CancellationToken)`, protected virtual `Dispose(bool)`,
`OnServerStarting(ApplicationConfiguration)`,
`StartApplicationAsync(ApplicationConfiguration, CancellationToken)`, and
`OnServerStoppingAsync(CancellationToken)`. Public `Dispose()` and `StartAsync`
are nonvirtual. StandardServer overrides those protected lifecycle hooks and
also exposes protected virtual `OnServerStarted(IServerInternal)`. A separate
reviewed application repair can test stop failure/partial startup using these
supported seams; this diagnostic does not repair or rebuild the archived product.
## Smallest future matched-host path (proposal only)
A standalone SDK2 driver mirroring this original standalone driver is the smallest
path: same .NET runtime, assertions, telemetry/logging configuration, PKI/security,
operation recipe/counts, raw request/correlation probes, NoInlining boundaries,
weak controls, sampling schedule and module provenance. Keep SDK-specific value
representations and read-only ownership observers explicit. Both xUnit hosts
would require creating/porting a larger original test-host graph and introduce
more loader/framework differences. Neither path has been implemented or approved
as an acceptance protocol. The current candidate xUnit control is semantic
corroboration, not process-memory comparability.
Any correction of archived app ownership must be a separate reviewed change and
new provenance; these controls do not substitute a repaired baseline or use its
post-counterfactual memory as a denominator. Long original runs, reset baselines,
RSS caps and acceptance remain outside this work.