feat(localdb): DriverHostActor stores applied artifacts in the pair-local cache

New Props/ctor parameter appended LAST at all three positions - the forwarding
list inside Props.Create is positional and compiles into an expression tree, so a
mis-ordered argument is usually type-compatible and binds the wrong dependency at
runtime. All 26 existing test construction sites use named arguments and needed no
change.

ReconcileDrivers now returns the blob it loaded. It catches its own DB failures and
returns without rethrowing, so ApplyAndAck can reach its success path having spawned
zero drivers; caching on 'the apply succeeded' would persist an empty artifact as
last-known-good and boot the node into an empty address space during the next
outage. Verified: weakening the guard to null-only turns EmptyArtifact_IsNotCached
red.

The store runs in its own try/catch because an Applied ACK has already been sent by
then - an escaping exception would land in ApplyAndAck's catch and emit a second,
contradictory Failed ACK for a deployment the fleet believes is live.
This commit is contained in:
Joseph Doherty
2026-07-20 11:06:57 -04:00
parent a38a52b831
commit 1becf59168
4 changed files with 402 additions and 7 deletions
@@ -2,6 +2,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.LocalDb;
using ZB.MOM.WW.LocalDb.Replication;
using ZB.MOM.WW.OtOpcUa.Runtime.DeploymentCache;
namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;
@@ -90,6 +91,11 @@ public static class LocalDbRegistration
services.AddZbLocalDb(configuration, LocalDbSetup.OnReady);
services.AddZbLocalDbReplication(configuration);
// The consumer-facing seam. WithOtOpcUaRuntimeActors resolves this optionally and threads it
// into DriverHostActor; registering the store without this leaves the cache tables present,
// replicating, and never written to.
services.AddSingleton<IDeploymentArtifactCache, LocalDbDeploymentArtifactCache>();
return services;
}
}