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:
Joseph Doherty
2026-11-11 18:44:44 -05:00
parent 2be43aa984
commit 95bde828eb
8 changed files with 157 additions and 7 deletions
@@ -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);
}
}