using Akka.Cluster.Hosting;
using Akka.Cluster.Hosting.SBR;
using Akka.Event;
using Akka.Hosting;
using Akka.Logger.Serilog;
using Akka.Remote.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
namespace ZB.MOM.WW.OtOpcUa.Cluster;
public static class ServiceCollectionExtensions
{
///
/// Binds and registers . The
/// actual ActorSystem + cluster bootstrap is layered on inside the host's AddAkka(...)
/// configurator via — keeping the entire Akka graph
/// under Akka.Hosting's management so cluster singletons land on the same ActorSystem.
///
/// The service collection to configure.
/// The application configuration containing cluster options.
/// The same service collection, for chaining.
public static IServiceCollection AddOtOpcUaCluster(this IServiceCollection services, IConfiguration configuration)
{
services.AddOptions()
.Bind(configuration.GetSection(AkkaClusterOptions.SectionName));
services.AddSingleton();
return services;
}
///
/// Configures the Akka.Hosting builder with the embedded OtOpcUa HOCON (split-brain resolver,
/// pinned dispatcher, failure detector tuning) + remote endpoint + cluster bootstrap derived
/// from .
///
/// Wire from Program.cs:
///
/// services.AddAkka("otopcua", (ab, sp) =>
/// {
/// ab.WithOtOpcUaClusterBootstrap(sp);
/// if (hasAdmin) ab.WithOtOpcUaControlPlaneSingletons();
/// if (hasDriver) ab.WithOtOpcUaRuntimeActors();
/// });
///
///
/// The Akka configuration builder to configure.
/// The service provider for resolving cluster options.
/// The same builder, for chaining.
public static AkkaConfigurationBuilder WithOtOpcUaClusterBootstrap(
this AkkaConfigurationBuilder builder,
IServiceProvider serviceProvider)
{
var options = serviceProvider.GetRequiredService>().Value;
builder.AddHocon(HoconLoader.LoadBaseConfig(), HoconAddMode.Append);
// Route Akka's internal ILoggingAdapter (DriverHostActor, DriverInstanceActor, cluster
// events, …) into Serilog so those logs reach the same sinks as the MEL/Serilog application
// logs. Akka.Hosting owns logger setup, so HOCON `akka.loggers` alone is not honored — the
// logger must be registered through ConfigureLoggers. Without this the actor graph logs only
// to the default StandardOutLogger (discarded under the Windows service host), which is why
// the driver-role actors were invisible during the 2026-06 data-plane investigation.
builder.ConfigureLoggers(setup =>
{
setup.LogLevel = LogLevel.DebugLevel;
setup.ClearLoggers();
setup.AddLogger();
});
builder.WithRemoting(new RemoteOptions
{
HostName = options.Hostname,
Port = options.Port,
PublicHostName = options.PublicHostname,
});
builder.WithClustering(BuildClusterOptions(options));
return builder;
}
///
/// Builds the for the fused-host cluster, including the
/// activating split-brain-resolver registration.
///
/// The split-brain-resolver HOCON block in Resources/akka.conf is inert on its own —
/// Akka.NET only runs the resolver when a downing provider is registered, otherwise the cluster
/// falls back to NoDowning and a hard-crashed node is never downed (singletons and the
/// driver role-leader never fail over, and a partition leaves both redundancy sides at
/// ServiceLevel 240 forever). Setting is what
/// activates Akka.Cluster.SBR.SplitBrainResolverProvider under the hood — this is the piece
/// that was missing (arch-review 03/S1).
///
/// with DownIfAlone=true mirrors the HOCON intent and is the
/// correct strategy for a 2-node warm-redundancy pair: on an even split the oldest member (typically
/// the long-running primary) survives, and down-if-alone downs a singleton that loses its
/// peer. keep-majority/static-quorum are wrong for two nodes. The strategy + down-if-alone
/// here MUST stay consistent with the HOCON block; stable-after lives only in HOCON because the
/// typed option cannot express it (it must stay ≥ failure-detector.acceptable-heartbeat-pause).
///
/// The bound cluster options carrying seed nodes and roles.
/// The cluster options with seed nodes, roles, and the activated split-brain resolver.
public static ClusterOptions BuildClusterOptions(AkkaClusterOptions options)
{
return new ClusterOptions
{
SeedNodes = options.SeedNodes,
Roles = options.Roles,
SplitBrainResolver = new KeepOldestOption { DownIfAlone = true },
};
}
}