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:
@@ -81,6 +81,43 @@ public static class SqlDriverFactoryExtensions
|
||||
public static SqlDriver CreateInstance(string driverInstanceId, string driverConfigJson)
|
||||
=> CreateInstance(driverInstanceId, driverConfigJson, loggerFactory: null);
|
||||
|
||||
/// <summary>
|
||||
/// Parses a Sql <c>DriverConfig</c> JSON document into <b>every</b> dependency the driver derives
|
||||
/// from config: the typed options, the <see cref="ISqlDialect"/> the <c>provider</c> key selects, and
|
||||
/// the connection string resolved from <c>connectionStringRef</c>.
|
||||
/// <para>Returned together because they must be adopted together. Re-parsing options alone on a
|
||||
/// reinitialize would run a NEW tag set against the OLD connection and dialect (Gitea #516) — a
|
||||
/// half-applied config change is worse than a discarded one, because it looks like it worked.</para>
|
||||
/// <para>Re-running this on reinitialize also re-resolves the connection string from its environment
|
||||
/// variable, so a rotated credential is picked up without a process restart.</para>
|
||||
/// </summary>
|
||||
/// <param name="driverInstanceId">The central config DB's identity for this driver instance.</param>
|
||||
/// <param name="driverConfigJson">The driver configuration JSON.</param>
|
||||
/// <returns>The parsed options, dialect and resolved connection string.</returns>
|
||||
/// <exception cref="InvalidOperationException">The config is malformed or names no connection string.</exception>
|
||||
internal static SqlDriverBinding ParseBinding(string driverInstanceId, string driverConfigJson)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
|
||||
|
||||
var dto = Deserialize(driverInstanceId, driverConfigJson);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(dto.ConnectionStringRef))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Sql driver config for '{driverInstanceId}' is missing the required connectionStringRef. " +
|
||||
$"Author the NAME of a connection string (the value itself is supplied out-of-band through " +
|
||||
$"the '{SqlConnectionStringResolver.EnvironmentVariablePrefix}<ref>' environment variable).");
|
||||
}
|
||||
|
||||
var dialect = CreateDialect(dto.Provider, driverInstanceId);
|
||||
|
||||
// Resolved BEFORE the knob validation in CreateInstance, so that every subsequent throw is a path
|
||||
// that HAS the secret in hand — which is exactly where a careless interpolation would leak it.
|
||||
var connectionString = SqlConnectionStringResolver.Resolve(dto.ConnectionStringRef!);
|
||||
return new SqlDriverBinding(BuildOptions(dto, driverInstanceId), dialect, connectionString, dto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logger-aware overload — used by <see cref="Register"/>'s closure when wired through DI.
|
||||
/// <para>Constructs the driver only: the driver builds its own <see cref="SqlPollReader"/>, because
|
||||
@@ -102,25 +139,11 @@ public static class SqlDriverFactoryExtensions
|
||||
public static SqlDriver CreateInstance(
|
||||
string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
|
||||
|
||||
var dto = Deserialize(driverInstanceId, driverConfigJson);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(dto.ConnectionStringRef))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Sql driver config for '{driverInstanceId}' is missing the required connectionStringRef. " +
|
||||
$"Author the NAME of a connection string (the value itself is supplied out-of-band through " +
|
||||
$"the '{SqlConnectionStringResolver.EnvironmentVariablePrefix}<ref>' environment variable).");
|
||||
}
|
||||
|
||||
var dialect = CreateDialect(dto.Provider, driverInstanceId);
|
||||
|
||||
// Resolved BEFORE the knob validation below, so that every subsequent throw is a path that HAS the
|
||||
// secret in hand — which is exactly where a careless interpolation would leak it.
|
||||
var connectionString = SqlConnectionStringResolver.Resolve(dto.ConnectionStringRef!);
|
||||
var options = BuildOptions(dto, driverInstanceId);
|
||||
var binding = ParseBinding(driverInstanceId, driverConfigJson);
|
||||
var dto = binding.Dto;
|
||||
var dialect = binding.Dialect;
|
||||
var connectionString = binding.ConnectionString;
|
||||
var options = binding.Options;
|
||||
|
||||
var logger = (ILogger?)loggerFactory?.CreateLogger(typeof(SqlDriverFactoryExtensions).FullName!)
|
||||
?? NullLogger.Instance;
|
||||
@@ -143,7 +166,11 @@ public static class SqlDriverFactoryExtensions
|
||||
dialect,
|
||||
connectionString,
|
||||
factory: null,
|
||||
logger: loggerFactory?.CreateLogger<SqlDriver>());
|
||||
logger: loggerFactory?.CreateLogger<SqlDriver>(),
|
||||
// Hand the driver the SAME parse it was built with, so InitializeAsync can re-derive options,
|
||||
// dialect and connection string together on a reinitialize (#516). Re-resolving the connection
|
||||
// string also picks up a rotated credential without a process restart.
|
||||
rebind: json => ParseBinding(driverInstanceId, json));
|
||||
|
||||
// Endpoint is the credential-free server/database rendering — the ONLY form of the connection string
|
||||
// permitted outside the provider call.
|
||||
@@ -250,3 +277,21 @@ public static class SqlDriverFactoryExtensions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Everything <c>SqlDriver</c> derives from its <c>DriverConfig</c> JSON, returned as one value so a
|
||||
/// reinitialize adopts all of it atomically.
|
||||
/// <para>Partial adoption is the hazard this type exists to prevent: the dialect and the resolved
|
||||
/// connection string are both config-derived, so adopting new <see cref="Options"/> alone would poll a
|
||||
/// NEW tag set against the OLD database (Gitea #516).</para>
|
||||
/// </summary>
|
||||
/// <param name="Options">The typed driver options.</param>
|
||||
/// <param name="Dialect">The provider seam the <c>provider</c> key selects.</param>
|
||||
/// <param name="ConnectionString">The connection string resolved from <c>connectionStringRef</c>. Never logged.</param>
|
||||
/// <param name="Dto">The raw parsed DTO, so a caller needing a key the options do not carry (e.g.
|
||||
/// <c>allowWrites</c>, which the driver ignores but warns about) need not re-deserialize.</param>
|
||||
public sealed record SqlDriverBinding(
|
||||
SqlDriverOptions Options,
|
||||
ISqlDialect Dialect,
|
||||
string ConnectionString,
|
||||
SqlDriverConfigDto Dto);
|
||||
|
||||
Reference in New Issue
Block a user