75403caa1a
The DriverInstance.ResilienceConfig column was authored in AdminUI, persisted to the entity, and serialized into the deployment artifact by ConfigComposer — but the runtime read path dropped it: DriverInstanceSpec didn't carry it and the invoker factory always passed null, so every driver got tier defaults regardless of its configured overrides (a silent dead-config gap — #10's residual sub-finding). Read-path plumbing (write side was already complete): - DriverInstanceSpec gains ResilienceConfig; DeploymentArtifact.TryReadSpec reads the column. - IDriverCapabilityInvokerFactory.Create takes resilienceConfigJson; DriverHostActor.SpawnChild threads spec.ResilienceConfig; the concrete factory parses it (ParseOrDefaults, layering on the tier), logs any parse diagnostic (never throws), and builds the invoker with the merged options. - Invalidate-on-change: the pipeline cache keys on (instance, host, capability) and ignores options on a hit, so Create() now Invalidate()s the instance's cached pipelines first (no-op on first spawn) — a respawn with changed options rebuilds them. - DriverSpawnPlanner treats a ResilienceConfig change as a stop+respawn (the invoker/options are bound to the child at spawn); a pure DriverConfig change stays an in-place delta (no reconnect). - Host DI passes a logger to the factory for the parse diagnostic. Verification (deterministic): factory Create applies a retryCount:0 override to actual execution (control test proves tier default retries), invalidates the instance's cache on re-create (scoped — sibling survives), malformed config logs+falls-back; planner respawns on ResilienceConfig change (incl null→json) and stays delta on a pure config change; artifact parse carries/omits the column. Core.Tests 243 (+5), Runtime.Tests 363 (+5), Host builds clean.
188 lines
6.8 KiB
C#
188 lines
6.8 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
|
|
|
|
public sealed class DriverSpawnPlannerTests
|
|
{
|
|
private static DriverInstanceSpec Spec(string id, string type = "Modbus", string config = "{\"host\":\"127.0.0.1\"}", bool enabled = true, string? resilience = null) =>
|
|
new(Guid.NewGuid(), id, id, type, enabled, config, ClusterId: null, ResilienceConfig: resilience);
|
|
|
|
/// <summary>Verifies that all new drivers are placed in ToSpawn when current is empty.</summary>
|
|
[Fact]
|
|
public void All_new_drivers_go_into_ToSpawn_when_current_is_empty()
|
|
{
|
|
var current = new Dictionary<string, DriverChildSnapshot>();
|
|
var target = new[] { Spec("a"), Spec("b") };
|
|
|
|
var plan = DriverSpawnPlanner.Compute(current, target);
|
|
|
|
plan.ToSpawn.Count.ShouldBe(2);
|
|
plan.ToApplyDelta.ShouldBeEmpty();
|
|
plan.ToStop.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>Verifies that the same configuration yields an empty plan.</summary>
|
|
[Fact]
|
|
public void Same_config_yields_empty_plan()
|
|
{
|
|
var current = new Dictionary<string, DriverChildSnapshot>
|
|
{
|
|
["a"] = new("Modbus", "{\"host\":\"127.0.0.1\"}"),
|
|
};
|
|
var target = new[] { Spec("a") };
|
|
|
|
var plan = DriverSpawnPlanner.Compute(current, target);
|
|
|
|
plan.ToSpawn.ShouldBeEmpty();
|
|
plan.ToApplyDelta.ShouldBeEmpty();
|
|
plan.ToStop.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>Verifies that different configuration is routed to ApplyDelta.</summary>
|
|
[Fact]
|
|
public void Different_config_routes_to_ApplyDelta()
|
|
{
|
|
var current = new Dictionary<string, DriverChildSnapshot>
|
|
{
|
|
["a"] = new("Modbus", "{\"host\":\"old\"}"),
|
|
};
|
|
var target = new[] { Spec("a", config: "{\"host\":\"new\"}") };
|
|
|
|
var plan = DriverSpawnPlanner.Compute(current, target);
|
|
|
|
plan.ToApplyDelta.Single().DriverInstanceId.ShouldBe("a");
|
|
plan.ToSpawn.ShouldBeEmpty();
|
|
plan.ToStop.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>Verifies that removed drivers are routed to ToStop.</summary>
|
|
[Fact]
|
|
public void Removed_driver_routes_to_ToStop()
|
|
{
|
|
var current = new Dictionary<string, DriverChildSnapshot>
|
|
{
|
|
["a"] = new("Modbus", "{\"host\":\"127.0.0.1\"}"),
|
|
["b"] = new("Modbus", "{}"),
|
|
};
|
|
var target = new[] { Spec("a") };
|
|
|
|
var plan = DriverSpawnPlanner.Compute(current, target);
|
|
|
|
plan.ToStop.ShouldBe(new[] { "b" });
|
|
plan.ToSpawn.ShouldBeEmpty();
|
|
plan.ToApplyDelta.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>Verifies that disabled drivers with running children are routed to ToStop.</summary>
|
|
[Fact]
|
|
public void Disabled_driver_with_running_child_routes_to_ToStop()
|
|
{
|
|
var current = new Dictionary<string, DriverChildSnapshot>
|
|
{
|
|
["a"] = new("Modbus", "{}"),
|
|
};
|
|
var target = new[] { Spec("a", enabled: false) };
|
|
|
|
var plan = DriverSpawnPlanner.Compute(current, target);
|
|
|
|
plan.ToStop.Single().ShouldBe("a");
|
|
plan.ToSpawn.ShouldBeEmpty();
|
|
plan.ToApplyDelta.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>Verifies that disabled new drivers are not spawned.</summary>
|
|
[Fact]
|
|
public void Disabled_new_driver_is_not_spawned()
|
|
{
|
|
var current = new Dictionary<string, DriverChildSnapshot>();
|
|
var target = new[] { Spec("a", enabled: false) };
|
|
|
|
var plan = DriverSpawnPlanner.Compute(current, target);
|
|
|
|
plan.ToSpawn.ShouldBeEmpty();
|
|
plan.ToApplyDelta.ShouldBeEmpty();
|
|
plan.ToStop.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>Verifies that driver type changes trigger stop followed by respawn.</summary>
|
|
[Fact]
|
|
public void Driver_type_change_triggers_stop_plus_respawn()
|
|
{
|
|
var current = new Dictionary<string, DriverChildSnapshot>
|
|
{
|
|
["a"] = new("Modbus", "{}"),
|
|
};
|
|
var target = new[] { Spec("a", type: "AbCip") };
|
|
|
|
var plan = DriverSpawnPlanner.Compute(current, target);
|
|
|
|
plan.ToStop.Single().ShouldBe("a");
|
|
plan.ToSpawn.Single().DriverType.ShouldBe("AbCip");
|
|
plan.ToApplyDelta.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// A ResilienceConfig change forces a stop+respawn (NOT an in-place delta): the CapabilityInvoker
|
|
/// and its resolved options are bound to the child at spawn, so the only way a changed config takes
|
|
/// effect is to rebuild the child (the respawn's Create call invalidates the stale cached pipelines).
|
|
/// </summary>
|
|
[Fact]
|
|
public void ResilienceConfig_change_triggers_stop_plus_respawn()
|
|
{
|
|
var current = new Dictionary<string, DriverChildSnapshot>
|
|
{
|
|
// Same DriverType + same DriverConfig; only ResilienceConfig differs.
|
|
["a"] = new("Modbus", "{\"host\":\"127.0.0.1\"}", ResilienceConfig: "{\"bulkheadMaxConcurrent\":8}"),
|
|
};
|
|
var target = new[] { Spec("a", resilience: "{\"bulkheadMaxConcurrent\":32}") };
|
|
|
|
var plan = DriverSpawnPlanner.Compute(current, target);
|
|
|
|
plan.ToStop.Single().ShouldBe("a");
|
|
plan.ToSpawn.Single().DriverInstanceId.ShouldBe("a");
|
|
plan.ToApplyDelta.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adding a ResilienceConfig where there was none (null → JSON) is also a respawn — the invoker
|
|
/// was built with tier defaults and must be rebuilt to pick up the overrides.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Adding_ResilienceConfig_from_null_triggers_respawn()
|
|
{
|
|
var current = new Dictionary<string, DriverChildSnapshot>
|
|
{
|
|
["a"] = new("Modbus", "{\"host\":\"127.0.0.1\"}", ResilienceConfig: null),
|
|
};
|
|
var target = new[] { Spec("a", resilience: "{\"bulkheadMaxConcurrent\":16}") };
|
|
|
|
var plan = DriverSpawnPlanner.Compute(current, target);
|
|
|
|
plan.ToStop.Single().ShouldBe("a");
|
|
plan.ToSpawn.Single().DriverInstanceId.ShouldBe("a");
|
|
plan.ToApplyDelta.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// A pure DriverConfig change with an UNCHANGED ResilienceConfig stays an in-place delta (no
|
|
/// reconnect) — the resilience pipeline is untouched, so there's no reason to respawn.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DriverConfig_change_with_unchanged_ResilienceConfig_stays_ApplyDelta()
|
|
{
|
|
var current = new Dictionary<string, DriverChildSnapshot>
|
|
{
|
|
["a"] = new("Modbus", "{\"host\":\"old\"}", ResilienceConfig: "{\"bulkheadMaxConcurrent\":16}"),
|
|
};
|
|
var target = new[] { Spec("a", config: "{\"host\":\"new\"}", resilience: "{\"bulkheadMaxConcurrent\":16}") };
|
|
|
|
var plan = DriverSpawnPlanner.Compute(current, target);
|
|
|
|
plan.ToApplyDelta.Single().DriverInstanceId.ShouldBe("a");
|
|
plan.ToSpawn.ShouldBeEmpty();
|
|
plan.ToStop.ShouldBeEmpty();
|
|
}
|
|
}
|