// ----------------------------------------------------------------------------- // 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; /// /// In-memory implementation of the gate evaluation queue. /// Uses System.Threading.Channels for async producer-consumer pattern. /// public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue { private readonly Channel _channel; private readonly ILogger _logger; private readonly TimeProvider _timeProvider; public InMemoryGateEvaluationQueue( ILogger logger, TimeProvider? timeProvider = null) { ArgumentNullException.ThrowIfNull(logger); _logger = logger; _timeProvider = timeProvider ?? TimeProvider.System; // Bounded channel to prevent unbounded memory growth _channel = Channel.CreateBounded(new BoundedChannelOptions(1000) { FullMode = BoundedChannelFullMode.Wait, SingleReader = false, SingleWriter = false }); } /// public async Task 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; } /// /// Gets the channel reader for consuming jobs. /// public ChannelReader 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}"; } } /// /// A gate evaluation job in the queue. /// public sealed record GateEvaluationJob { public required string JobId { get; init; } public required GateEvaluationRequest Request { get; init; } public required DateTimeOffset QueuedAt { get; init; } } /// /// Background service that processes gate evaluation jobs from the queue. /// Orchestrates: image analysis -> drift delta computation -> gate evaluation. /// public sealed class GateEvaluationWorker : BackgroundService { private readonly InMemoryGateEvaluationQueue _queue; private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _logger; public GateEvaluationWorker( InMemoryGateEvaluationQueue queue, IServiceScopeFactory scopeFactory, ILogger 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(); // 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 } }