using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using ZB.MOM.WW.Secrets.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Host.Configuration; /// /// Forces this node's secret-replication actor into existence at startup. /// /// /// /// The replication actor is created lazily, on the first resolution of /// — resolving the store builds ReplicatingSecretStore, /// which takes ISecretReplicator, whose factory touches /// SecretReplicationActorProvider.ActorRef and thereby spawns the actor. /// /// /// Laziness is correct for the library (the ActorSystem is usually registered after /// the replication call), but it makes participation in anti-entropy accidental: a node that /// happens never to read or write a secret — a plausible steady state for a driver-role node /// whose driver configs carry no ${secret:} references — would never create the actor, /// never announce its manifest, and never converge. Nothing would fail; it would just /// silently not replicate. Resolving the store once at startup makes participation /// unconditional. /// /// /// History — this hook has met two library defects, both fixed. Against 0.2.0 it was /// inert: the package never bound its own ISecretReplicator (TryAdd registration /// order), so this resolve built a ReplicatingSecretStore around a no-op sink and /// spawned no actor. Against 0.2.1 it was worse: the resolve deadlocked the host at /// startup — the package's DI graph had a circular singleton dependency, invisible to /// the container through factory lambdas (scadaproj#1). Fixed in 0.2.2 by deferring the /// cycle-closing invalidator edge; /// SecretsReplicationRegistrationTests.The_startup_hook_actually_creates_the_replication_actor /// exercises this hook against a built provider on a real single-node cluster. /// /// /// Root provider used to resolve the (decorated) secret store exactly once. public sealed class SecretReplicationStarter(IServiceProvider services) : IHostedService { /// Resolves , which spawns the replication actor. /// Unused — resolution is synchronous and non-blocking. /// A completed task. public Task StartAsync(CancellationToken cancellationToken) { // The resolution itself is the side effect; the instance is deliberately unused. _ = services.GetRequiredService(); return Task.CompletedTask; } /// No-op: the actor's lifetime is the actor system's. /// Unused. /// A completed task. public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; }