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
41 lines
1.7 KiB
C#
41 lines
1.7 KiB
C#
namespace ZB.MOM.WW.Secrets.Replicator.AkkaDotNet;
|
||
|
||
/// <summary>
|
||
/// Configuration for peer-to-peer secret replication over the Akka cluster.
|
||
/// </summary>
|
||
public sealed class AkkaSecretsReplicationOptions
|
||
{
|
||
/// <summary>
|
||
/// How often this node broadcasts its manifest for anti-entropy. Defaults to 30 seconds.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// This is the convergence bound, not the propagation latency: a live write reaches peers in
|
||
/// milliseconds, and this interval only governs how quickly a <em>dropped</em> write or a
|
||
/// rejoining node is repaired. The traffic is one manifest per node per interval — names,
|
||
/// revisions, and timestamps, no ciphertext — so shortening it is cheap for a small cluster and
|
||
/// grows as O(nodes × secrets).
|
||
/// </remarks>
|
||
public TimeSpan AnnounceInterval { get; set; } = TimeSpan.FromSeconds(30);
|
||
|
||
/// <summary>Name of the replication actor within the actor system. Defaults to <c>zb-secret-replication</c>.</summary>
|
||
public string ActorName { get; set; } = "zb-secret-replication";
|
||
|
||
/// <summary>
|
||
/// Throws if the options are unusable.
|
||
/// </summary>
|
||
/// <exception cref="InvalidOperationException">The announce interval is not positive, or the actor name is blank.</exception>
|
||
public void Validate()
|
||
{
|
||
if (AnnounceInterval <= TimeSpan.Zero)
|
||
{
|
||
throw new InvalidOperationException(
|
||
$"Secret replication AnnounceInterval must be positive (was {AnnounceInterval}).");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(ActorName))
|
||
{
|
||
throw new InvalidOperationException("Secret replication ActorName must not be blank.");
|
||
}
|
||
}
|
||
}
|