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;
|
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 Microsoft.Extensions.Logging;
|
||||||
using StellaOps.Policy.Engine.Adapters;
|
using StellaOps.Policy.Engine.Adapters;
|
||||||
using StellaOps.Policy.Engine.Evaluation;
|
using StellaOps.Policy.Engine.Evaluation;
|
||||||
|
|||||||
Reference in New Issue
Block a user