up
This commit is contained in:
@@ -37,6 +37,8 @@ public sealed class GhsaConnector : IFeedConnector
|
||||
private readonly GhsaDiagnostics _diagnostics;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<GhsaConnector> _logger;
|
||||
private readonly object _rateLimitWarningLock = new();
|
||||
private readonly Dictionary<(string Phase, string Resource), bool> _rateLimitWarnings = new();
|
||||
|
||||
public GhsaConnector(
|
||||
SourceFetchService fetchService,
|
||||
@@ -412,6 +414,62 @@ public sealed class GhsaConnector : IFeedConnector
|
||||
await _stateRepository.UpdateCursorAsync(SourceName, cursor.ToBsonDocument(), _timeProvider.GetUtcNow(), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private bool ShouldLogRateLimitWarning(in GhsaRateLimitSnapshot snapshot, out bool recovered)
|
||||
{
|
||||
recovered = false;
|
||||
|
||||
if (!snapshot.Remaining.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var key = (snapshot.Phase, snapshot.Resource ?? "global");
|
||||
var warn = snapshot.Remaining.Value <= _options.RateLimitWarningThreshold;
|
||||
|
||||
lock (_rateLimitWarningLock)
|
||||
{
|
||||
var previouslyWarned = _rateLimitWarnings.TryGetValue(key, out var flagged) && flagged;
|
||||
|
||||
if (warn)
|
||||
{
|
||||
if (previouslyWarned)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_rateLimitWarnings[key] = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (previouslyWarned)
|
||||
{
|
||||
_rateLimitWarnings.Remove(key);
|
||||
recovered = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static double? CalculateHeadroomPercentage(in GhsaRateLimitSnapshot snapshot)
|
||||
{
|
||||
if (!snapshot.Limit.HasValue || !snapshot.Remaining.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var limit = snapshot.Limit.Value;
|
||||
if (limit <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (double)snapshot.Remaining.Value / limit * 100d;
|
||||
}
|
||||
|
||||
private static string FormatHeadroom(double? headroomPct)
|
||||
=> headroomPct.HasValue ? $" (headroom {headroomPct.Value:F1}%)" : string.Empty;
|
||||
|
||||
private async Task<bool> ApplyRateLimitAsync(IReadOnlyDictionary<string, string>? headers, string phase, CancellationToken cancellationToken)
|
||||
{
|
||||
var snapshot = GhsaRateLimitParser.TryParse(headers, _timeProvider.GetUtcNow(), phase);
|
||||
@@ -422,19 +480,31 @@ public sealed class GhsaConnector : IFeedConnector
|
||||
|
||||
_diagnostics.RecordRateLimit(snapshot.Value);
|
||||
|
||||
if (snapshot.Value.Remaining.HasValue && snapshot.Value.Remaining.Value <= _options.RateLimitWarningThreshold)
|
||||
var headroomPct = CalculateHeadroomPercentage(snapshot.Value);
|
||||
if (ShouldLogRateLimitWarning(snapshot.Value, out var recovered))
|
||||
{
|
||||
var resetMessage = snapshot.Value.ResetAfter.HasValue
|
||||
? $" (resets in {snapshot.Value.ResetAfter.Value:c})"
|
||||
: snapshot.Value.ResetAt.HasValue ? $" (resets at {snapshot.Value.ResetAt.Value:O})" : string.Empty;
|
||||
|
||||
_logger.LogWarning(
|
||||
"GHSA rate limit warning: remaining {Remaining} of {Limit} for {Phase} {Resource}{ResetMessage}",
|
||||
"GHSA rate limit warning: remaining {Remaining} of {Limit} for {Phase} {Resource}{ResetMessage}{Headroom}",
|
||||
snapshot.Value.Remaining,
|
||||
snapshot.Value.Limit,
|
||||
phase,
|
||||
snapshot.Value.Resource ?? "global",
|
||||
resetMessage);
|
||||
resetMessage,
|
||||
FormatHeadroom(headroomPct));
|
||||
}
|
||||
else if (recovered)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"GHSA rate limit recovered for {Phase} {Resource}: remaining {Remaining} of {Limit}{Headroom}",
|
||||
phase,
|
||||
snapshot.Value.Resource ?? "global",
|
||||
snapshot.Value.Remaining,
|
||||
snapshot.Value.Limit,
|
||||
FormatHeadroom(headroomPct));
|
||||
}
|
||||
|
||||
if (snapshot.Value.Remaining.HasValue && snapshot.Value.Remaining.Value <= 0)
|
||||
@@ -445,10 +515,11 @@ public sealed class GhsaConnector : IFeedConnector
|
||||
if (delay > TimeSpan.Zero)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"GHSA rate limit exhausted for {Phase} {Resource}; delaying {Delay}",
|
||||
"GHSA rate limit exhausted for {Phase} {Resource}; delaying {Delay}{Headroom}",
|
||||
phase,
|
||||
snapshot.Value.Resource ?? "global",
|
||||
delay);
|
||||
delay,
|
||||
FormatHeadroom(headroomPct));
|
||||
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Feedser.Source.Ghsa.Internal;
|
||||
@@ -19,9 +20,12 @@ public sealed class GhsaDiagnostics : IDisposable
|
||||
private readonly Histogram<long> _rateLimitRemaining;
|
||||
private readonly Histogram<long> _rateLimitLimit;
|
||||
private readonly Histogram<double> _rateLimitResetSeconds;
|
||||
private readonly Histogram<double> _rateLimitHeadroomPct;
|
||||
private readonly ObservableGauge<double> _rateLimitHeadroomGauge;
|
||||
private readonly Counter<long> _rateLimitExhausted;
|
||||
private readonly object _rateLimitLock = new();
|
||||
private GhsaRateLimitSnapshot? _lastRateLimitSnapshot;
|
||||
private readonly Dictionary<(string Phase, string? Resource), GhsaRateLimitSnapshot> _rateLimitSnapshots = new();
|
||||
|
||||
public GhsaDiagnostics()
|
||||
{
|
||||
@@ -37,6 +41,8 @@ public sealed class GhsaDiagnostics : IDisposable
|
||||
_rateLimitRemaining = _meter.CreateHistogram<long>("ghsa.ratelimit.remaining", unit: "requests");
|
||||
_rateLimitLimit = _meter.CreateHistogram<long>("ghsa.ratelimit.limit", unit: "requests");
|
||||
_rateLimitResetSeconds = _meter.CreateHistogram<double>("ghsa.ratelimit.reset_seconds", unit: "s");
|
||||
_rateLimitHeadroomPct = _meter.CreateHistogram<double>("ghsa.ratelimit.headroom_pct", unit: "percent");
|
||||
_rateLimitHeadroomGauge = _meter.CreateObservableGauge("ghsa.ratelimit.headroom_pct_current", ObserveHeadroom, unit: "percent");
|
||||
_rateLimitExhausted = _meter.CreateCounter<long>("ghsa.ratelimit.exhausted", unit: "events");
|
||||
}
|
||||
|
||||
@@ -79,9 +85,15 @@ public sealed class GhsaDiagnostics : IDisposable
|
||||
_rateLimitResetSeconds.Record(snapshot.ResetAfter.Value.TotalSeconds, tags);
|
||||
}
|
||||
|
||||
if (TryCalculateHeadroom(snapshot, out var headroom))
|
||||
{
|
||||
_rateLimitHeadroomPct.Record(headroom, tags);
|
||||
}
|
||||
|
||||
lock (_rateLimitLock)
|
||||
{
|
||||
_lastRateLimitSnapshot = snapshot;
|
||||
_rateLimitSnapshots[(snapshot.Phase, snapshot.Resource)] = snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,5 +108,48 @@ public sealed class GhsaDiagnostics : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => _meter.Dispose();
|
||||
private IEnumerable<Measurement<double>> ObserveHeadroom()
|
||||
{
|
||||
lock (_rateLimitLock)
|
||||
{
|
||||
if (_rateLimitSnapshots.Count == 0)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var snapshot in _rateLimitSnapshots.Values)
|
||||
{
|
||||
if (TryCalculateHeadroom(snapshot, out var headroom))
|
||||
{
|
||||
yield return new Measurement<double>(
|
||||
headroom,
|
||||
new KeyValuePair<string, object?>("phase", snapshot.Phase),
|
||||
new KeyValuePair<string, object?>("resource", snapshot.Resource ?? "unknown"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryCalculateHeadroom(in GhsaRateLimitSnapshot snapshot, out double headroomPct)
|
||||
{
|
||||
headroomPct = 0;
|
||||
if (!snapshot.Limit.HasValue || !snapshot.Remaining.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var limit = snapshot.Limit.Value;
|
||||
if (limit <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
headroomPct = (double)snapshot.Remaining.Value / limit * 100d;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_meter.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,7 @@
|
||||
|Production credential & scheduler rollout|Ops, BE-Conn-GHSA|Docs, WebService|**DONE (2025-10-12)** – Scheduler defaults registered via `JobSchedulerBuilder`, credential provisioning documented (Compose/Helm samples), and staged backfill guidance captured in `docs/ops/feedser-ghsa-operations.md`.|
|
||||
|FEEDCONN-GHSA-04-002 Conflict regression fixtures|BE-Conn-GHSA, QA|Merge `FEEDMERGE-ENGINE-04-001`|**DONE (2025-10-12)** – Added `conflict-ghsa.canonical.json` + `GhsaConflictFixtureTests`; SemVer ranges and credits align with merge precedence triple and shareable with QA. Validation: `dotnet test src/StellaOps.Feedser.Source.Ghsa.Tests/StellaOps.Feedser.Source.Ghsa.Tests.csproj --filter GhsaConflictFixtureTests`.|
|
||||
|FEEDCONN-GHSA-02-004 GHSA credits & ecosystem severity mapping|BE-Conn-GHSA|Models `FEEDMODELS-SCHEMA-01-002`|**DONE (2025-10-11)** – Mapper emits advisory credits with provenance masks, fixtures assert role/contact ordering, and severity normalization remains unchanged.|
|
||||
|FEEDCONN-GHSA-02-007 Credit parity regression fixtures|BE-Conn-GHSA, QA|Source.Nvd, Source.Osv|**DONE (2025-10-12)** – Credit parity fixtures recorded, regression tests cover GHSA/OSV/NVD alignment, and regeneration workflow documented in `docs/dev/fixtures.md`.|
|
||||
|FEEDCONN-GHSA-02-007 Credit parity regression fixtures|BE-Conn-GHSA, QA|Source.Nvd, Source.Osv|**DONE (2025-10-12)** – Parity fixtures regenerated via `tools/FixtureUpdater`, normalized SemVer notes verified against GHSA/NVD/OSV snapshots, and the fixtures guide now documents the headroom checks.|
|
||||
|FEEDCONN-GHSA-02-001 Normalized versions rollout|BE-Conn-GHSA|Models `FEEDMODELS-SCHEMA-01-003`, Normalization playbook|**DONE (2025-10-11)** – GHSA mapper now emits SemVer primitives + normalized ranges, fixtures refreshed, connector tests passing; report logged via FEEDMERGE-COORD-02-900.|
|
||||
|FEEDCONN-GHSA-02-005 Quota monitoring hardening|BE-Conn-GHSA, Observability|Source.Common metrics|**DONE (2025-10-12)** – Diagnostics expose headroom histograms/gauges, warning logs dedupe below the configured threshold, and the ops runbook gained alerting and mitigation guidance.|
|
||||
|FEEDCONN-GHSA-02-006 Scheduler rollout integration|BE-Conn-GHSA, Ops|Job scheduler|**DONE (2025-10-12)** – Dependency routine tests assert cron/timeouts, and the runbook highlights cron overrides plus backoff toggles for staged rollouts.|
|
||||
|
||||
Reference in New Issue
Block a user