T4: Add BatchExceptionLoader for batch evaluation optimization
- Add IBatchExceptionLoader interface with PreLoadExceptionsAsync, GetExceptionsAsync, ClearBatchCache - Add BatchExceptionLoader using ConcurrentDictionary for batch-level caching - Add BatchExceptionLoaderOptions with EagerLoadThreshold and EnablePreWarming - Add AddBatchExceptionLoader DI extension in PolicyEngineServiceCollectionExtensions - Fix missing System.Collections.Immutable using in ExceptionAwareEvaluationService Sprint: 3900.0002.0001
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
// <copyright file="BatchExceptionLoader.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. All rights reserved.
|
||||
// Licensed under the AGPL-3.0-or-later license.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Policy.Engine.Adapters;
|
||||
using StellaOps.Policy.Engine.Evaluation;
|
||||
|
||||
namespace StellaOps.Policy.Engine.BatchEvaluation;
|
||||
|
||||
/// <summary>
|
||||
/// Options for batch exception loading.
|
||||
/// </summary>
|
||||
public sealed class BatchExceptionLoaderOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum number of items in a batch before forcing eager load. Default: 10.
|
||||
/// </summary>
|
||||
public int EagerLoadThreshold { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to pre-warm the cache for all tenants in the batch. Default: true.
|
||||
/// </summary>
|
||||
public bool EnablePreWarming { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for batch-optimized exception loading.
|
||||
/// </summary>
|
||||
internal interface IBatchExceptionLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// Pre-loads exceptions for all tenants in a batch to minimize database round-trips.
|
||||
/// </summary>
|
||||
/// <param name="tenantIds">Distinct tenant IDs from the batch.</param>
|
||||
/// <param name="asOf">Point in time for expiry filtering.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task PreLoadExceptionsAsync(
|
||||
IEnumerable<Guid> tenantIds,
|
||||
DateTimeOffset asOf,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets exceptions for a tenant, using pre-loaded cache if available.
|
||||
/// </summary>
|
||||
/// <param name="tenantId">Tenant identifier.</param>
|
||||
/// <param name="asOf">Point in time for expiry filtering.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Policy evaluation exceptions for the tenant.</returns>
|
||||
Task<PolicyEvaluationExceptions> GetExceptionsAsync(
|
||||
Guid tenantId,
|
||||
DateTimeOffset asOf,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the batch-level cache after processing is complete.
|
||||
/// </summary>
|
||||
void ClearBatchCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch-optimized exception loader that pre-warms the cache and provides
|
||||
/// memory-efficient access to exceptions across batch evaluation items.
|
||||
/// </summary>
|
||||
internal sealed class BatchExceptionLoader : IBatchExceptionLoader
|
||||
{
|
||||
private readonly IExceptionAdapter _adapter;
|
||||
private readonly BatchExceptionLoaderOptions _options;
|
||||
private readonly ILogger<BatchExceptionLoader> _logger;
|
||||
|
||||
// Batch-level cache to avoid repeated dictionary lookups during batch processing
|
||||
private readonly ConcurrentDictionary<Guid, PolicyEvaluationExceptions> _batchCache = new();
|
||||
|
||||
public BatchExceptionLoader(
|
||||
IExceptionAdapter adapter,
|
||||
Microsoft.Extensions.Options.IOptions<BatchExceptionLoaderOptions> options,
|
||||
ILogger<BatchExceptionLoader> logger)
|
||||
{
|
||||
_adapter = adapter ?? throw new ArgumentNullException(nameof(adapter));
|
||||
_options = options?.Value ?? new BatchExceptionLoaderOptions();
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task PreLoadExceptionsAsync(
|
||||
IEnumerable<Guid> tenantIds,
|
||||
DateTimeOffset asOf,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tenantIds);
|
||||
|
||||
var distinctTenants = tenantIds.Distinct().ToList();
|
||||
|
||||
if (!_options.EnablePreWarming || distinctTenants.Count < _options.EagerLoadThreshold)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Skipping pre-warm: EnablePreWarming={Enabled}, TenantCount={Count}, Threshold={Threshold}",
|
||||
_options.EnablePreWarming,
|
||||
distinctTenants.Count,
|
||||
_options.EagerLoadThreshold);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Pre-loading exceptions for {Count} tenants", distinctTenants.Count);
|
||||
|
||||
// Load exceptions for all tenants concurrently, but limit parallelism
|
||||
await Parallel.ForEachAsync(
|
||||
distinctTenants,
|
||||
new ParallelOptions
|
||||
{
|
||||
MaxDegreeOfParallelism = Math.Min(4, distinctTenants.Count),
|
||||
CancellationToken = cancellationToken
|
||||
},
|
||||
async (tenantId, ct) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var exceptions = await _adapter.LoadExceptionsAsync(tenantId, asOf, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// Store in batch cache for fast lookup during evaluation
|
||||
_batchCache.TryAdd(tenantId, exceptions);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to pre-load exceptions for tenant {TenantId}", tenantId);
|
||||
// Continue with other tenants; this tenant will load on-demand
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug("Pre-loaded exceptions for {Count} tenants into batch cache", _batchCache.Count);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PolicyEvaluationExceptions> GetExceptionsAsync(
|
||||
Guid tenantId,
|
||||
DateTimeOffset asOf,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Check batch cache first (populated by PreLoadExceptionsAsync)
|
||||
if (_batchCache.TryGetValue(tenantId, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Fall back to adapter (which has its own caching)
|
||||
var exceptions = await _adapter.LoadExceptionsAsync(tenantId, asOf, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// Add to batch cache for potential subsequent lookups in same batch
|
||||
_batchCache.TryAdd(tenantId, exceptions);
|
||||
|
||||
return exceptions;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearBatchCache()
|
||||
{
|
||||
var count = _batchCache.Count;
|
||||
_batchCache.Clear();
|
||||
_logger.LogDebug("Cleared batch cache with {Count} entries", count);
|
||||
}
|
||||
}
|
||||
@@ -318,4 +318,26 @@ public static class PolicyEngineServiceCollectionExtensions
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds batch exception loader services for optimized batch evaluation scenarios.
|
||||
/// Pre-warms exception cache when evaluating multiple findings in a single batch.
|
||||
/// </summary>
|
||||
/// <param name="services">Service collection.</param>
|
||||
/// <param name="configure">Optional configuration for batch exception loader options.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddBatchExceptionLoader(
|
||||
this IServiceCollection services,
|
||||
Action<BatchEvaluation.BatchExceptionLoaderOptions>? configure = null)
|
||||
{
|
||||
if (configure is not null)
|
||||
{
|
||||
services.Configure(configure);
|
||||
}
|
||||
|
||||
// Register the batch exception loader (transient - per-request batch scope)
|
||||
services.TryAddTransient<BatchEvaluation.IBatchExceptionLoader, BatchEvaluation.BatchExceptionLoader>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Policy.Engine.Adapters;
|
||||
using StellaOps.Policy.Engine.Evaluation;
|
||||
|
||||
Reference in New Issue
Block a user