adbf2eb182
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.
113 lines
4.1 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|