Files
git.stella-ops.org/src/Policy/StellaOps.Policy.Gateway/Services/InMemoryGateEvaluationQueue.cs
2026-02-01 21:37:40 +02:00

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
}
}