2254ae3dea
Bumps the four ZB.MOM.WW.Secrets pins 0.2.1 -> 0.2.2 (closes the OtOpcUa side of scadaproj#1, tracked here as #482). 0.2.1's Akka replicator deadlocked any hosted process at startup when Secrets:Replication:Enabled was true: the package's DI graph closed a circular singleton dependency through factory lambdas (store decorator -> replicator -> actor provider -> cache invalidator -> resolver -> store), which MS.DI's StackGuard turns into a silent cross-thread call-site-lock deadlock. 0.2.2 defers the invalidator edge to first eviction. The flag stays default-false; enabling remains a per-environment decision. The_startup_hook_actually_creates_the_replication_actor is now a real test: SecretReplicationStarter's docs had promised it since the adoption, and the upstream fix finally makes a provider-based resolve runnable - container built exactly as the host does, hook started under a watchdog, replication actor proven to exist by ActorSelection on a self-joined single-node cluster (no TestKit needed, which matters because Akka.TestKit.Xunit2 is xunit-v2-only and this project is on xunit.v3). Also corrects the stale rationale that blamed the old hang on DistributedPubSub needing a joined cluster - the actor constructor was never reached; it was the DI cycle. Verified: SecretsReplicationRegistrationTests 8/8 on the 0.2.2 feed packages; full slnx build 0 errors; the 2-node Akka live convergence gate re-run against the published 0.2.2 packages passes 6/6 (write->peer, tombstone propagation without resurrection, delete visibility through the resolver cache, reverse direction, wrong-KEK fail-closed). Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
56 lines
3.1 KiB
C#
56 lines
3.1 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using ZB.MOM.WW.Secrets.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
|
|
|
/// <summary>
|
|
/// Forces this node's secret-replication actor into existence at startup.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The replication actor is created <b>lazily</b>, on the first resolution of
|
|
/// <see cref="ISecretStore"/> — resolving the store builds <c>ReplicatingSecretStore</c>,
|
|
/// which takes <c>ISecretReplicator</c>, whose factory touches
|
|
/// <c>SecretReplicationActorProvider.ActorRef</c> and thereby spawns the actor.
|
|
/// </para>
|
|
/// <para>
|
|
/// Laziness is correct for the library (the <c>ActorSystem</c> 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 <c>${secret:}</c> 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.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>History — this hook has met two library defects, both fixed.</b> Against 0.2.0 it was
|
|
/// inert: the package never bound its own <c>ISecretReplicator</c> (TryAdd registration
|
|
/// order), so this resolve built a <c>ReplicatingSecretStore</c> around a no-op sink and
|
|
/// spawned no actor. Against 0.2.1 it was worse: the resolve <b>deadlocked the host at
|
|
/// startup</b> — 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;
|
|
/// <c>SecretsReplicationRegistrationTests.The_startup_hook_actually_creates_the_replication_actor</c>
|
|
/// exercises this hook against a built provider on a real single-node cluster.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <param name="services">Root provider used to resolve the (decorated) secret store exactly once.</param>
|
|
public sealed class SecretReplicationStarter(IServiceProvider services) : IHostedService
|
|
{
|
|
/// <summary>Resolves <see cref="ISecretStore"/>, which spawns the replication actor.</summary>
|
|
/// <param name="cancellationToken">Unused — resolution is synchronous and non-blocking.</param>
|
|
/// <returns>A completed task.</returns>
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
// The resolution itself is the side effect; the instance is deliberately unused.
|
|
_ = services.GetRequiredService<ISecretStore>();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>No-op: the actor's lifetime is the actor system's.</summary>
|
|
/// <param name="cancellationToken">Unused.</param>
|
|
/// <returns>A completed task.</returns>
|
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
|
}
|