Driver-instance bootstrap pipeline (#248) — DriverInstance rows materialise as live IDriver instances
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.
This commit is contained in:
@@ -18,6 +18,7 @@ public sealed class OpcUaServerService(
|
||||
DriverHost driverHost,
|
||||
OpcUaApplicationHost applicationHost,
|
||||
DriverEquipmentContentRegistry equipmentContentRegistry,
|
||||
DriverInstanceBootstrapper driverBootstrapper,
|
||||
Phase7Composer phase7Composer,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<OpcUaServerService> logger) : BackgroundService
|
||||
@@ -37,6 +38,13 @@ public sealed class OpcUaServerService(
|
||||
// address space until the first publish, then the registry fills on next restart.
|
||||
if (result.GenerationId is { } gen)
|
||||
{
|
||||
// Task #248 — register IDriver instances from the published DriverInstance
|
||||
// rows BEFORE the equipment-content load + Phase 7 compose, so the rest of
|
||||
// the pipeline sees a populated DriverHost. Without this step Phase 7's
|
||||
// CachedTagUpstreamSource has no upstream feed + virtual-tag scripts read
|
||||
// BadNodeIdUnknown for every tag path (gap surfaced by task #240 smoke).
|
||||
await driverBootstrapper.RegisterDriversFromGenerationAsync(gen, stoppingToken);
|
||||
|
||||
await PopulateEquipmentContentAsync(gen, stoppingToken);
|
||||
|
||||
// Phase 7 follow-up #246 — load Script + VirtualTag + ScriptedAlarm rows,
|
||||
|
||||
Reference in New Issue
Block a user