using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
namespace ZB.MOM.WW.OtOpcUa.Server;
///
/// Bootstraps a node: fetches the current generation from the central DB via
/// sp_GetCurrentGenerationForCluster. If the DB is unreachable and a LiteDB cache entry
/// exists, falls back to cached config per decision #79 (degraded-but-running).
///
public sealed class NodeBootstrap(
NodeOptions options,
ILocalConfigCache localCache,
ILogger logger)
{
public async Task LoadCurrentGenerationAsync(CancellationToken ct)
{
try
{
await using var conn = new SqlConnection(options.ConfigDbConnectionString);
await conn.OpenAsync(ct);
await using var cmd = conn.CreateCommand();
cmd.CommandText = "EXEC dbo.sp_GetCurrentGenerationForCluster @NodeId=@n, @ClusterId=@c";
cmd.Parameters.AddWithValue("@n", options.NodeId);
cmd.Parameters.AddWithValue("@c", options.ClusterId);
await using var reader = await cmd.ExecuteReaderAsync(ct);
if (!await reader.ReadAsync(ct))
{
logger.LogWarning("Cluster {Cluster} has no Published generation yet", options.ClusterId);
return BootstrapResult.EmptyFromDb();
}
var generationId = reader.GetInt64(0);
logger.LogInformation("Bootstrapped from central DB: generation {GenerationId}", generationId);
return BootstrapResult.FromDb(generationId);
}
catch (Exception ex) when (ex is SqlException or InvalidOperationException or TimeoutException)
{
logger.LogWarning(ex, "Central DB unreachable; trying LiteDB cache fallback (decision #79)");
var cached = await localCache.GetMostRecentAsync(options.ClusterId, ct);
if (cached is null)
throw new BootstrapException(
"Central DB unreachable and no local cache available — cannot bootstrap.", ex);
logger.LogWarning("Bootstrapping from cache: generation {GenerationId} cached at {At}",
cached.GenerationId, cached.CachedAt);
return BootstrapResult.FromCache(cached.GenerationId);
}
}
}
public sealed record BootstrapResult(long? GenerationId, BootstrapSource Source)
{
public static BootstrapResult FromDb(long g) => new(g, BootstrapSource.CentralDb);
public static BootstrapResult FromCache(long g) => new(g, BootstrapSource.LocalCache);
public static BootstrapResult EmptyFromDb() => new(null, BootstrapSource.CentralDb);
}
public enum BootstrapSource { CentralDb, LocalCache }
public sealed class BootstrapException(string message, Exception inner) : Exception(message, inner);