diff --git a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Replicator.Grpc/ISecretsHubSyncStatus.cs b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Replicator.Grpc/ISecretsHubSyncStatus.cs index dccc292..d4ad04c 100644 --- a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Replicator.Grpc/ISecretsHubSyncStatus.cs +++ b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Replicator.Grpc/ISecretsHubSyncStatus.cs @@ -36,21 +36,46 @@ public interface ISecretsHubSyncStatus } /// The mutable implementation the sync service writes to; consumers see only the interface. +/// +/// Writes (/) run under , +/// and every getter takes the same lock rather than reading its backing field bare — a plain field +/// read races the writer with no synchronization otherwise, and a torn/half-written +/// (a multi-field struct) or an interleaving that pairs one write's +/// LastSweepUtc with another write's LastError is exactly the inconsistency a status +/// consumer must not see. +/// internal sealed class SecretsHubSyncStatus : ISecretsHubSyncStatus { private readonly object _gate = new(); - /// - public DateTimeOffset? LastSweepUtc { get; private set; } + private DateTimeOffset? _lastSweepUtc; + private DateTimeOffset? _lastSuccessUtc; + private int _consecutiveFailures; + private string? _lastError; /// - public DateTimeOffset? LastSuccessUtc { get; private set; } + public DateTimeOffset? LastSweepUtc + { + get { lock (_gate) { return _lastSweepUtc; } } + } /// - public int ConsecutiveFailures { get; private set; } + public DateTimeOffset? LastSuccessUtc + { + get { lock (_gate) { return _lastSuccessUtc; } } + } /// - public string? LastError { get; private set; } + public int ConsecutiveFailures + { + get { lock (_gate) { return _consecutiveFailures; } } + } + + /// + public string? LastError + { + get { lock (_gate) { return _lastError; } } + } /// /// Records a successful sweep, resetting the failure streak. @@ -64,10 +89,10 @@ internal sealed class SecretsHubSyncStatus : ISecretsHubSyncStatus { lock (_gate) { - int prior = ConsecutiveFailures; - LastSweepUtc = LastSuccessUtc = now; - ConsecutiveFailures = 0; - LastError = null; + int prior = _consecutiveFailures; + _lastSweepUtc = _lastSuccessUtc = now; + _consecutiveFailures = 0; + _lastError = null; return prior; } } @@ -79,9 +104,9 @@ internal sealed class SecretsHubSyncStatus : ISecretsHubSyncStatus { lock (_gate) { - LastSweepUtc = now; - ConsecutiveFailures++; - LastError = ex.GetType().Name + ": " + ex.Message; + _lastSweepUtc = now; + _consecutiveFailures++; + _lastError = ex.GetType().Name + ": " + ex.Message; } } }