fix(drivers): stop silently discarding driver config edits (§8.3, #516)
Five drivers ignored the config handed to them on reinitialize, serving the
options their constructor captured — and the deployment sealed green anyway.
Fixed on both halves, as chosen.
Seam (load-bearing): DriverSpawnPlanner routes a changed DriverConfig to
ToStop + ToSpawn instead of an in-place delta, making the factory the single
parse authority. This REVERSES the deliberate decision documented at
DriverSpawnPlan.cs:49-50 ("a pure DriverConfig change stays an in-place delta
— no reconnect"). That reasoning was correct about the resilience pipeline and
wrong about the driver. The accepted price is a reconnect per config edit.
Per-driver, and this split was not anticipated:
- Re-parse in place — Modbus, AbLegacy, OpcUaClient. ParseOptions extracted
from each factory, called from InitializeAsync behind a HasConfigBody guard
so "{}" still keeps the constructor options.
- Respawn-only, deliberately NOT re-parsed — Sql and FOCAS. Each builds more
than options from config (Sql's dialect + resolved connection string,
FOCAS's client-factory backend, both injected at construction), so adopting
new options alone would run a NEW tag set against an OLD connection. Half a
re-parse is worse than none. SqlDriver's doc-comment asserted the opposite
premise — "config parsing belongs to the factory, which builds a fresh
instance" — which was false when written and is true only now.
Two green seals removed from ApplyChildDelta: it overwrote the cached Spec
synchronously BEFORE the child dequeued the message, so the host believed the
new config was live and the next reconcile computed no delta, sealing the
drift permanently; and it Tell'd with no Receive<ApplyResult> registered, so a
failed reinit — including Galaxy's deliberate NotSupportedException —
dead-lettered.
Exposed (not created) by the change: a factory throw is a CONFIG error, and
SpawnChild catches it and silently substitutes a stub. Only a brand-new driver
could reach that before; an ordinary config edit can now. Raised Warning ->
Error with an actionable message. It still does not fail the deployment —
doing so would let one malformed driver block a fleet deploy, so that is a
follow-up rather than a drive-by.
Tests: every pre-existing reinit test in these suites passes "{}", the exact
input a guarded re-parser treats as "keep constructor options" — blind to this
defect by construction. New tests pass CHANGED json; the Modbus one was
verified falsifiable by deleting the re-parse (goes red). Two tests asserted
the old behaviour and were rewritten, including the positive control in
DriverHostActorUnreadableArtifactTests, whose observable moved from
UnsubscribeAsync to ShutdownAsync now that teardown happens by stopping the
child rather than emptying its desired set.
Remaining full-suite failures are pre-existing and environmental, verified
identical on the pre-change tree: Host.IntegrationTests (3) and
Driver.AbLegacy.IntegrationTests (4, docker fixture-gated).
This commit is contained in:
@@ -23,7 +23,9 @@ public sealed class ModbusDriver
|
||||
{
|
||||
// ---- instance fields (grouped at top for auditability) ----
|
||||
|
||||
private readonly ModbusDriverOptions _options;
|
||||
/// <summary>Mutable so <see cref="InitializeAsync"/> can adopt a re-parsed config on reinitialize
|
||||
/// (#516). Only ever written on the init path, before any reader/transport uses it.</summary>
|
||||
private ModbusDriverOptions _options;
|
||||
private readonly Func<ModbusDriverOptions, IModbusTransport> _transportFactory;
|
||||
private readonly string _driverInstanceId;
|
||||
private readonly ILogger<ModbusDriver> _logger;
|
||||
@@ -188,12 +190,29 @@ public sealed class ModbusDriver
|
||||
/// <inheritdoc />
|
||||
public string DriverType => "Modbus";
|
||||
|
||||
/// <summary>True when the supplied DriverConfig JSON carries a real body. The bootstrapper always
|
||||
/// passes a populated document; some unit tests pass <c>"{}"</c> or an empty string to exercise
|
||||
/// lifecycle shape without a config — those keep the constructor-supplied options.</summary>
|
||||
private static bool HasConfigBody(string? driverConfigJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(driverConfigJson)) return false;
|
||||
var trimmed = driverConfigJson.Trim();
|
||||
return trimmed is not "{}" and not "[]";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
|
||||
{
|
||||
WriteHealth(new DriverHealth(DriverState.Initializing, null, null));
|
||||
try
|
||||
{
|
||||
// #516: re-parse the supplied DriverConfig so a config change delivered through the IDriver
|
||||
// contract is honoured. Without this the driver keeps serving the options it was CONSTRUCTED
|
||||
// with, so an operator's edit is discarded while the deployment still seals green. An empty /
|
||||
// placeholder document (the "{}" some unit tests pass) keeps the constructor-supplied options.
|
||||
if (HasConfigBody(driverConfigJson))
|
||||
_options = ModbusDriverFactoryExtensions.ParseOptions(_driverInstanceId, driverConfigJson);
|
||||
|
||||
_transport = _transportFactory(_options);
|
||||
await _transport.ConnectAsync(cancellationToken).ConfigureAwait(false);
|
||||
// Build the RawPath → definition table from the authored raw tags. Each entry's TagConfig
|
||||
|
||||
@@ -44,6 +44,23 @@ public static class ModbusDriverFactoryExtensions
|
||||
/// <param name="loggerFactory">Optional logger factory for creating loggers per driver instance.</param>
|
||||
/// <returns>The constructed <see cref="ModbusDriver"/> instance.</returns>
|
||||
public static ModbusDriver CreateInstance(string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory)
|
||||
{
|
||||
return new ModbusDriver(
|
||||
ParseOptions(driverInstanceId, driverConfigJson), driverInstanceId,
|
||||
transportFactory: null,
|
||||
logger: loggerFactory?.CreateLogger<ModbusDriver>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a Modbus <c>DriverConfig</c> JSON document into typed options. Extracted from
|
||||
/// <see cref="CreateInstance(string,string,ILoggerFactory?)"/> so <see cref="ModbusDriver.InitializeAsync"/>
|
||||
/// can re-parse a CHANGED config on reinitialize (Gitea #516) rather than serving the options it was
|
||||
/// constructed with — which silently discarded every edit while the deployment still sealed green.
|
||||
/// </summary>
|
||||
/// <param name="driverInstanceId">The unique identifier for the driver instance.</param>
|
||||
/// <param name="driverConfigJson">The JSON configuration string for the driver.</param>
|
||||
/// <returns>The parsed <see cref="ModbusDriverOptions"/>.</returns>
|
||||
internal static ModbusDriverOptions ParseOptions(string driverInstanceId, string driverConfigJson)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
|
||||
@@ -106,10 +123,7 @@ public static class ModbusDriverFactoryExtensions
|
||||
},
|
||||
};
|
||||
|
||||
return new ModbusDriver(
|
||||
options, driverInstanceId,
|
||||
transportFactory: null,
|
||||
logger: loggerFactory?.CreateLogger<ModbusDriver>());
|
||||
return options;
|
||||
}
|
||||
|
||||
private static T ParseEnum<T>(string? raw, string? tagName, string driverInstanceId, string field) where T : struct, Enum
|
||||
|
||||
Reference in New Issue
Block a user