1ea1da8236
Since #516 a DriverConfig change is stop + respawn, so DriverSpawnPlan.ToApplyDelta was always empty and the in-place path had no producer. #663 and #664 were both latent bugs in it. This deletes it: - host: ApplyChildDelta, _pendingDelta, HandleApplyResult and HandleDeltaApplied - plan: DriverSpawnPlan.ToApplyDelta - actor: the ApplyDelta, ApplyResult and DeltaApplied messages, the Connected reinit handler and the mid-connect AdoptConfigDuringInit - tests: the actor tests that drove the seam IDriver.ReinitializeAsync and each driver's implementation and unit tests stay. Comments and docs that cited AdoptConfigDuringInit as the reason a driver must release prior state at init now cite the IDriver contract and overlapping retries, which still apply. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
88 lines
4.9 KiB
C#
88 lines
4.9 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 two lists and calls
|
|
/// spawn / stop on its child actors accordingly. There is no in-place config path: since #516 a config
|
|
/// change is a stop + respawn, and #665 deleted the unreachable in-place <c>ApplyDelta</c> seam.
|
|
/// </summary>
|
|
/// <param name="ToSpawn">Specs with no current child (or whose child is being replaced) — create a new actor.</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<string> ToStop);
|
|
|
|
public static class DriverSpawnPlanner
|
|
{
|
|
/// <summary>
|
|
/// Compute the spawn/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 and stop sets.</returns>
|
|
public static DriverSpawnPlan Compute(
|
|
IReadOnlyDictionary<string, DriverChildSnapshot> current,
|
|
IReadOnlyList<DriverInstanceSpec> target)
|
|
{
|
|
var toSpawn = 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;
|
|
}
|
|
// ANY of the three config surfaces changing forces a stop + respawn, so the FACTORY is the
|
|
// single parse authority for everything a driver is built from.
|
|
//
|
|
// • DriverType — factory-bound, can't be reinitialized in place.
|
|
// • ResilienceConfig — the CapabilityInvoker (and its resolved options) binds to the child
|
|
// actor at spawn time; the factory invalidates the stale cached pipelines on respawn.
|
|
// • DriverConfig — was an in-place ApplyDelta until #516. It is now a respawn.
|
|
//
|
|
// #516: the in-place delta silently DISCARDED the edit on five drivers (Modbus, FOCAS,
|
|
// OpcUaClient, AbLegacy, Sql), whose InitializeAsync served the options captured by their
|
|
// constructor and never looked at the driverConfigJson they were handed. The deployment still
|
|
// sealed green. Those five now re-parse (belt), and this respawn is the braces: several
|
|
// drivers build MORE than options from config — Sql derives its dialect + connection string
|
|
// and FOCAS its client-factory backend, both injected at construction — so an in-place
|
|
// re-parse of options ALONE would apply a new tag set against an old connection. Only a
|
|
// respawn rebuilds all of it.
|
|
//
|
|
// The accepted cost is a reconnect on every DriverConfig edit, replacing the prior
|
|
// no-reconnect in-place path. That is deliberate: a correct reconnect beats a silent no-op.
|
|
if (!string.Equals(snap.DriverType, spec.DriverType, StringComparison.Ordinal)
|
|
|| !string.Equals(snap.ResilienceConfig, spec.ResilienceConfig, StringComparison.Ordinal)
|
|
|| !string.Equals(snap.LastConfigJson, spec.DriverConfig, StringComparison.Ordinal))
|
|
{
|
|
toStop.Add(id);
|
|
toSpawn.Add(spec);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
foreach (var (id, spec) in targetById)
|
|
{
|
|
if (!spec.Enabled) continue;
|
|
if (current.ContainsKey(id)) continue;
|
|
toSpawn.Add(spec);
|
|
}
|
|
|
|
return new DriverSpawnPlan(toSpawn, 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);
|