chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)

Group all 69 projects into category subfolders under src/ and tests/ so the
Rider Solution Explorer mirrors the module structure. Folders: Core, Server,
Drivers (with a nested Driver CLIs subfolder), Client, Tooling.

- Move every project folder on disk with git mv (history preserved as renames).
- Recompute relative paths in 57 .csproj files: cross-category ProjectReferences,
  the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external
  mxaccessgw refs in Driver.Galaxy and its test project.
- Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders.
- Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL,
  integration, install).

Build green (0 errors); unit tests pass. Docs left for a separate pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-17 01:55:28 -04:00
parent 69f02fed7f
commit a25593a9c6
1044 changed files with 365 additions and 343 deletions

View File

@@ -0,0 +1,100 @@
using System.Text.Json;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
namespace ZB.MOM.WW.OtOpcUa.Server;
/// <summary>
/// Phase 6.1 Stream D consumption hook — bootstraps the node's current generation through
/// the <see cref="ResilientConfigReader"/> pipeline + writes every successful central-DB
/// read into the <see cref="GenerationSealedCache"/> so the next cache-miss path has a
/// sealed snapshot to fall back to.
/// </summary>
/// <remarks>
/// <para>Alongside the original <see cref="NodeBootstrap"/> (which uses the single-file
/// <see cref="ILocalConfigCache"/>). Program.cs can switch to this one once operators are
/// ready for the generation-sealed semantics. The original stays for backward compat
/// with the three integration tests that construct <see cref="NodeBootstrap"/> directly.</para>
///
/// <para>Closes release blocker #2 in <c>docs/v2/v2-release-readiness.md</c> — the
/// generation-sealed cache + resilient reader + stale-config flag ship as unit-tested
/// primitives in PR #81 but no production path consumed them until this wrapper.</para>
/// </remarks>
public sealed class SealedBootstrap
{
private readonly NodeOptions _options;
private readonly GenerationSealedCache _cache;
private readonly ResilientConfigReader _reader;
private readonly StaleConfigFlag _staleFlag;
private readonly ILogger<SealedBootstrap> _logger;
public SealedBootstrap(
NodeOptions options,
GenerationSealedCache cache,
ResilientConfigReader reader,
StaleConfigFlag staleFlag,
ILogger<SealedBootstrap> logger)
{
_options = options;
_cache = cache;
_reader = reader;
_staleFlag = staleFlag;
_logger = logger;
}
/// <summary>
/// Resolve the current generation for this node. Routes the central-DB fetch through
/// <see cref="ResilientConfigReader"/> (timeout → retry → fallback-to-cache) + seals a
/// fresh snapshot on every successful DB read so a future cache-miss has something to
/// serve.
/// </summary>
public async Task<BootstrapResult> LoadCurrentGenerationAsync(CancellationToken ct)
{
return await _reader.ReadAsync(
_options.ClusterId,
centralFetch: async innerCt => await FetchFromCentralAsync(innerCt).ConfigureAwait(false),
fromSnapshot: snap => BootstrapResult.FromCache(snap.GenerationId),
ct).ConfigureAwait(false);
}
private async ValueTask<BootstrapResult> FetchFromCentralAsync(CancellationToken ct)
{
await using var conn = new SqlConnection(_options.ConfigDbConnectionString);
await conn.OpenAsync(ct).ConfigureAwait(false);
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).ConfigureAwait(false);
if (!await reader.ReadAsync(ct).ConfigureAwait(false))
{
_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}; sealing snapshot", generationId);
// Seal a minimal snapshot with the generation pointer. A richer snapshot that carries
// the full sp_GetGenerationContent payload lands when the bootstrap flow grows to
// consume the content during offline operation (separate follow-up — see decision #148
// and phase-6-1 Stream D.3). The pointer alone is enough for the fallback path to
// surface the last-known-good generation id + flip UsingStaleConfig.
await _cache.SealAsync(new GenerationSnapshot
{
ClusterId = _options.ClusterId,
GenerationId = generationId,
CachedAt = DateTime.UtcNow,
PayloadJson = JsonSerializer.Serialize(new { generationId, source = "sp_GetCurrentGenerationForCluster" }),
}, ct).ConfigureAwait(false);
// StaleConfigFlag bookkeeping: ResilientConfigReader.MarkFresh on the returning call
// path; we're on the fresh branch so we don't touch the flag here.
_ = _staleFlag; // held so the field isn't flagged unused
return BootstrapResult.FromDb(generationId);
}
}