sprints enhancements
This commit is contained in:
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Concelier.Core;
|
||||
using StellaOps.Concelier.Core.Events;
|
||||
using StellaOps.Concelier.Merge.Identity;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Aliases;
|
||||
@@ -41,6 +42,7 @@ public sealed class AdvisoryMergeService
|
||||
private readonly IAdvisoryEventLog _eventLog;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly CanonicalMerger _canonicalMerger;
|
||||
private readonly IMergeHashCalculator? _mergeHashCalculator;
|
||||
private readonly ILogger<AdvisoryMergeService> _logger;
|
||||
|
||||
public AdvisoryMergeService(
|
||||
@@ -51,7 +53,8 @@ public sealed class AdvisoryMergeService
|
||||
CanonicalMerger canonicalMerger,
|
||||
IAdvisoryEventLog eventLog,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<AdvisoryMergeService> logger)
|
||||
ILogger<AdvisoryMergeService> logger,
|
||||
IMergeHashCalculator? mergeHashCalculator = null)
|
||||
{
|
||||
_aliasResolver = aliasResolver ?? throw new ArgumentNullException(nameof(aliasResolver));
|
||||
_advisoryStore = advisoryStore ?? throw new ArgumentNullException(nameof(advisoryStore));
|
||||
@@ -61,6 +64,7 @@ public sealed class AdvisoryMergeService
|
||||
_eventLog = eventLog ?? throw new ArgumentNullException(nameof(eventLog));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_mergeHashCalculator = mergeHashCalculator; // Optional during migration
|
||||
}
|
||||
|
||||
public async Task<AdvisoryMergeResult> MergeAsync(string seedAdvisoryKey, CancellationToken cancellationToken)
|
||||
@@ -102,7 +106,7 @@ public sealed class AdvisoryMergeService
|
||||
throw;
|
||||
}
|
||||
|
||||
var merged = precedenceResult.Advisory;
|
||||
var merged = EnrichWithMergeHash(precedenceResult.Advisory);
|
||||
var conflictDetails = precedenceResult.Conflicts;
|
||||
|
||||
if (component.Collisions.Count > 0)
|
||||
@@ -309,7 +313,48 @@ public sealed class AdvisoryMergeService
|
||||
source.Provenance,
|
||||
source.Description,
|
||||
source.Cwes,
|
||||
source.CanonicalMetricId);
|
||||
source.CanonicalMetricId,
|
||||
source.MergeHash);
|
||||
|
||||
/// <summary>
|
||||
/// Enriches an advisory with its computed merge hash if calculator is available.
|
||||
/// </summary>
|
||||
private Advisory EnrichWithMergeHash(Advisory advisory)
|
||||
{
|
||||
if (_mergeHashCalculator is null)
|
||||
{
|
||||
return advisory;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var mergeHash = _mergeHashCalculator.ComputeMergeHash(advisory);
|
||||
return new Advisory(
|
||||
advisory.AdvisoryKey,
|
||||
advisory.Title,
|
||||
advisory.Summary,
|
||||
advisory.Language,
|
||||
advisory.Published,
|
||||
advisory.Modified,
|
||||
advisory.Severity,
|
||||
advisory.ExploitKnown,
|
||||
advisory.Aliases,
|
||||
advisory.Credits,
|
||||
advisory.References,
|
||||
advisory.AffectedPackages,
|
||||
advisory.CvssMetrics,
|
||||
advisory.Provenance,
|
||||
advisory.Description,
|
||||
advisory.Cwes,
|
||||
advisory.CanonicalMetricId,
|
||||
mergeHash);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to compute merge hash for {AdvisoryKey}, continuing without hash", advisory.AdvisoryKey);
|
||||
return advisory;
|
||||
}
|
||||
}
|
||||
|
||||
private CanonicalMergeResult? ApplyCanonicalMergeIfNeeded(string canonicalKey, List<Advisory> inputs)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// MergeHashBackfillService.cs
|
||||
// Sprint: SPRINT_8200_0012_0001_CONCEL_merge_hash_library
|
||||
// Task: MHASH-8200-020
|
||||
// Description: Shadow-write mode for computing merge_hash on existing advisories
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Concelier.Merge.Identity;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for backfilling merge hashes on existing advisories without changing their identity.
|
||||
/// Runs in shadow-write mode: computes merge_hash and updates only that field.
|
||||
/// </summary>
|
||||
public sealed class MergeHashBackfillService
|
||||
{
|
||||
private readonly IAdvisoryStore _advisoryStore;
|
||||
private readonly IMergeHashCalculator _mergeHashCalculator;
|
||||
private readonly ILogger<MergeHashBackfillService> _logger;
|
||||
|
||||
public MergeHashBackfillService(
|
||||
IAdvisoryStore advisoryStore,
|
||||
IMergeHashCalculator mergeHashCalculator,
|
||||
ILogger<MergeHashBackfillService> logger)
|
||||
{
|
||||
_advisoryStore = advisoryStore ?? throw new ArgumentNullException(nameof(advisoryStore));
|
||||
_mergeHashCalculator = mergeHashCalculator ?? throw new ArgumentNullException(nameof(mergeHashCalculator));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Backfills merge hashes for all advisories that don't have one.
|
||||
/// </summary>
|
||||
/// <param name="batchSize">Number of advisories to process before yielding progress.</param>
|
||||
/// <param name="dryRun">If true, computes hashes but doesn't persist them.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Backfill result with statistics.</returns>
|
||||
public async Task<MergeHashBackfillResult> BackfillAsync(
|
||||
int batchSize = 100,
|
||||
bool dryRun = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var processed = 0;
|
||||
var updated = 0;
|
||||
var skipped = 0;
|
||||
var errors = 0;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Starting merge hash backfill (dryRun={DryRun}, batchSize={BatchSize})",
|
||||
dryRun, batchSize);
|
||||
|
||||
await foreach (var advisory in _advisoryStore.StreamAsync(cancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
processed++;
|
||||
|
||||
// Skip if already has merge hash
|
||||
if (!string.IsNullOrEmpty(advisory.MergeHash))
|
||||
{
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var mergeHash = _mergeHashCalculator.ComputeMergeHash(advisory);
|
||||
|
||||
if (!dryRun)
|
||||
{
|
||||
var enrichedAdvisory = CreateAdvisoryWithMergeHash(advisory, mergeHash);
|
||||
await _advisoryStore.UpsertAsync(enrichedAdvisory, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
updated++;
|
||||
|
||||
if (updated % batchSize == 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Backfill progress: {Updated} updated, {Skipped} skipped, {Errors} errors (of {Processed} processed)",
|
||||
updated, skipped, errors, processed);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors++;
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Failed to compute/update merge hash for {AdvisoryKey}",
|
||||
advisory.AdvisoryKey);
|
||||
}
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
var result = new MergeHashBackfillResult(
|
||||
TotalProcessed: processed,
|
||||
Updated: updated,
|
||||
Skipped: skipped,
|
||||
Errors: errors,
|
||||
DryRun: dryRun,
|
||||
Duration: stopwatch.Elapsed);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Merge hash backfill completed: {Updated} updated, {Skipped} skipped, {Errors} errors (of {Processed} processed) in {Duration}",
|
||||
result.Updated, result.Skipped, result.Errors, result.TotalProcessed, result.Duration);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes merge hash for a single advisory without persisting.
|
||||
/// Useful for testing or preview mode.
|
||||
/// </summary>
|
||||
public string ComputeMergeHash(Advisory advisory)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(advisory);
|
||||
return _mergeHashCalculator.ComputeMergeHash(advisory);
|
||||
}
|
||||
|
||||
private static Advisory CreateAdvisoryWithMergeHash(Advisory source, string mergeHash)
|
||||
=> new(
|
||||
source.AdvisoryKey,
|
||||
source.Title,
|
||||
source.Summary,
|
||||
source.Language,
|
||||
source.Published,
|
||||
source.Modified,
|
||||
source.Severity,
|
||||
source.ExploitKnown,
|
||||
source.Aliases,
|
||||
source.Credits,
|
||||
source.References,
|
||||
source.AffectedPackages,
|
||||
source.CvssMetrics,
|
||||
source.Provenance,
|
||||
source.Description,
|
||||
source.Cwes,
|
||||
source.CanonicalMetricId,
|
||||
mergeHash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a merge hash backfill operation.
|
||||
/// </summary>
|
||||
public sealed record MergeHashBackfillResult(
|
||||
int TotalProcessed,
|
||||
int Updated,
|
||||
int Skipped,
|
||||
int Errors,
|
||||
bool DryRun,
|
||||
TimeSpan Duration)
|
||||
{
|
||||
/// <summary>
|
||||
/// Percentage of advisories that were successfully updated.
|
||||
/// </summary>
|
||||
public double SuccessRate => TotalProcessed > 0
|
||||
? (double)(Updated + Skipped) / TotalProcessed * 100
|
||||
: 100;
|
||||
|
||||
/// <summary>
|
||||
/// Average time per advisory in milliseconds.
|
||||
/// </summary>
|
||||
public double AvgTimePerAdvisoryMs => TotalProcessed > 0
|
||||
? Duration.TotalMilliseconds / TotalProcessed
|
||||
: 0;
|
||||
}
|
||||
Reference in New Issue
Block a user