fix(galaxy): #875 Galaxy initialize gets a 20 s budget via per-driver-type capability defaults
Gate F (#761) against the VD03 gateway: Galaxy initialize (session open + platform seed) took 4–5 s against Tier A's 5 s Initialize timeout, so 10 of 40 restarts failed, paid a 10 s retry and left a killed gateway worker. DriverFactoryRegistry.Register takes optional capabilityDefaults per driver type (GetCapabilityDefaults); DriverResilienceOptionsParser layers them over the tier table and under the per-instance ResilienceConfig; the Host wires the registry into DriverCapabilityInvokerFactory. Galaxy registers Initialize = 20 s (no retry, breaker 2), under the actor's 30 s pessimistic bound. Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
This commit is contained in:
+71
@@ -0,0 +1,71 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Resilience;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Resilience;
|
||||
|
||||
/// <summary>
|
||||
/// #875: a driver TYPE can register capability defaults that layer over its tier table and under the
|
||||
/// per-instance ResilienceConfig — Galaxy's initialize did not fit Tier A's 5 s.
|
||||
/// </summary>
|
||||
public sealed class DriverTypeCapabilityDefaultsTests
|
||||
{
|
||||
private static readonly Dictionary<DriverCapability, CapabilityPolicy> TypeDefaults = new()
|
||||
{
|
||||
[DriverCapability.Initialize] = new(TimeoutSeconds: 20, RetryCount: 0, BreakerFailureThreshold: 2),
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Type_defaults_override_the_tier_for_their_capability_only()
|
||||
{
|
||||
var options = DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.A, null, TypeDefaults, out var diag);
|
||||
diag.ShouldBeNull();
|
||||
options.Resolve(DriverCapability.Initialize).TimeoutSeconds.ShouldBe(20);
|
||||
options.Resolve(DriverCapability.Read).ShouldBe(DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Read]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_per_instance_ResilienceConfig_still_overrides_the_type_default()
|
||||
{
|
||||
var options = DriverResilienceOptionsParser.ParseOrDefaults(
|
||||
DriverTier.A, """{"capabilityPolicies":{"Initialize":{"timeoutSeconds":12}}}""", TypeDefaults, out _);
|
||||
options.Resolve(DriverCapability.Initialize).TimeoutSeconds.ShouldBe(12);
|
||||
options.Resolve(DriverCapability.Initialize).BreakerFailureThreshold.ShouldBe(2, "unspecified fields come from the type default");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Without_type_defaults_the_tier_table_is_unchanged()
|
||||
{
|
||||
DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.A, null, null, out _)
|
||||
.Resolve(DriverCapability.Initialize).TimeoutSeconds.ShouldBe(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_registry_returns_what_a_type_registered_and_null_otherwise()
|
||||
{
|
||||
var registry = new DriverFactoryRegistry();
|
||||
registry.Register("WithDefaults", (_, _) => null!, capabilityDefaults: TypeDefaults);
|
||||
registry.Register("Plain", (_, _) => null!);
|
||||
registry.GetCapabilityDefaults("withdefaults")!.ShouldContainKey(DriverCapability.Initialize);
|
||||
registry.GetCapabilityDefaults("Plain").ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_invoker_factory_applies_the_resolved_type_defaults()
|
||||
{
|
||||
var registry = new DriverFactoryRegistry();
|
||||
registry.Register("Slow", (_, _) => null!, capabilityDefaults: TypeDefaults);
|
||||
var tracker = new DriverResilienceStatusTracker();
|
||||
var factory = new DriverCapabilityInvokerFactory(new DriverResiliencePipelineBuilder(statusTracker: tracker), tracker,
|
||||
registry.GetTier, typeDefaultsResolver: registry.GetCapabilityDefaults);
|
||||
var invoker = factory.Create("slow-1", "Slow");
|
||||
// The options the invoker's pipelines are built from (private accessor; no public surface exposes them).
|
||||
var accessor = (Func<DriverResilienceOptions>)invoker.GetType()
|
||||
.GetField("_optionsAccessor", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!
|
||||
.GetValue(invoker)!;
|
||||
accessor().Resolve(DriverCapability.Initialize).TimeoutSeconds.ShouldBe(20);
|
||||
accessor().Resolve(DriverCapability.Read).TimeoutSeconds.ShouldBe(2);
|
||||
}
|
||||
}
|
||||
@@ -351,4 +351,15 @@ public sealed class GalaxyDriverFactoryTests
|
||||
var driver = GalaxyDriverFactoryExtensions.CreateInstance("g", MinimalConfig);
|
||||
driver.DriverType.ShouldBe("GalaxyMxGateway");
|
||||
}
|
||||
|
||||
/// <summary>#875: the Galaxy registration carries a 20 s Initialize budget over Tier A's 5 s.</summary>
|
||||
[Fact]
|
||||
public void Register_carries_the_Galaxy_initialize_budget()
|
||||
{
|
||||
var registry = new ZB.MOM.WW.OtOpcUa.Core.Hosting.DriverFactoryRegistry();
|
||||
GalaxyDriverFactoryExtensions.Register(registry, ZB.MOM.WW.OtOpcUa.Driver.Galaxy.NullSecretResolver.Instance);
|
||||
var defaults = registry.GetCapabilityDefaults(GalaxyDriverFactoryExtensions.DriverTypeName);
|
||||
defaults.ShouldNotBeNull();
|
||||
defaults![ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverCapability.Initialize].TimeoutSeconds.ShouldBe(20);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user