consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
using StellaOps.RiskEngine.Core.Contracts;
|
||||
|
||||
namespace StellaOps.RiskEngine.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Persists risk score results for later retrieval/ledger projection.
|
||||
/// Implementations must be deterministic and side‑effect free for identical inputs.
|
||||
/// </summary>
|
||||
public interface IRiskScoreResultStore
|
||||
{
|
||||
Task SaveAsync(RiskScoreResult result, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to read a previously persisted result by job identifier.
|
||||
/// Implementations must be deterministic and side-effect free.
|
||||
/// </summary>
|
||||
bool TryGet(Guid jobId, out RiskScoreResult result);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
|
||||
using StellaOps.RiskEngine.Core.Contracts;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace StellaOps.RiskEngine.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic FIFO queue for risk score jobs.
|
||||
/// </summary>
|
||||
public sealed class RiskScoreQueue
|
||||
{
|
||||
private readonly Channel<RiskScoreJob> channel;
|
||||
|
||||
public RiskScoreQueue(int? capacity = null)
|
||||
{
|
||||
if (capacity.HasValue)
|
||||
{
|
||||
var options = new BoundedChannelOptions(capacity.Value)
|
||||
{
|
||||
AllowSynchronousContinuations = false,
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
FullMode = BoundedChannelFullMode.Wait
|
||||
};
|
||||
|
||||
channel = Channel.CreateBounded<RiskScoreJob>(options);
|
||||
}
|
||||
else
|
||||
{
|
||||
var options = new UnboundedChannelOptions
|
||||
{
|
||||
AllowSynchronousContinuations = false,
|
||||
SingleReader = true,
|
||||
SingleWriter = false
|
||||
};
|
||||
|
||||
channel = Channel.CreateUnbounded<RiskScoreJob>(options);
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask EnqueueAsync(ScoreRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
var job = new RiskScoreJob(Guid.NewGuid(), request);
|
||||
return channel.Writer.WriteAsync(job, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues a request and returns the assigned job id for later retrieval.
|
||||
/// </summary>
|
||||
public async ValueTask<Guid> EnqueueWithIdAsync(ScoreRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
var job = new RiskScoreJob(Guid.NewGuid(), request);
|
||||
await channel.Writer.WriteAsync(job, cancellationToken).ConfigureAwait(false);
|
||||
return job.JobId;
|
||||
}
|
||||
|
||||
public ValueTask<RiskScoreJob> DequeueAsync(CancellationToken cancellationToken) =>
|
||||
channel.Reader.ReadAsync(cancellationToken);
|
||||
|
||||
public bool TryDequeue([NotNullWhen(true)] out RiskScoreJob? job) => channel.Reader.TryRead(out job);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using StellaOps.RiskEngine.Core.Contracts;
|
||||
using StellaOps.RiskEngine.Core.Providers;
|
||||
|
||||
namespace StellaOps.RiskEngine.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Single-reader worker that pulls jobs from the queue and executes providers deterministically.
|
||||
/// </summary>
|
||||
public sealed class RiskScoreWorker
|
||||
{
|
||||
private readonly RiskScoreQueue queue;
|
||||
private readonly IRiskScoreProviderRegistry registry;
|
||||
private readonly IRiskScoreResultStore? resultStore;
|
||||
private readonly TimeProvider timeProvider;
|
||||
|
||||
public RiskScoreWorker(
|
||||
RiskScoreQueue queue,
|
||||
IRiskScoreProviderRegistry registry,
|
||||
IRiskScoreResultStore? resultStore = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
this.queue = queue ?? throw new ArgumentNullException(nameof(queue));
|
||||
this.registry = registry ?? throw new ArgumentNullException(nameof(registry));
|
||||
this.resultStore = resultStore;
|
||||
this.timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<RiskScoreResult> ProcessNextAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var job = await queue.DequeueAsync(cancellationToken).ConfigureAwait(false);
|
||||
var request = job.Request;
|
||||
|
||||
RiskScoreResult Build(double score, bool success, string? error) =>
|
||||
new(
|
||||
job.JobId,
|
||||
request.Provider,
|
||||
request.Subject,
|
||||
Score: score,
|
||||
Success: success,
|
||||
Error: error,
|
||||
request.Signals,
|
||||
CompletedAtUtc: timeProvider.GetUtcNow());
|
||||
|
||||
if (!registry.TryGet(request.Provider, out var provider))
|
||||
{
|
||||
var missing = Build(0d, false, "Provider not registered");
|
||||
await PersistAsync(missing, cancellationToken).ConfigureAwait(false);
|
||||
return missing;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var score = await provider.ScoreAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
var success = Build(score, true, null);
|
||||
await PersistAsync(success, cancellationToken).ConfigureAwait(false);
|
||||
return success;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var failure = Build(0d, false, ex.Message);
|
||||
await PersistAsync(failure, cancellationToken).ConfigureAwait(false);
|
||||
return failure;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<RiskScoreResult>> ProcessBatchAsync(int expectedCount, CancellationToken cancellationToken)
|
||||
{
|
||||
if (expectedCount < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(expectedCount));
|
||||
}
|
||||
|
||||
var results = new List<RiskScoreResult>(expectedCount);
|
||||
for (var i = 0; i < expectedCount; i++)
|
||||
{
|
||||
results.Add(await ProcessNextAsync(cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private Task PersistAsync(RiskScoreResult result, CancellationToken cancellationToken) =>
|
||||
resultStore is null
|
||||
? Task.CompletedTask
|
||||
: resultStore.SaveAsync(result, cancellationToken);
|
||||
}
|
||||
Reference in New Issue
Block a user