186 lines
6.4 KiB
C#
186 lines
6.4 KiB
C#
// -----------------------------------------------------------------------------
|
|
// InMemoryGateEvaluationQueue.cs
|
|
// Sprint: SPRINT_20251226_001_BE_cicd_gate_integration
|
|
// Task: CICD-GATE-02 - Gate evaluation queue implementation
|
|
// Description: In-memory queue for gate evaluation jobs with background processing
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Policy.Engine.Gates;
|
|
using StellaOps.Policy.Gateway.Endpoints;
|
|
using System.Threading.Channels;
|
|
|
|
namespace StellaOps.Policy.Gateway.Services;
|
|
|
|
/// <summary>
|
|
/// In-memory implementation of the gate evaluation queue.
|
|
/// Uses System.Threading.Channels for async producer-consumer pattern.
|
|
/// </summary>
|
|
public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue
|
|
{
|
|
private readonly Channel<GateEvaluationJob> _channel;
|
|
private readonly ILogger<InMemoryGateEvaluationQueue> _logger;
|
|
private readonly TimeProvider _timeProvider;
|
|
|
|
public InMemoryGateEvaluationQueue(
|
|
ILogger<InMemoryGateEvaluationQueue> logger,
|
|
TimeProvider? timeProvider = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(logger);
|
|
_logger = logger;
|
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
|
|
|
// Bounded channel to prevent unbounded memory growth
|
|
_channel = Channel.CreateBounded<GateEvaluationJob>(new BoundedChannelOptions(1000)
|
|
{
|
|
FullMode = BoundedChannelFullMode.Wait,
|
|
SingleReader = false,
|
|
SingleWriter = false
|
|
});
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<string> EnqueueAsync(GateEvaluationRequest request, CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
var jobId = GenerateJobId();
|
|
var job = new GateEvaluationJob
|
|
{
|
|
JobId = jobId,
|
|
Request = request,
|
|
QueuedAt = _timeProvider.GetUtcNow()
|
|
};
|
|
|
|
await _channel.Writer.WriteAsync(job, cancellationToken).ConfigureAwait(false);
|
|
|
|
_logger.LogDebug(
|
|
"Enqueued gate evaluation job {JobId} for {Repository}@{Digest}",
|
|
jobId,
|
|
request.Repository,
|
|
request.ImageDigest);
|
|
|
|
return jobId;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the channel reader for consuming jobs.
|
|
/// </summary>
|
|
public ChannelReader<GateEvaluationJob> Reader => _channel.Reader;
|
|
|
|
private string GenerateJobId()
|
|
{
|
|
// Format: gate-{timestamp}-{random}
|
|
var timestamp = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds();
|
|
var random = Guid.NewGuid().ToString("N")[..8];
|
|
return $"gate-{timestamp}-{random}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A gate evaluation job in the queue.
|
|
/// </summary>
|
|
public sealed record GateEvaluationJob
|
|
{
|
|
public required string JobId { get; init; }
|
|
public required GateEvaluationRequest Request { get; init; }
|
|
public required DateTimeOffset QueuedAt { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Background service that processes gate evaluation jobs from the queue.
|
|
/// Orchestrates: image analysis -> drift delta computation -> gate evaluation.
|
|
/// </summary>
|
|
public sealed class GateEvaluationWorker : BackgroundService
|
|
{
|
|
private readonly InMemoryGateEvaluationQueue _queue;
|
|
private readonly IServiceScopeFactory _scopeFactory;
|
|
private readonly ILogger<GateEvaluationWorker> _logger;
|
|
|
|
public GateEvaluationWorker(
|
|
InMemoryGateEvaluationQueue queue,
|
|
IServiceScopeFactory scopeFactory,
|
|
ILogger<GateEvaluationWorker> logger)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(queue);
|
|
ArgumentNullException.ThrowIfNull(scopeFactory);
|
|
ArgumentNullException.ThrowIfNull(logger);
|
|
|
|
_queue = queue;
|
|
_scopeFactory = scopeFactory;
|
|
_logger = logger;
|
|
}
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
_logger.LogInformation("Gate evaluation worker starting");
|
|
|
|
await foreach (var job in _queue.Reader.ReadAllAsync(stoppingToken))
|
|
{
|
|
try
|
|
{
|
|
await ProcessJobAsync(job, stoppingToken).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
{
|
|
_logger.LogError(ex,
|
|
"Error processing gate evaluation job {JobId} for {Repository}@{Digest}",
|
|
job.JobId,
|
|
job.Request.Repository,
|
|
job.Request.ImageDigest);
|
|
}
|
|
}
|
|
|
|
_logger.LogInformation("Gate evaluation worker stopping");
|
|
}
|
|
|
|
private async Task ProcessJobAsync(GateEvaluationJob job, CancellationToken cancellationToken)
|
|
{
|
|
_logger.LogInformation(
|
|
"Processing gate evaluation job {JobId} for {Repository}@{Digest}",
|
|
job.JobId,
|
|
job.Request.Repository,
|
|
job.Request.ImageDigest);
|
|
|
|
using var scope = _scopeFactory.CreateScope();
|
|
var evaluator = scope.ServiceProvider.GetRequiredService<IDriftGateEvaluator>();
|
|
|
|
// Build a minimal context for the gate evaluation.
|
|
// In production, this would involve:
|
|
// 1. Fetching or triggering a scan of the image
|
|
// 2. Computing the reachability delta against the baseline
|
|
// 3. Building the DriftGateContext with actual metrics
|
|
//
|
|
// For now, we create a placeholder context that represents "no drift detected"
|
|
// which allows the gate to pass. The full implementation requires Scanner integration.
|
|
var driftContext = new DriftGateContext
|
|
{
|
|
DeltaReachable = 0,
|
|
DeltaUnreachable = 0,
|
|
HasKevReachable = false,
|
|
BaseScanId = job.Request.BaselineRef,
|
|
HeadScanId = job.Request.ImageDigest
|
|
};
|
|
|
|
var evalRequest = new DriftGateRequest
|
|
{
|
|
Context = driftContext,
|
|
PolicyId = null, // Use default policy
|
|
AllowOverride = false
|
|
};
|
|
|
|
var result = await evaluator.EvaluateAsync(evalRequest, cancellationToken).ConfigureAwait(false);
|
|
|
|
_logger.LogInformation(
|
|
"Gate evaluation {JobId} completed: Decision={Decision}, GateCount={GateCount}",
|
|
job.JobId,
|
|
result.Decision,
|
|
result.Gates.Length);
|
|
|
|
// TODO: Store result and notify via webhook/event
|
|
// This will be implemented in CICD-GATE-03
|
|
}
|
|
}
|