168 lines
10 KiB
Markdown
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.
|