dd0a846b64
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
34 lines
1.3 KiB
C#
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;
|
|
}
|
|
}
|