fix(drivers): complete the Sql + FOCAS in-place config re-parse (#516)
The first #516 pass deliberately left these two out, because each derives more than options from config: Sql its ISqlDialect and resolved connection string, FOCAS the IFocasClientFactory its Backend key selects — all injected at construction. Adopting new options alone would poll a NEW tag set through the OLD database/backend, and a half-applied config change is worse than a discarded one because it looks like it worked. Rather than accept that exclusion, this fixes the blocker. Each factory now exposes a ParseBinding returning EVERY config-derived dependency as one value, and the driver adopts them atomically: - Sql: ApplyBinding takes (options, dialect, connectionString) and rebuilds everything downstream — provider factory, Endpoint, and the SqlPollReader that captures all three. It runs BEFORE BuildTagTable, so the tag table and the connection it is polled over always come from the same revision. A test-injected DbProviderFactory survives a rebind (_explicitFactory), so a re-derived dialect cannot displace what a test passed in. - FOCAS: options and backend move as a pair in InitializeAsync. Re-resolving Sql's connection string on reinit is a side benefit: a rotated credential is picked up without a process restart. Drivers constructed directly get no rebinder and keep their constructor binding, so every existing "{}"-passing lifecycle test is unaffected by construction rather than by luck. The tests pin ATOMICITY, not merely that a re-parse happened — a test checking only options would have passed against the broken version. Confirmed by simulating the half-fix (adopt options, skip the backend): 2 of 3 FOCAS tests go red. Reverting Sql's rebind turns its connection-string test red. Sql also asserts that a reinit which cannot resolve its new connection leaves the previous binding whole rather than half-adopting. All 12 drivers now honour a changed config in place. Sql.Tests 226, FOCAS.Tests 275 pass.
This commit is contained in:
@@ -47,6 +47,29 @@ public static class FocasDriverFactoryExtensions
|
||||
/// <param name="driverConfigJson">The driver configuration JSON string.</param>
|
||||
/// <returns>A configured <see cref="FocasDriver"/> instance.</returns>
|
||||
internal static FocasDriver CreateInstance(string driverInstanceId, string driverConfigJson)
|
||||
{
|
||||
var binding = ParseBinding(driverInstanceId, driverConfigJson);
|
||||
return new FocasDriver(
|
||||
binding.Options,
|
||||
driverInstanceId,
|
||||
binding.ClientFactory,
|
||||
// Hand the driver the SAME parse it was built with, so InitializeAsync can re-derive EVERY
|
||||
// config-derived dependency on a reinitialize — options AND the backend client factory
|
||||
// together (#516). Re-parsing options alone would run a new device/tag set against the old
|
||||
// backend, which is worse than not re-parsing at all.
|
||||
rebind: json => ParseBinding(driverInstanceId, json));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a FOCAS <c>DriverConfig</c> JSON document into <b>every</b> dependency the driver derives
|
||||
/// from config: the typed options and the <see cref="IFocasClientFactory"/> the <c>Backend</c> key
|
||||
/// selects. Returned together because they must be adopted together — see the <c>rebind</c> note in
|
||||
/// <see cref="CreateInstance"/>.
|
||||
/// </summary>
|
||||
/// <param name="driverInstanceId">The unique driver instance identifier.</param>
|
||||
/// <param name="driverConfigJson">The driver configuration JSON string.</param>
|
||||
/// <returns>The parsed options + backend factory.</returns>
|
||||
internal static FocasDriverBinding ParseBinding(string driverInstanceId, string driverConfigJson)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
|
||||
@@ -86,8 +109,7 @@ public static class FocasDriverFactoryExtensions
|
||||
HandleRecycle = BuildHandleRecycle(dto.HandleRecycle),
|
||||
};
|
||||
|
||||
var clientFactory = BuildClientFactory(dto, driverInstanceId);
|
||||
return new FocasDriver(options, driverInstanceId, clientFactory);
|
||||
return new FocasDriverBinding(options, BuildClientFactory(dto, driverInstanceId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -327,3 +349,14 @@ public static class FocasDriverFactoryExtensions
|
||||
public TimeSpan? Interval { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Everything <c>FocasDriver</c> derives from its <c>DriverConfig</c> JSON, returned as one value so a
|
||||
/// reinitialize adopts all of it atomically.
|
||||
/// <para>This exists because a partial adoption is a real hazard, not a theoretical one: the driver's
|
||||
/// <see cref="IFocasClientFactory"/> is chosen by the config's <c>Backend</c> key, so re-parsing
|
||||
/// <see cref="Options"/> alone would poll a NEW device/tag set through the OLD backend (Gitea #516).</para>
|
||||
/// </summary>
|
||||
/// <param name="Options">The typed driver options.</param>
|
||||
/// <param name="ClientFactory">The backend client factory the <c>Backend</c> key selects.</param>
|
||||
public sealed record FocasDriverBinding(FocasDriverOptions Options, IFocasClientFactory ClientFactory);
|
||||
|
||||
Reference in New Issue
Block a user