refactor(drivers): delete the inert driver-tier recycle machinery (#522)
The tier system was documented, operator-authorable, and inert. Deleted rather than activated,
because its premise is gone rather than merely unused.
"Tier C" meant a driver running out-of-process behind an IDriverSupervisor that could restart its
Host without tearing down the OPC UA session. No such process exists anywhere: Galaxy reaches
MXAccess over gRPC to the external mxaccessgw sidecar (PR 7.2 retired the in-process
Galaxy.Host/Proxy/Shared projects) and FOCAS has run in-process since its managed wire client
landed 2026-04-24. Consistently, IDriverSupervisor had ZERO implementations and there was nothing
for one to implement against.
The issue understated the inertness. It says the Tier-C-only protections never engaged, implying
the Tier A/B parts did. They did not: nothing constructs MemoryTracking, MemoryRecycle or
ScheduledRecycleScheduler outside their own unit tests, so the whole Core/Stability recycle layer
was dead — meaning option (a), "pass real tiers", was never a flag flip. It would have meant
writing the wiring that never existed AND arming it.
Deleted: MemoryTracking, MemoryRecycle, ScheduledRecycleScheduler, IDriverSupervisor, the
vestigial DriverTypeRegistry (referenced only by its own tests), and the RecycleIntervalSeconds
knob — which the AdminUI let an operator author and the parser validated while it configured
nothing.
DriverTier itself SURVIVES and is load-bearing: DriverResilienceOptions.GetTierDefaults supplies
the real per-capability timeout/retry/breaker policies via DriverFactoryRegistry.GetTier and
DriverCapabilityInvokerFactory. Only the isolation-and-recycle layer above it is gone.
Deliberately NOT deleted:
- WedgeDetector came along in the same directory and is equally dead in production, but it is
tier-agnostic and is not recycle machinery — it only shares the folder. Restored rather than
swept up in a decision that was not about it.
- IDriver.GetMemoryFootprint() and FlushOptionalCachesAsync() lose their only consumer here.
Removing them touches all 12 drivers and every test stub, so they are documented as
consumerless and filed as #525 instead of buried in this diff.
Compatibility: a deployed ResilienceConfig blob still carrying "recycleIntervalSeconds" parses
cleanly (unknown keys are ignored — guarded by a new test, because a blob that suddenly failed to
parse would fall back to tier defaults and silently discard the operator's real overrides), and
the AdminUI's preserve-unknown-keys bag keeps the key rather than rewriting stored config on an
unrelated edit.
The 01/U-6 knob-inertness guard carried an explicit carve-out admitting RecycleIntervalSeconds was
dormant and out of scope; that carve-out is now gone, so the expected set is literally what the
test's name claims.
Note: Host.IntegrationTests has 2 failures (DriverProbeRegistrationTests.is_idempotent,
PrimaryGateFailoverTests) — verified pre-existing by reproducing both on clean master dc9d947b.
Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
This commit is contained in:
@@ -1,104 +0,0 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Process-singleton registry of driver types known to this OtOpcUa instance.
|
||||
/// Per-driver assemblies register their type metadata at startup; the Core uses
|
||||
/// the registry to validate <c>DriverInstance.DriverType</c> values from the central config DB.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Per <c>docs/v2/plan.md</c> decisions on JSON content validation happening in the Admin app
|
||||
/// (not SQL CLR), and on driver type → namespace kind mapping being enforced by
|
||||
/// <c>sp_ValidateDraft</c>. The registry is the source of truth for both checks.
|
||||
///
|
||||
/// Thread-safety: registration is typically single-threaded at startup; lookups happen on
|
||||
/// every config-apply (multi-threaded). The check-then-act inside <see cref="Register"/> is
|
||||
/// guarded by a private lock so concurrent registrations are atomic — the "registered only
|
||||
/// once per process" guarantee holds even if two callers race. Readers operate against the
|
||||
/// volatile snapshot reference produced by the last successful <see cref="Register"/> and
|
||||
/// never block.
|
||||
/// </remarks>
|
||||
public sealed class DriverTypeRegistry
|
||||
{
|
||||
private readonly Lock _writeLock = new();
|
||||
|
||||
private IReadOnlyDictionary<string, DriverTypeMetadata> _types =
|
||||
new Dictionary<string, DriverTypeMetadata>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>Register a driver type. Throws if the type name is already registered.</summary>
|
||||
/// <remarks>
|
||||
/// The check-then-act (duplicate check → copy-on-write rebuild → swap) is performed under
|
||||
/// <see cref="_writeLock"/> so concurrent <see cref="Register"/> calls cannot silently
|
||||
/// discard each other's registrations.
|
||||
/// </remarks>
|
||||
/// <param name="metadata">The driver type metadata to register.</param>
|
||||
public void Register(DriverTypeMetadata metadata)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(metadata);
|
||||
|
||||
lock (_writeLock)
|
||||
{
|
||||
var snapshot = _types;
|
||||
if (snapshot.ContainsKey(metadata.TypeName))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Driver type '{metadata.TypeName}' is already registered. " +
|
||||
$"Each driver type may be registered only once per process.");
|
||||
}
|
||||
|
||||
var next = new Dictionary<string, DriverTypeMetadata>(snapshot, StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[metadata.TypeName] = metadata,
|
||||
};
|
||||
_types = next;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Look up a driver type by name. Throws if unknown.</summary>
|
||||
/// <param name="driverType">The driver type name to look up.</param>
|
||||
/// <returns>The registered metadata for the driver type.</returns>
|
||||
public DriverTypeMetadata Get(string driverType)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
|
||||
|
||||
if (_types.TryGetValue(driverType, out var metadata))
|
||||
return metadata;
|
||||
|
||||
throw new KeyNotFoundException(
|
||||
$"Driver type '{driverType}' is not registered. " +
|
||||
$"Known types: {string.Join(", ", _types.Keys)}.");
|
||||
}
|
||||
|
||||
/// <summary>Try to look up a driver type by name. Returns null if unknown (no exception).</summary>
|
||||
/// <param name="driverType">The driver type name to look up.</param>
|
||||
/// <returns>The registered metadata, or <see langword="null"/> if the type is not registered.</returns>
|
||||
public DriverTypeMetadata? TryGet(string driverType)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
|
||||
return _types.GetValueOrDefault(driverType);
|
||||
}
|
||||
|
||||
/// <summary>Snapshot of all registered driver types.</summary>
|
||||
/// <returns>A snapshot collection of all registered driver type metadata.</returns>
|
||||
public IReadOnlyCollection<DriverTypeMetadata> All() => _types.Values.ToList();
|
||||
}
|
||||
|
||||
/// <summary>Per-driver-type metadata used by the Core, validator, and Admin UI.</summary>
|
||||
/// <param name="TypeName">Driver type name (matches <c>DriverInstance.DriverType</c> column values).</param>
|
||||
/// <param name="DriverConfigJsonSchema">JSON Schema (Draft 2020-12) the driver's <c>DriverConfig</c> column must validate against.</param>
|
||||
/// <param name="DeviceConfigJsonSchema">JSON Schema for <c>DeviceConfig</c> (multi-device drivers); null if the driver has no device layer.</param>
|
||||
/// <param name="TagConfigJsonSchema">JSON Schema for <c>TagConfig</c>; required for every driver since every driver has tags.</param>
|
||||
/// <param name="Tier">
|
||||
/// Stability tier per <c>docs/v2/driver-stability.md</c> §2-4 and the tiering decisions in
|
||||
/// <c>docs/v2/plan.md</c>. Drives the shared resilience pipeline defaults
|
||||
/// (<see cref="Tier"/> × capability → <c>CapabilityPolicy</c>), the <c>MemoryTracking</c>
|
||||
/// hybrid-formula constants, and whether process-level <c>MemoryRecycle</c> / scheduled-
|
||||
/// recycle protections apply (Tier C only). Every registered driver type must declare one.
|
||||
/// </param>
|
||||
// v3: AllowedNamespaceKinds retired with the Namespace entity — the two OPC UA namespaces (Raw/UNS)
|
||||
// are implicit, so a driver type no longer declares a namespace-kind compatibility bitmask.
|
||||
public sealed record DriverTypeMetadata(
|
||||
string TypeName,
|
||||
string DriverConfigJsonSchema,
|
||||
string? DeviceConfigJsonSchema,
|
||||
string TagConfigJsonSchema,
|
||||
DriverTier Tier);
|
||||
@@ -48,20 +48,21 @@ public interface IDriver
|
||||
|
||||
/// <summary>
|
||||
/// Approximate driver-attributable footprint in bytes (caches, queues, symbol tables).
|
||||
/// Polled every 30s by Core; on cache-budget breach, Core asks the driver to flush via
|
||||
/// <see cref="FlushOptionalCachesAsync"/>.
|
||||
/// <para>⚠️ <b>Nothing calls this (Gitea #525).</b> Its only consumer was <c>MemoryTracking</c>,
|
||||
/// deleted with the rest of the driver-tier recycle machinery in Gitea #522 — the doc-comment here
|
||||
/// used to claim Core polled it every 30 s, which was never true in v3. All 12 drivers still
|
||||
/// implement it; most return a real number, and returning <c>0</c> is equally correct today. Do not
|
||||
/// read a value from it and act on it without first building a consumer that is actually wired.</para>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Per <c>docs/v2/driver-stability.md</c> §"In-process only (Tier A/B) — driver-instance
|
||||
/// allocation tracking". Tier C drivers (process-isolated) report through the same
|
||||
/// interface but the cache-flush is internal to their host.
|
||||
/// </remarks>
|
||||
/// <returns>The approximate memory footprint in bytes.</returns>
|
||||
long GetMemoryFootprint();
|
||||
|
||||
/// <summary>
|
||||
/// Drop optional caches (symbol cache, browse cache, etc.) to bring footprint back below budget.
|
||||
/// Required-for-correctness state must NOT be flushed.
|
||||
/// <para>⚠️ <b>Nothing calls this (Gitea #525)</b> — same reason as
|
||||
/// <see cref="GetMemoryFootprint"/>: the memory-pressure path that would have invoked it is
|
||||
/// gone.</para>
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Process-level supervisor contract a Tier C driver's out-of-process topology provides
|
||||
/// (e.g. <c>Driver.Galaxy.Proxy/Supervisor/</c>). Concerns: restart the Host process when a
|
||||
/// hard fault is detected (memory breach, wedge, scheduled recycle window).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Tier A/B drivers do NOT have a supervisor because they run in-process — recycling would
|
||||
/// kill every OPC UA session and every co-hosted driver. The Core.Stability layer only invokes
|
||||
/// this interface for Tier C instances after asserting the tier via
|
||||
/// <see cref="DriverTypeMetadata.Tier"/>.
|
||||
/// </remarks>
|
||||
public interface IDriverSupervisor
|
||||
{
|
||||
/// <summary>Driver instance this supervisor governs.</summary>
|
||||
string DriverInstanceId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Request the supervisor to recycle (terminate + restart) the Host process. Implementations
|
||||
/// are expected to be idempotent under repeat calls during an in-flight recycle.
|
||||
/// </summary>
|
||||
/// <param name="reason">Human-readable reason — flows into the supervisor's logs.</param>
|
||||
/// <param name="cancellationToken">Cancels the recycle request; an in-flight restart is not interrupted.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task RecycleAsync(string reason, CancellationToken cancellationToken);
|
||||
}
|
||||
Reference in New Issue
Block a user