97dc715dac
v2-ci / unit-tests (push) Has been cancelled
Three write bounds were configured independently: the inbound-write gateway's 10 s Ask, DriverHostActor's fixed 8 s Ask of the child, and the child's own write timeout (5 s + up to 2 s grace) with a fixed 8 s queue budget. An outer Ask that fires first reports the write failed and the node reverts while the child is still driving the device, so any write timeout above ~6 s (or a write dequeued late in its 8 s budget) let the device take a value the client was told failed. DriverWriteBounds (Runtime) derives them all from DriverWrites:Timeout: token cancel = Timeout abandon = Timeout + min(Timeout/2, 2 s) (RunBound) queue budget = RunBound child replies <= queue budget + RunBound (ReplyBound) host Ask = ReplyBound + 1 s gateway Ask = host Ask + 1 s Default Timeout is 10 s (the Tier C Write timeout, so no tier default is capped below its own policy); host Ask 25 s, gateway Ask 26 s. Tier A/B writes are still cancelled first by their 2 s / 4 s tier timeouts. - DriverHostActor takes writeBounds + writeClock, spawns children with them, and runs its Ask deadline as a token on the write clock (Infinite Akka timeout), so host and child bounds share one clock. - WriteAttribute gains IssuedAt (write-clock timestamp, stamped by the host): the queue budget counts mailbox time too, and a write past it is failed BadTimeout and never started - including on arrival. Stale queued writes are expired across the whole queue, not only up to a free slot. - ActorNodeWriteGateway's default Ask = Default.GatewayAsk; the hosted service passes the configured one. - Startup validation (Host DriverWriteBoundsBinding, same warn-and-clamp style as ScriptingOptionsBinding): unparsable / non-positive -> default, above 5 min -> clamped, and each tier whose default Write timeout exceeds the write timeout is named in a Warning (its policy would never take effect). The derived bounds are logged at Information. Per-instance ResilienceConfig Write overrides arrive with a deployment, so DriverCapabilityInvokerFactory warns at spawn (writeTimeoutCeiling). - appsettings.json documents DriverWrites:Timeout. Tests (manual clock): - DriverHostActorWriteAskTests: a held write under a 9 s timeout survives an 8.5 s step and succeeds; a token-ignoring write is answered by the child's BadTimeout at 9 s + grace, not by the Ask; the Infinite-timeout Ask ends exactly at its clock deadline. - DriverInstanceActorConcurrentWriteTests: a write issued longer ago than the queue budget is failed and never reaches the driver; positive control at exactly the budget is written. - DriverWriteBoundsTests: every outer bound covers the inner ones for 0.5 s to 300 s timeouts. - DriverWriteBoundsBindingTests: default fits every tier; 3 s timeout is caught at startup with Tier B and C named (not A); bad values fall back with a warning; over-max clamps. - DriverCapabilityInvokerFactoryTests: spawn warns when a Write override exceeds the ceiling; no warning within it or without a ceiling. Mutation checks: a fixed 8 s host Ask on the same clock fails both 9 s host tests; stamping arrival time instead of IssuedAt fails the issued-at test; dropping the tier check / factory check fails their tests. Runtime.Tests 961 passed / 3 skipped at AKKA_TEST_TIMEFACTOR 3 and 1; Host.Tests 232/232 at 3 and 1; Core.Tests 361 passed / 1 skipped. Solution Release build: 0 warnings. ContinuousHistorizationRecorderTypedValueTests (historian) deliberately left for the historian lane. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
85 lines
3.6 KiB
C#
85 lines
3.6 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Resilience;
|
|
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Host.Tests.Configuration;
|
|
|
|
/// <summary>
|
|
/// lmxopcua#777: <c>DriverWrites:Timeout</c> is the one value every write bound derives from, and it is checked
|
|
/// against the resilience tiers' Write timeouts at startup.
|
|
/// </summary>
|
|
public sealed class DriverWriteBoundsBindingTests
|
|
{
|
|
private static IConfiguration Config(Dictionary<string, string?> values) =>
|
|
new ConfigurationBuilder().AddInMemoryCollection(values).Build();
|
|
|
|
/// <summary>The shipped default is consistent with every tier — no tier's Write policy is capped below itself.</summary>
|
|
[Fact]
|
|
public void An_absent_key_yields_the_default_which_fits_every_tier()
|
|
{
|
|
var bounds = DriverWriteBoundsBinding.Bind(Config(new()), out var warnings);
|
|
|
|
warnings.ShouldBeEmpty();
|
|
bounds.ShouldBe(DriverWriteBounds.Default);
|
|
foreach (var tier in Enum.GetValues<DriverTier>())
|
|
{
|
|
TimeSpan.FromSeconds(DriverResilienceOptions.GetTierDefaults(tier)[DriverCapability.Write].TimeoutSeconds)
|
|
.ShouldBeLessThanOrEqualTo(bounds.Timeout, $"tier {tier}");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void The_configured_timeout_binds_and_every_bound_derives_from_it()
|
|
{
|
|
var bounds = DriverWriteBoundsBinding.Bind(Config(new() { ["DriverWrites:Timeout"] = "00:00:12" }), out var warnings);
|
|
|
|
warnings.ShouldBeEmpty();
|
|
bounds.Timeout.ShouldBe(TimeSpan.FromSeconds(12));
|
|
bounds.HostAsk.ShouldBe(TimeSpan.FromSeconds(12 + 2 + 12 + 2 + 1));
|
|
bounds.GatewayAsk.ShouldBe(bounds.HostAsk + DriverWriteBounds.ReplyMargin);
|
|
}
|
|
|
|
/// <summary>
|
|
/// An inconsistent config is caught at startup: a write timeout below a tier's Write timeout means that tier's
|
|
/// policy never takes effect, and each such tier is named. Tier A (2 s) fits under 3 s and is not.
|
|
/// </summary>
|
|
[Fact]
|
|
public void A_timeout_below_a_tiers_Write_timeout_is_reported_per_tier()
|
|
{
|
|
var bounds = DriverWriteBoundsBinding.Bind(Config(new() { ["DriverWrites:Timeout"] = "00:00:03" }), out var warnings);
|
|
|
|
bounds.Timeout.ShouldBe(TimeSpan.FromSeconds(3), "an inconsistency is reported, not overridden");
|
|
warnings.Count.ShouldBe(2);
|
|
warnings.ShouldContain(w => w.StartsWith("Tier B ", StringComparison.Ordinal) && w.Contains("(4s)"));
|
|
warnings.ShouldContain(w => w.StartsWith("Tier C ", StringComparison.Ordinal) && w.Contains("(10s)"));
|
|
warnings.ShouldNotContain(w => w.StartsWith("Tier A ", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("not-a-timespan")]
|
|
[InlineData("00:00:00")]
|
|
[InlineData("-00:00:05")]
|
|
public void An_unusable_value_falls_back_to_the_default_with_a_warning(string raw)
|
|
{
|
|
var bounds = DriverWriteBoundsBinding.Bind(Config(new() { ["DriverWrites:Timeout"] = raw }), out var warnings);
|
|
|
|
bounds.ShouldBe(DriverWriteBounds.Default);
|
|
warnings.Count.ShouldBe(1);
|
|
warnings[0].ShouldContain("DriverWrites:Timeout");
|
|
}
|
|
|
|
[Fact]
|
|
public void A_value_above_the_max_is_clamped_with_a_warning()
|
|
{
|
|
var bounds = DriverWriteBoundsBinding.Bind(Config(new() { ["DriverWrites:Timeout"] = "01:00:00" }), out var warnings);
|
|
|
|
bounds.Timeout.ShouldBe(DriverWriteBounds.MaxTimeout);
|
|
warnings.Count.ShouldBe(1);
|
|
warnings[0].ShouldContain("clamped");
|
|
}
|
|
}
|