feat(runtime): WithOtOpcUaRuntimeActors extension for driver-role node startup (F19)
Mirrors WithOtOpcUaControlPlaneSingletons for the driver role. Spawns DriverHostActor + DbHealthProbeActor on the host's ActorSystem and registers both under marker keys. Host's Program.cs now calls it when the node carries the driver role, so driver-only and admin+driver deployments both auto-bootstrap the per-node actors. Integration test covers the registration round-trip via Microsoft.Extensions.Hosting + Akka.Hosting AddAkka.
This commit is contained in:
@@ -8,6 +8,7 @@ using ZB.MOM.WW.OtOpcUa.Configuration;
|
|||||||
using ZB.MOM.WW.OtOpcUa.ControlPlane;
|
using ZB.MOM.WW.OtOpcUa.ControlPlane;
|
||||||
using ZB.MOM.WW.OtOpcUa.Host;
|
using ZB.MOM.WW.OtOpcUa.Host;
|
||||||
using ZB.MOM.WW.OtOpcUa.Host.Health;
|
using ZB.MOM.WW.OtOpcUa.Host.Health;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Runtime;
|
||||||
using ZB.MOM.WW.OtOpcUa.Security;
|
using ZB.MOM.WW.OtOpcUa.Security;
|
||||||
using ZB.MOM.WW.OtOpcUa.Security.Endpoints;
|
using ZB.MOM.WW.OtOpcUa.Security.Endpoints;
|
||||||
|
|
||||||
@@ -43,10 +44,8 @@ builder.Services.AddAkka("otopcua", (ab, _) =>
|
|||||||
{
|
{
|
||||||
if (hasAdmin)
|
if (hasAdmin)
|
||||||
ab.WithOtOpcUaControlPlaneSingletons();
|
ab.WithOtOpcUaControlPlaneSingletons();
|
||||||
// Driver-role startup (DriverHostActor spawn + child probes) is wired in F19 once a
|
if (hasDriver)
|
||||||
// RuntimeStartup contract is added — the actor itself exists (Phase 6), the registration
|
ab.WithOtOpcUaRuntimeActors();
|
||||||
// extension does not yet. Without it, driver-role nodes still join the cluster and serve
|
|
||||||
// health/redundancy traffic but won't auto-spawn DriverHostActor.
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasAdmin)
|
if (hasAdmin)
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using Akka.Actor;
|
||||||
|
using Akka.Hosting;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Runtime.Health;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Runtime;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public const string DriverRole = "driver";
|
||||||
|
|
||||||
|
public const string DriverHostActorName = "driver-host";
|
||||||
|
public const string DbHealthProbeActorName = "db-health";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns the per-node driver-role actors on the host's <see cref="ActorSystem"/>:
|
||||||
|
/// <see cref="DriverHostActor"/> (one per node) and <see cref="DbHealthProbeActor"/>
|
||||||
|
/// (consumed by the health endpoint + redundancy calc).
|
||||||
|
///
|
||||||
|
/// Mirror of <c>WithOtOpcUaControlPlaneSingletons</c> for the driver role. Both must
|
||||||
|
/// be registered on the same <see cref="AkkaConfigurationBuilder"/> as the cluster
|
||||||
|
/// bootstrap so the actors share the host's ActorSystem.
|
||||||
|
///
|
||||||
|
/// Wire from the fused Host's Program.cs when the node carries the <c>driver</c> role:
|
||||||
|
/// <code>
|
||||||
|
/// if (hasDriver)
|
||||||
|
/// ab.WithOtOpcUaRuntimeActors();
|
||||||
|
/// </code>
|
||||||
|
/// </summary>
|
||||||
|
public static AkkaConfigurationBuilder WithOtOpcUaRuntimeActors(this AkkaConfigurationBuilder builder)
|
||||||
|
{
|
||||||
|
builder.WithActors((system, registry, resolver) =>
|
||||||
|
{
|
||||||
|
var dbFactory = resolver.GetService<IDbContextFactory<OtOpcUaConfigDbContext>>();
|
||||||
|
var roleInfo = resolver.GetService<IClusterRoleInfo>();
|
||||||
|
|
||||||
|
var dbHealth = system.ActorOf(
|
||||||
|
DbHealthProbeActor.Props(dbFactory),
|
||||||
|
DbHealthProbeActorName);
|
||||||
|
registry.Register<DbHealthProbeActorKey>(dbHealth);
|
||||||
|
|
||||||
|
var driverHost = system.ActorOf(
|
||||||
|
DriverHostActor.Props(dbFactory, roleInfo.LocalNode),
|
||||||
|
DriverHostActorName);
|
||||||
|
registry.Register<DriverHostActorKey>(driverHost);
|
||||||
|
});
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Marker key types used by <c>Akka.Hosting</c> to resolve runtime actors from the registry.</summary>
|
||||||
|
public sealed class DriverHostActorKey { }
|
||||||
|
public sealed class DbHealthProbeActorKey { }
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
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>>();
|
||||||
|
|
||||||
|
driverHost.ActorRef.ShouldNotBeNull();
|
||||||
|
dbHealth.ActorRef.ShouldNotBeNull();
|
||||||
|
driverHost.ActorRef.Path.Name.ShouldBe(ServiceCollectionExtensions.DriverHostActorName);
|
||||||
|
dbHealth.ActorRef.Path.Name.ShouldBe(ServiceCollectionExtensions.DbHealthProbeActorName);
|
||||||
|
}
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user