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.
This commit is contained in:
@@ -36,21 +36,46 @@ public interface ISecretsHubSyncStatus
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>The mutable implementation the sync service writes to; consumers see only the interface.</summary>
|
/// <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
|
internal sealed class SecretsHubSyncStatus : ISecretsHubSyncStatus
|
||||||
{
|
{
|
||||||
private readonly object _gate = new();
|
private readonly object _gate = new();
|
||||||
|
|
||||||
/// <inheritdoc/>
|
private DateTimeOffset? _lastSweepUtc;
|
||||||
public DateTimeOffset? LastSweepUtc { get; private set; }
|
private DateTimeOffset? _lastSuccessUtc;
|
||||||
|
private int _consecutiveFailures;
|
||||||
|
private string? _lastError;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public DateTimeOffset? LastSuccessUtc { get; private set; }
|
public DateTimeOffset? LastSweepUtc
|
||||||
|
{
|
||||||
|
get { lock (_gate) { return _lastSweepUtc; } }
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public int ConsecutiveFailures { get; private set; }
|
public DateTimeOffset? LastSuccessUtc
|
||||||
|
{
|
||||||
|
get { lock (_gate) { return _lastSuccessUtc; } }
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string? LastError { get; private set; }
|
public int ConsecutiveFailures
|
||||||
|
{
|
||||||
|
get { lock (_gate) { return _consecutiveFailures; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string? LastError
|
||||||
|
{
|
||||||
|
get { lock (_gate) { return _lastError; } }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Records a successful sweep, resetting the failure streak.
|
/// Records a successful sweep, resetting the failure streak.
|
||||||
@@ -64,10 +89,10 @@ internal sealed class SecretsHubSyncStatus : ISecretsHubSyncStatus
|
|||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
int prior = ConsecutiveFailures;
|
int prior = _consecutiveFailures;
|
||||||
LastSweepUtc = LastSuccessUtc = now;
|
_lastSweepUtc = _lastSuccessUtc = now;
|
||||||
ConsecutiveFailures = 0;
|
_consecutiveFailures = 0;
|
||||||
LastError = null;
|
_lastError = null;
|
||||||
return prior;
|
return prior;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,9 +104,9 @@ internal sealed class SecretsHubSyncStatus : ISecretsHubSyncStatus
|
|||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
LastSweepUtc = now;
|
_lastSweepUtc = now;
|
||||||
ConsecutiveFailures++;
|
_consecutiveFailures++;
|
||||||
LastError = ex.GetType().Name + ": " + ex.Message;
|
_lastError = ex.GetType().Name + ": " + ex.Message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user