up
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using StellaOps.Policy.Engine.ExceptionCache;
|
||||
using StellaOps.Policy.Engine.Events;
|
||||
using StellaOps.Policy.Engine.Options;
|
||||
using StellaOps.Policy.Engine.Storage.Mongo.Repositories;
|
||||
using StellaOps.Policy.Engine.Telemetry;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Workers;
|
||||
|
||||
/// <summary>
|
||||
/// Executes activation/expiry flows for exceptions and emits lifecycle events.
|
||||
/// Split from the hosted worker for testability.
|
||||
/// </summary>
|
||||
internal sealed class ExceptionLifecycleService
|
||||
{
|
||||
private readonly IExceptionRepository _repository;
|
||||
private readonly IExceptionEventPublisher _publisher;
|
||||
private readonly IOptions<PolicyEngineOptions> _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<ExceptionLifecycleService> _logger;
|
||||
|
||||
public ExceptionLifecycleService(
|
||||
IExceptionRepository repository,
|
||||
IExceptionEventPublisher publisher,
|
||||
IOptions<PolicyEngineOptions> options,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<ExceptionLifecycleService> logger)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
_publisher = publisher ?? throw new ArgumentNullException(nameof(publisher));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task ProcessOnceAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var lifecycle = _options.Value.ExceptionLifecycle;
|
||||
|
||||
var pendingActivations = await _repository
|
||||
.ListExceptionsAsync(new ExceptionQueryOptions
|
||||
{
|
||||
Statuses = ImmutableArray.Create("approved"),
|
||||
}, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
pendingActivations = pendingActivations
|
||||
.Where(ex => ex.EffectiveFrom is null || ex.EffectiveFrom <= now)
|
||||
.Take(lifecycle.MaxBatchSize)
|
||||
.ToImmutableArray();
|
||||
|
||||
foreach (var ex in pendingActivations)
|
||||
{
|
||||
var activated = await _repository.UpdateExceptionStatusAsync(
|
||||
ex.TenantId, ex.Id, "active", now, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!activated)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PolicyEngineTelemetry.RecordExceptionLifecycle(ex.TenantId, "activated");
|
||||
await _publisher.PublishAsync(new ExceptionEvent
|
||||
{
|
||||
EventType = "activated",
|
||||
TenantId = ex.TenantId,
|
||||
ExceptionId = ex.Id,
|
||||
ExceptionName = ex.Name,
|
||||
ExceptionType = ex.ExceptionType,
|
||||
OccurredAt = now,
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Activated exception {ExceptionId} for tenant {TenantId} (effective from {EffectiveFrom:o})",
|
||||
ex.Id,
|
||||
ex.TenantId,
|
||||
ex.EffectiveFrom);
|
||||
}
|
||||
|
||||
var expiryWindowStart = now - lifecycle.ExpiryLookback;
|
||||
var expiryWindowEnd = now + lifecycle.ExpiryHorizon;
|
||||
|
||||
var expiring = await _repository
|
||||
.ListExceptionsAsync(new ExceptionQueryOptions
|
||||
{
|
||||
Statuses = ImmutableArray.Create("active"),
|
||||
}, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
expiring = expiring
|
||||
.Where(ex => ex.ExpiresAt is not null && ex.ExpiresAt >= expiryWindowStart && ex.ExpiresAt <= expiryWindowEnd)
|
||||
.Take(lifecycle.MaxBatchSize)
|
||||
.ToImmutableArray();
|
||||
|
||||
foreach (var ex in expiring)
|
||||
{
|
||||
var expired = await _repository.UpdateExceptionStatusAsync(
|
||||
ex.TenantId, ex.Id, "expired", now, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!expired)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PolicyEngineTelemetry.RecordExceptionLifecycle(ex.TenantId, "expired");
|
||||
await _publisher.PublishAsync(new ExceptionEvent
|
||||
{
|
||||
EventType = "expired",
|
||||
TenantId = ex.TenantId,
|
||||
ExceptionId = ex.Id,
|
||||
ExceptionName = ex.Name,
|
||||
ExceptionType = ex.ExceptionType,
|
||||
OccurredAt = now,
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Expired exception {ExceptionId} for tenant {TenantId} at {ExpiresAt:o}",
|
||||
ex.Id,
|
||||
ex.TenantId,
|
||||
ex.ExpiresAt);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Policy.Engine.Options;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Workers;
|
||||
|
||||
/// <summary>
|
||||
/// Hosted service that periodically runs exception activation/expiry checks.
|
||||
/// </summary>
|
||||
internal sealed class ExceptionLifecycleWorker : BackgroundService
|
||||
{
|
||||
private readonly ExceptionLifecycleService _service;
|
||||
private readonly PolicyEngineExceptionLifecycleOptions _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<ExceptionLifecycleWorker> _logger;
|
||||
|
||||
public ExceptionLifecycleWorker(
|
||||
ExceptionLifecycleService service,
|
||||
PolicyEngineExceptionLifecycleOptions options,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<ExceptionLifecycleWorker> logger)
|
||||
{
|
||||
_service = service ?? throw new ArgumentNullException(nameof(service));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("Starting exception lifecycle worker (interval {Interval}s)", _options.PollIntervalSeconds);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _service.ProcessOnceAsync(stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Exception lifecycle worker iteration failed");
|
||||
}
|
||||
|
||||
await Task.Delay(_options.PollInterval, stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user