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,214 +0,0 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
|
||||
|
||||
public sealed class DriverTypeRegistryTests
|
||||
{
|
||||
private static DriverTypeMetadata SampleMetadata(
|
||||
string typeName = "Modbus",
|
||||
DriverTier tier = DriverTier.B) =>
|
||||
new(typeName,
|
||||
DriverConfigJsonSchema: "{\"type\": \"object\"}",
|
||||
DeviceConfigJsonSchema: "{\"type\": \"object\"}",
|
||||
TagConfigJsonSchema: "{\"type\": \"object\"}",
|
||||
Tier: tier);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Register followed by Get round-trips metadata correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Register_ThenGet_RoundTrips()
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
var metadata = SampleMetadata();
|
||||
|
||||
registry.Register(metadata);
|
||||
|
||||
registry.Get("Modbus").ShouldBe(metadata);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Register accepts and preserves non-null tier values.
|
||||
/// </summary>
|
||||
/// <param name="tier">The driver tier to test.</param>
|
||||
[Theory]
|
||||
[InlineData(DriverTier.A)]
|
||||
[InlineData(DriverTier.B)]
|
||||
[InlineData(DriverTier.C)]
|
||||
public void Register_Requires_NonNullTier(DriverTier tier)
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
var metadata = SampleMetadata(typeName: $"Driver-{tier}", tier: tier);
|
||||
|
||||
registry.Register(metadata);
|
||||
|
||||
registry.Get(metadata.TypeName).Tier.ShouldBe(tier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Get is case-insensitive when looking up driver types.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Get_IsCaseInsensitive()
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
registry.Register(SampleMetadata("Galaxy"));
|
||||
|
||||
registry.Get("galaxy").ShouldNotBeNull();
|
||||
registry.Get("GALAXY").ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Get throws when looking up an unknown type.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Get_UnknownType_Throws()
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
registry.Register(SampleMetadata("Modbus"));
|
||||
|
||||
Should.Throw<KeyNotFoundException>(() => registry.Get("UnregisteredType"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that TryGet returns null when looking up an unknown type.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TryGet_UnknownType_ReturnsNull()
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
registry.Register(SampleMetadata("Modbus"));
|
||||
|
||||
registry.TryGet("UnregisteredType").ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Register throws when attempting to register a duplicate type.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Register_DuplicateType_Throws()
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
registry.Register(SampleMetadata("Modbus"));
|
||||
|
||||
Should.Throw<InvalidOperationException>(() => registry.Register(SampleMetadata("Modbus")));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that duplicate type detection is case-insensitive.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Register_DuplicateTypeIsCaseInsensitive()
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
registry.Register(SampleMetadata("Modbus"));
|
||||
|
||||
Should.Throw<InvalidOperationException>(() => registry.Register(SampleMetadata("modbus")));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that All returns all registered driver types.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void All_ReturnsRegisteredTypes()
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
registry.Register(SampleMetadata("Modbus"));
|
||||
registry.Register(SampleMetadata("S7"));
|
||||
registry.Register(SampleMetadata("Galaxy"));
|
||||
|
||||
var all = registry.All();
|
||||
|
||||
all.Count.ShouldBe(3);
|
||||
all.Select(m => m.TypeName).ShouldBe(new[] { "Modbus", "S7", "Galaxy" }, ignoreOrder: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Get rejects empty or null type names.
|
||||
/// </summary>
|
||||
/// <param name="typeName">The type name to test (null, empty, or whitespace).</param>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void Get_RejectsEmptyTypeName(string? typeName)
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
Should.Throw<ArgumentException>(() => registry.Get(typeName!));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core.Abstractions-004: concurrent <see cref="DriverTypeRegistry.Register"/> calls for
|
||||
/// two different driver types must both succeed and both end up visible to readers. A
|
||||
/// non-atomic check-then-act would let the second swap silently discard the first
|
||||
/// registration; this test asserts the "registered only once per process" guarantee
|
||||
/// holds under concurrent writers too.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Register_ConcurrentDistinctTypes_AllSucceed()
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
const int parallelism = 32;
|
||||
var ready = new ManualResetEventSlim(false);
|
||||
|
||||
var threads = Enumerable.Range(0, parallelism)
|
||||
.Select(i => new Thread(() =>
|
||||
{
|
||||
ready.Wait();
|
||||
registry.Register(SampleMetadata($"Driver-{i}"));
|
||||
}))
|
||||
.ToArray();
|
||||
|
||||
foreach (var t in threads) t.Start();
|
||||
ready.Set(); // release all threads simultaneously
|
||||
foreach (var t in threads) t.Join();
|
||||
|
||||
var all = registry.All();
|
||||
all.Count.ShouldBe(parallelism);
|
||||
for (var i = 0; i < parallelism; i++)
|
||||
registry.TryGet($"Driver-{i}").ShouldNotBeNull(
|
||||
$"Driver-{i} was lost to a race in concurrent Register");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core.Abstractions-004: concurrent <see cref="DriverTypeRegistry.Register"/> calls for
|
||||
/// the SAME driver type must result in exactly one successful registration and exactly
|
||||
/// (parallelism - 1) <see cref="InvalidOperationException"/> throws — not a silent
|
||||
/// last-writer-wins replacement.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Register_ConcurrentDuplicateType_ExactlyOneWins()
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
const int parallelism = 16;
|
||||
var ready = new ManualResetEventSlim(false);
|
||||
var successes = 0;
|
||||
var failures = 0;
|
||||
|
||||
var threads = Enumerable.Range(0, parallelism)
|
||||
.Select(_ => new Thread(() =>
|
||||
{
|
||||
ready.Wait();
|
||||
try
|
||||
{
|
||||
registry.Register(SampleMetadata("Contended"));
|
||||
Interlocked.Increment(ref successes);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
Interlocked.Increment(ref failures);
|
||||
}
|
||||
}))
|
||||
.ToArray();
|
||||
|
||||
foreach (var t in threads) t.Start();
|
||||
ready.Set();
|
||||
foreach (var t in threads) t.Join();
|
||||
|
||||
successes.ShouldBe(1);
|
||||
failures.ShouldBe(parallelism - 1);
|
||||
registry.All().Count.ShouldBe(1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user