65 lines
2.8 KiB
C#
65 lines
2.8 KiB
C#
using Microsoft.Data.SqlClient;
|
|
using Microsoft.Extensions.Logging;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Server;
|
|
|
|
/// <summary>
|
|
/// Bootstraps a node: fetches the current generation from the central DB via
|
|
/// <c>sp_GetCurrentGenerationForCluster</c>. If the DB is unreachable and a LiteDB cache entry
|
|
/// exists, falls back to cached config per decision #79 (degraded-but-running).
|
|
/// </summary>
|
|
public sealed class NodeBootstrap(
|
|
NodeOptions options,
|
|
ILocalConfigCache localCache,
|
|
ILogger<NodeBootstrap> logger)
|
|
{
|
|
public async Task<BootstrapResult> 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);
|