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.
80 lines
4.0 KiB
C#
80 lines
4.0 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
|
|
|
/// <summary>
|
|
/// Pure diff between the currently-running driver children (keyed by
|
|
/// <c>DriverInstance.DriverInstanceId</c>) and the target spec list from a freshly-applied
|
|
/// deployment artifact. The DriverHostActor consumes the three lists and calls
|
|
/// spawn / ApplyDelta / stop on its child actors accordingly.
|
|
/// </summary>
|
|
/// <param name="ToSpawn">Specs with no current child — create a new actor.</param>
|
|
/// <param name="ToApplyDelta">Specs whose child exists but config JSON or type differs.</param>
|
|
/// <param name="ToStop">DriverInstanceIds currently running but missing from the new artifact, or now disabled.</param>
|
|
public sealed record DriverSpawnPlan(
|
|
IReadOnlyList<DriverInstanceSpec> ToSpawn,
|
|
IReadOnlyList<DriverInstanceSpec> ToApplyDelta,
|
|
IReadOnlyList<string> ToStop);
|
|
|
|
public static class DriverSpawnPlanner
|
|
{
|
|
/// <summary>
|
|
/// Compute the spawn/delta/stop sets. Disabled entries in <paramref name="target"/> are
|
|
/// treated as "not desired here": if a child exists for the id it goes into ToStop,
|
|
/// otherwise the row is dropped entirely (no spawn for a disabled driver).
|
|
/// </summary>
|
|
/// <param name="current">The currently running driver children keyed by ID.</param>
|
|
/// <param name="target">The target driver instances from the deployment artifact.</param>
|
|
/// <returns>The computed <see cref="DriverSpawnPlan"/> with spawn, apply-delta, and stop sets.</returns>
|
|
public static DriverSpawnPlan Compute(
|
|
IReadOnlyDictionary<string, DriverChildSnapshot> current,
|
|
IReadOnlyList<DriverInstanceSpec> target)
|
|
{
|
|
var toSpawn = new List<DriverInstanceSpec>();
|
|
var toDelta = new List<DriverInstanceSpec>();
|
|
var toStop = new List<string>();
|
|
|
|
var targetById = new Dictionary<string, DriverInstanceSpec>(StringComparer.Ordinal);
|
|
foreach (var spec in target) targetById[spec.DriverInstanceId] = spec;
|
|
|
|
foreach (var (id, snap) in current)
|
|
{
|
|
if (!targetById.TryGetValue(id, out var spec) || !spec.Enabled)
|
|
{
|
|
toStop.Add(id);
|
|
continue;
|
|
}
|
|
// A driver TYPE change can't be reinitialized in-place (factory-bound) — stop + respawn.
|
|
// A RESILIENCE-CONFIG change likewise forces a respawn: the CapabilityInvoker (and its
|
|
// resolved options) is bound to the child actor at spawn time, so the only way a changed
|
|
// ResilienceConfig takes effect is to rebuild the child. The factory invalidates the stale
|
|
// cached pipelines on the respawn's Create call. (A pure DriverConfig change stays an
|
|
// in-place delta — no reconnect — because it doesn't touch the resilience pipeline.)
|
|
if (!string.Equals(snap.DriverType, spec.DriverType, StringComparison.Ordinal)
|
|
|| !string.Equals(snap.ResilienceConfig, spec.ResilienceConfig, StringComparison.Ordinal))
|
|
{
|
|
toStop.Add(id);
|
|
toSpawn.Add(spec);
|
|
continue;
|
|
}
|
|
if (!string.Equals(snap.LastConfigJson, spec.DriverConfig, StringComparison.Ordinal))
|
|
{
|
|
toDelta.Add(spec);
|
|
}
|
|
}
|
|
|
|
foreach (var (id, spec) in targetById)
|
|
{
|
|
if (!spec.Enabled) continue;
|
|
if (current.ContainsKey(id)) continue;
|
|
toSpawn.Add(spec);
|
|
}
|
|
|
|
return new DriverSpawnPlan(toSpawn, toDelta, toStop);
|
|
}
|
|
}
|
|
|
|
/// <summary>Snapshot of one running driver child as the host sees it. Used as the diff input.</summary>
|
|
/// <param name="DriverType">The driver type name the child was spawned for.</param>
|
|
/// <param name="LastConfigJson">The <c>DriverConfig</c> JSON the child currently serves.</param>
|
|
/// <param name="ResilienceConfig">The per-instance <c>ResilienceConfig</c> JSON the child's invoker was built with (null = tier defaults).</param>
|
|
public sealed record DriverChildSnapshot(string DriverType, string LastConfigJson, string? ResilienceConfig = null);
|