Closes the gap surfaced by Phase 7 live smoke (#240): DriverInstance rows in the central config DB had no path to materialise as live IDriver instances in DriverHost, so virtual-tag scripts read BadNodeIdUnknown for every tag. ## DriverFactoryRegistry (Core.Hosting) Process-singleton type-name → factory map. Each driver project's static Register call pre-loads its factory at Program.cs startup; the bootstrapper looks up by DriverInstance.DriverType + invokes with (DriverInstanceId, DriverConfig JSON). Case-insensitive; duplicate-type registration throws. ## GalaxyProxyDriverFactoryExtensions.Register (Driver.Galaxy.Proxy) Static helper — no Microsoft.Extensions.DependencyInjection dep, keeps the driver project free of DI machinery. Parses DriverConfig JSON for PipeName + SharedSecret + ConnectTimeoutMs. DriverInstanceId from the row wins over JSON per the schema's UX_DriverInstance_Generation_LogicalId. ## DriverInstanceBootstrapper (Server) After NodeBootstrap loads the published generation: queries DriverInstance rows scoped to that generation, looks up the factory per row, constructs + DriverHost.RegisterAsync (which calls InitializeAsync). Per plan decision #12 (driver isolation), failure of one driver doesn't prevent others — logs ERR + continues + returns the count actually registered. Unknown DriverType (factory not registered) logs WRN + skips so a missing-assembly deployment doesn't take down the whole server. ## Wired into OpcUaServerService.ExecuteAsync After NodeBootstrap.LoadCurrentGenerationAsync, before PopulateEquipmentContentAsync + Phase7Composer.PrepareAsync. The Phase 7 chain now sees a populated DriverHost so CachedTagUpstreamSource has an upstream feed. ## Live evidence on the dev box Re-ran the Phase 7 smoke from task #240. Pre-#248 vs post-#248: Equipment namespace snapshots loaded for 0/0 driver(s) ← before Equipment namespace snapshots loaded for 1/1 driver(s) ← after Galaxy.Host pipe ACL denied our SID (env-config issue documented in docs/ServiceHosting.md, NOT a code issue) — the bootstrapper logged it as "failed to initialize, driver state will reflect Faulted" and continued past the failure exactly per plan #12. The rest of the pipeline (Equipment walker + Phase 7 composer) ran to completion. ## Tests — 5 new DriverFactoryRegistryTests Register + TryGet round-trip, case-insensitive lookup, duplicate-type throws, null-arg guards, RegisteredTypes snapshot. Pure functions; no DI/DB needed. The bootstrapper's DB-query path is exercised by the live smoke (#240) which operators run before each release.
89 lines
3.9 KiB
C#
89 lines
3.9 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Server;
|
|
|
|
/// <summary>
|
|
/// Task #248 — bridges the gap surfaced by the Phase 7 live smoke (#240) where
|
|
/// <c>DriverInstance</c> rows in the central config DB had no path to materialise
|
|
/// as live <see cref="Core.Abstractions.IDriver"/> instances in <see cref="DriverHost"/>.
|
|
/// Called from <c>OpcUaServerService.ExecuteAsync</c> after the bootstrap loads
|
|
/// the published generation, before address-space build.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Per row: looks up the <c>DriverType</c> string in
|
|
/// <see cref="DriverFactoryRegistry"/>, calls the factory with the row's
|
|
/// <c>DriverInstanceId</c> + <c>DriverConfig</c> JSON to construct an
|
|
/// <see cref="Core.Abstractions.IDriver"/>, then registers via
|
|
/// <see cref="DriverHost.RegisterAsync"/> which invokes <c>InitializeAsync</c>
|
|
/// under the host's lifecycle semantics.
|
|
/// </para>
|
|
/// <para>
|
|
/// Unknown <c>DriverType</c> = factory not registered = log a warning and skip.
|
|
/// Per plan decision #12 (driver isolation), failure to construct or initialize
|
|
/// one driver doesn't prevent the rest from coming up — the Server keeps serving
|
|
/// the others' subtrees + the operator can fix the misconfigured row + republish
|
|
/// to retry.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class DriverInstanceBootstrapper(
|
|
DriverFactoryRegistry factories,
|
|
DriverHost driverHost,
|
|
IServiceScopeFactory scopeFactory,
|
|
ILogger<DriverInstanceBootstrapper> logger)
|
|
{
|
|
public async Task<int> RegisterDriversFromGenerationAsync(long generationId, CancellationToken ct)
|
|
{
|
|
using var scope = scopeFactory.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<OtOpcUaConfigDbContext>();
|
|
|
|
var rows = await db.DriverInstances.AsNoTracking()
|
|
.Where(d => d.GenerationId == generationId && d.Enabled)
|
|
.ToListAsync(ct).ConfigureAwait(false);
|
|
|
|
var registered = 0;
|
|
var skippedUnknownType = 0;
|
|
var failedInit = 0;
|
|
|
|
foreach (var row in rows)
|
|
{
|
|
var factory = factories.TryGet(row.DriverType);
|
|
if (factory is null)
|
|
{
|
|
logger.LogWarning(
|
|
"DriverInstance {Id} skipped — DriverType '{Type}' has no registered factory (known: {Known})",
|
|
row.DriverInstanceId, row.DriverType, string.Join(",", factories.RegisteredTypes));
|
|
skippedUnknownType++;
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
var driver = factory(row.DriverInstanceId, row.DriverConfig);
|
|
await driverHost.RegisterAsync(driver, row.DriverConfig, ct).ConfigureAwait(false);
|
|
registered++;
|
|
logger.LogInformation(
|
|
"DriverInstance {Id} ({Type}) registered + initialized", row.DriverInstanceId, row.DriverType);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Plan decision #12 — driver isolation. Log + continue so one bad row
|
|
// doesn't deny the OPC UA endpoint to the rest of the fleet.
|
|
logger.LogError(ex,
|
|
"DriverInstance {Id} ({Type}) failed to initialize — driver state will reflect Faulted; operator can republish to retry",
|
|
row.DriverInstanceId, row.DriverType);
|
|
failedInit++;
|
|
}
|
|
}
|
|
|
|
logger.LogInformation(
|
|
"DriverInstanceBootstrapper: gen={Gen} registered={Registered} skippedUnknownType={Skipped} failedInit={Failed}",
|
|
generationId, registered, skippedUnknownType, failedInit);
|
|
return registered;
|
|
}
|
|
}
|