Some checks failed
v2-ci / build (push) Failing after 45s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (push) Has been skipped
Wires the OPC UA SDK into the fused Host's lifecycle on driver-role
nodes + spawns OpcUaPublishActor with the proper sink/publisher/dbFactory/
applier resolution. The full read+write data path is now live in
production: Deploy → DriverHost → OpcUaPublish → SDK NodeManager →
subscribed OPC UA clients.
DeferredAddressSpaceSink (Commons.OpcUa):
- Thread-safe wrapper IOpcUaAddressSpaceSink that delegates to an
inner sink swapped in at runtime. Needed because Akka actors
resolve the sink at construction time, but the production sink
(SdkAddressSpaceSink wrapping OtOpcUaNodeManager) only exists
after the SDK StandardServer has started.
- Defaults to NullOpcUaAddressSpaceSink so calls before swap are
safe; SetSink(null) reverts (for graceful shutdown).
OtOpcUaServerHostedService (Host.OpcUa):
- IHostedService that owns the OPC UA SDK lifecycle. Reads
OpcUaApplicationHostOptions from the 'OpcUa' config section,
creates an OtOpcUaSdkServer, boots it through OpcUaApplicationHost,
then swaps a real SdkAddressSpaceSink into the DeferredAddressSpaceSink
singleton.
- SDK boot failure is logged + non-fatal — the rest of the host
(admin UI, driver actors) keeps running. Stop reverts to null sink.
WithOtOpcUaRuntimeActors (Runtime):
- Now spawns OpcUaPublishActor (new actor) + threads its ActorRef
into DriverHostActor's Props so successful applies trigger the
address-space rebuild pipeline.
- Phase7Applier is constructed here from the resolved sink + a
logger; OpcUaPublishActor takes both.
- Prepends the opcua-synchronized-dispatcher HOCON so the extension
is self-contained — consumers (Host, tests) don't need to redeclare
the dispatcher block.
- New OpcUaPublishActorKey + OpcUaPublishActorName for actor-registry
resolution.
- AddOtOpcUaRuntime now also TryAddSingleton's NullOpcUaAddressSpaceSink
+ NullServiceLevelPublisher so admin-only nodes (or tests that
don't bind the Deferred sink) stay safe.
Host.Program.cs (driver-role only):
- Binds DeferredAddressSpaceSink as singleton + as IOpcUaAddressSpaceSink
- AddHostedService<OtOpcUaServerHostedService>()
Tests: OpcUaServer 24 -> 28 (+4 DeferredAddressSpaceSink unit tests),
Runtime 69 -> 69 (existing ServiceCollectionExtensionsTests extended
to verify the new mux + publish actor registration).
All 6 v2 test suites green: 177 tests passing.
Closes #108. Engine-wiring is now production-bound end-to-end on
driver-role nodes — Deploy reaches real OPC UA Variable nodes that
subscribed clients see.
95 lines
4.1 KiB
C#
95 lines
4.1 KiB
C#
using Akka.Hosting;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests;
|
|
|
|
/// <summary>
|
|
/// Verifies <c>WithOtOpcUaRuntimeActors</c> spawns <c>DriverHostActor</c> + <c>DbHealthProbeActor</c>
|
|
/// on the host's <c>ActorSystem</c> and registers both under their marker keys. This is the
|
|
/// driver-role mirror of the admin-role <c>WithOtOpcUaControlPlaneSingletons</c> bootstrap.
|
|
/// </summary>
|
|
public sealed class ServiceCollectionExtensionsTests
|
|
{
|
|
[Fact]
|
|
public async Task WithOtOpcUaRuntimeActors_spawns_driver_host_and_db_health_probe()
|
|
{
|
|
using var host = Host.CreateDefaultBuilder()
|
|
.ConfigureServices((_, services) =>
|
|
{
|
|
services.AddSingleton<IDbContextFactory<OtOpcUaConfigDbContext>>(
|
|
new InMemoryConfigDbFactory(Guid.NewGuid().ToString("N")));
|
|
services.AddSingleton<IClusterRoleInfo>(new FakeClusterRoleInfo());
|
|
|
|
services.AddAkka("otopcua-test", (ab, _) =>
|
|
{
|
|
ab.AddHocon(@"
|
|
akka.actor.provider = ""Akka.Cluster.ClusterActorRefProvider, Akka.Cluster""
|
|
akka.remote.dot-netty.tcp.hostname = ""127.0.0.1""
|
|
akka.remote.dot-netty.tcp.port = 0
|
|
akka.cluster.seed-nodes = []
|
|
akka.cluster.roles = [""driver""]
|
|
", HoconAddMode.Prepend);
|
|
ab.WithOtOpcUaRuntimeActors();
|
|
});
|
|
})
|
|
.Build();
|
|
|
|
await host.StartAsync();
|
|
try
|
|
{
|
|
var driverHost = host.Services.GetRequiredService<IRequiredActor<DriverHostActorKey>>();
|
|
var dbHealth = host.Services.GetRequiredService<IRequiredActor<DbHealthProbeActorKey>>();
|
|
var historian = host.Services.GetRequiredService<IRequiredActor<HistorianAdapterActorKey>>();
|
|
var mux = host.Services.GetRequiredService<IRequiredActor<DependencyMuxActorKey>>();
|
|
var publish = host.Services.GetRequiredService<IRequiredActor<OpcUaPublishActorKey>>();
|
|
|
|
driverHost.ActorRef.ShouldNotBeNull();
|
|
dbHealth.ActorRef.ShouldNotBeNull();
|
|
historian.ActorRef.ShouldNotBeNull();
|
|
mux.ActorRef.ShouldNotBeNull();
|
|
publish.ActorRef.ShouldNotBeNull();
|
|
driverHost.ActorRef.Path.Name.ShouldBe(ServiceCollectionExtensions.DriverHostActorName);
|
|
dbHealth.ActorRef.Path.Name.ShouldBe(ServiceCollectionExtensions.DbHealthProbeActorName);
|
|
historian.ActorRef.Path.Name.ShouldBe(ServiceCollectionExtensions.HistorianAdapterActorName);
|
|
mux.ActorRef.Path.Name.ShouldBe(ServiceCollectionExtensions.DependencyMuxActorName);
|
|
publish.ActorRef.Path.Name.ShouldBe(ServiceCollectionExtensions.OpcUaPublishActorName);
|
|
}
|
|
finally
|
|
{
|
|
await host.StopAsync();
|
|
}
|
|
}
|
|
|
|
private sealed class InMemoryConfigDbFactory(string dbName) : IDbContextFactory<OtOpcUaConfigDbContext>
|
|
{
|
|
public OtOpcUaConfigDbContext CreateDbContext()
|
|
{
|
|
var opts = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
|
|
.UseInMemoryDatabase(dbName)
|
|
.Options;
|
|
return new OtOpcUaConfigDbContext(opts);
|
|
}
|
|
}
|
|
|
|
private sealed class FakeClusterRoleInfo : IClusterRoleInfo
|
|
{
|
|
public NodeId LocalNode { get; } = NodeId.Parse("test-node");
|
|
public IReadOnlySet<string> LocalRoles { get; } = new HashSet<string>(["driver"]);
|
|
public bool HasRole(string role) => LocalRoles.Contains(role);
|
|
public IReadOnlyList<NodeId> MembersWithRole(string role) => Array.Empty<NodeId>();
|
|
public NodeId? RoleLeader(string role) => null;
|
|
public event EventHandler<RoleLeaderChangedEventArgs>? RoleLeaderChanged
|
|
{
|
|
add { _ = value; }
|
|
remove { _ = value; }
|
|
}
|
|
}
|
|
}
|