Driver-instance bootstrap pipeline (#248) — DriverInstance rows materialise as live IDriver instances #196

Merged
dohertj2 merged 2 commits from phase-7-fu-248-driver-bootstrap into v2 2026-04-20 22:52:14 -04:00
Owner

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 #240. Pre 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.

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 #240. Pre 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.
dohertj2 added 2 commits 2026-04-20 22:52:01 -04:00
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.
The previous commit (#248 wiring) inadvertently picked up
src/ZB.MOM.WW.OtOpcUa.Server/config_cache.db — generated by the live smoke
re-run that proved the bootstrapper works. Remove from tracking + ignore
going forward so future runs don't dirty the working tree.
dohertj2 merged commit 7e143e293b into v2 2026-04-20 22:52:14 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#196