Files
scadaproj/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Replicator.AkkaDotNet/AkkaSecretReplicator.cs
T
Joseph Doherty dd0a846b64 feat(secrets): cluster replication via SQL Server and Akka.NET (G-7, 0.2.0)
Secrets were per-node SQLite, so a secret written on one node was invisible to
the rest of a cluster. G-7's design resolved the "shared SQL store vs Akka
replicator" fork to build only the former; both are built here so the choice is
a deployment decision (availability vs partition tolerance) rather than a
library limitation.

Two new packages — ZB.MOM.WW.Secrets.Replicator.SqlServer (shared store, plus a
local-store-with-hub mode) and .Replicator.AkkaDotNet (peer-to-peer over
distributed pub/sub). Core gains ISecretsStoreMigrator, one shared
SecretLastWriterWins predicate so no two stores can disagree on a tie, the
transport-agnostic reconciler, and ReplicatingSecretStore — which closes a real
gap: nothing had ever called ISecretReplicator.PublishAsync, so the seam was
inert and local writes would not have propagated at all.

Verified 182 pass / 1 skip / 0 warnings, including 15 live tests against a real
SQL Server 2022 (the SQLite suite ported case-for-case, so any behavioural
divergence between the stores fails) and a 9-test in-process 2-node Akka
cluster over real remoting. A post-build review caught six defects, all fixed
and now covered: both replication modes could not resolve from the container
(no test had built one), an unbounded fetch that broke past SQL Server's
2100-parameter cap, a poison row that aborted the rest of its batch forever,
Enum.Parse on peer input that could restart the actor in a loop, null crypto
blobs crossing the trust boundary, and a silently dropped pull-read failure.

Packed at 0.2.0 and vulnerability-scanned clean; not yet published to the feed.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-18 04:08:23 -04:00

34 lines
1.3 KiB
C#

using Akka.Actor;
using ZB.MOM.WW.Secrets.Abstractions;
namespace ZB.MOM.WW.Secrets.Replicator.AkkaDotNet;
/// <summary>
/// Publishes local secret writes to cluster peers by handing them to this node's
/// <see cref="SecretReplicationActor"/>.
/// </summary>
/// <remarks>
/// The hand-off is a <c>Tell</c>, so <see cref="PublishAsync"/> completes as soon as the row is in
/// the actor's mailbox rather than when peers have applied it. That is the correct contract here:
/// the caller (<c>ReplicatingSecretStore</c>) has already durably written the row locally and treats
/// publishing as best-effort, and blocking a write until every peer acknowledged would make each
/// node's availability depend on all the others. Delivery is guaranteed by anti-entropy, not by
/// this call.
/// </remarks>
/// <param name="replicationActor">This node's replication actor.</param>
public sealed class AkkaSecretReplicator(IActorRef replicationActor) : ISecretReplicator
{
private readonly IActorRef _replicationActor =
replicationActor ?? throw new ArgumentNullException(nameof(replicationActor));
/// <inheritdoc />
public Task PublishAsync(StoredSecret row, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(row);
_replicationActor.Tell(new SecretReplicationActor.PublishLocalWrite(row));
return Task.CompletedTask;
}
}