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);