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 }, }; } }