Files
scadaproj/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Replicator.Grpc/ISecretsHubSyncStatus.cs
T
Joseph Doherty adbf2eb182 fix(secrets): synchronize SecretsHubSyncStatus getters under the write lock
Review finding (Fix round 1, Task 1): the property getters read their
backing fields without synchronization while RecordSuccess/RecordFailure
write them under _gate, so a concurrent read could observe a torn
DateTimeOffset or a mix of fields from two different writes. Converted the
four auto-properties to explicit properties over private fields, each
getter taking the same lock the writers use. Public interface unchanged.
2026-09-28 17:30:33 -04:00

113 lines
4.1 KiB
C#

namespace ZB.MOM.WW.Secrets.Replicator.Grpc;
/// <summary>Point-in-time outcome of the hub follower's sweeps. Never carries secret material.</summary>
/// <remarks>
/// Registered as a singleton by <c>AddZbSecretsGrpcHubClient</c> alongside the background sweep
/// service, so a host can surface hub-sync health (a readiness check, a status page) without
/// scraping logs. Every field reflects the outcome of the most recently completed sweep only —
/// there is deliberately no history here, just "what happened last time".
/// </remarks>
public interface ISecretsHubSyncStatus
{
/// <summary>
/// When the most recent sweep finished, whether it succeeded or failed. <see langword="null"/>
/// before the first sweep has completed.
/// </summary>
DateTimeOffset? LastSweepUtc { get; }
/// <summary>
/// When the most recent <em>successful</em> sweep finished. <see langword="null"/> if no sweep
/// has ever succeeded.
/// </summary>
DateTimeOffset? LastSuccessUtc { get; }
/// <summary>
/// The number of sweeps that have failed in a row, ending at the most recent sweep. Reset to
/// zero the moment a sweep succeeds.
/// </summary>
int ConsecutiveFailures { get; }
/// <summary>
/// The message from the most recent sweep failure, or <see langword="null"/> if the most recent
/// sweep succeeded (or none has run yet). This is the exception's type and message only — never
/// a stack trace, and never secret material, since the sweep never has plaintext in scope.
/// </summary>
string? LastError { get; }
}
/// <summary>The mutable implementation the sync service writes to; consumers see only the interface.</summary>
/// <remarks>
/// Writes (<see cref="RecordSuccess"/>/<see cref="RecordFailure"/>) run under <see cref="_gate"/>,
/// 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
/// <see cref="DateTimeOffset"/> (a multi-field struct) or an interleaving that pairs one write's
/// <c>LastSweepUtc</c> with another write's <c>LastError</c> is exactly the inconsistency a status
/// consumer must not see.
/// </remarks>
internal sealed class SecretsHubSyncStatus : ISecretsHubSyncStatus
{
private readonly object _gate = new();
private DateTimeOffset? _lastSweepUtc;
private DateTimeOffset? _lastSuccessUtc;
private int _consecutiveFailures;
private string? _lastError;
/// <inheritdoc/>
public DateTimeOffset? LastSweepUtc
{
get { lock (_gate) { return _lastSweepUtc; } }
}
/// <inheritdoc/>
public DateTimeOffset? LastSuccessUtc
{
get { lock (_gate) { return _lastSuccessUtc; } }
}
/// <inheritdoc/>
public int ConsecutiveFailures
{
get { lock (_gate) { return _consecutiveFailures; } }
}
/// <inheritdoc/>
public string? LastError
{
get { lock (_gate) { return _lastError; } }
}
/// <summary>
/// Records a successful sweep, resetting the failure streak.
/// </summary>
/// <param name="now">The instant the sweep completed.</param>
/// <returns>
/// The consecutive-failure count <em>before</em> this success — the caller uses a non-zero
/// result to decide whether the sweep is a recovery worth logging.
/// </returns>
internal int RecordSuccess(DateTimeOffset now)
{
lock (_gate)
{
int prior = _consecutiveFailures;
_lastSweepUtc = _lastSuccessUtc = now;
_consecutiveFailures = 0;
_lastError = null;
return prior;
}
}
/// <summary>Records a failed sweep, extending the failure streak.</summary>
/// <param name="now">The instant the sweep failed.</param>
/// <param name="ex">The exception the sweep failed with.</param>
internal void RecordFailure(DateTimeOffset now, Exception ex)
{
lock (_gate)
{
_lastSweepUtc = now;
_consecutiveFailures++;
_lastError = ex.GetType().Name + ": " + ex.Message;
}
}
}