feat: validate single-client broadcast operations
Build web service image / build (push) Successful in 1m9s
Build web service image / build (push) Successful in 1m9s
This commit is contained in:
@@ -6,11 +6,13 @@ using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace WxAgent.Service;
|
||||
|
||||
public sealed record OperationExecutionResult(string? Details = null, string? ErrorCode = null, bool Unconfirmed = false);
|
||||
|
||||
public sealed class OperationQueue(OperationStore store, ServiceSecurity security) : BackgroundService
|
||||
{
|
||||
private const int QueueCapacity = 100;
|
||||
private sealed record Work(OperationRecord Record, ServiceIdentity Identity, string Permission,
|
||||
Func<CancellationToken, Task> Action, CancellationTokenSource Cancel);
|
||||
Func<CancellationToken, Task<OperationExecutionResult>> Action, CancellationTokenSource Cancel);
|
||||
private readonly Channel<Work> queue = Channel.CreateBounded<Work>(new BoundedChannelOptions(QueueCapacity)
|
||||
{ SingleReader = true, FullMode = BoundedChannelFullMode.Wait });
|
||||
private readonly ConcurrentDictionary<string, CancellationTokenSource> cancellations = new();
|
||||
@@ -18,7 +20,17 @@ public sealed class OperationQueue(OperationStore store, ServiceSecurity securit
|
||||
private bool stopping;
|
||||
|
||||
public OperationRecord Submit(ServiceIdentity identity, string accountId, AgentCapability capability,
|
||||
string? idempotencyKey, string canonicalParameters, Func<CancellationToken, Task> action)
|
||||
string? idempotencyKey, string canonicalParameters, Func<CancellationToken, Task> action) =>
|
||||
SubmitResult(identity, accountId, capability, idempotencyKey, canonicalParameters, null,
|
||||
async cancellationToken =>
|
||||
{
|
||||
await action(cancellationToken).ConfigureAwait(false);
|
||||
return new OperationExecutionResult();
|
||||
});
|
||||
|
||||
public OperationRecord SubmitResult(ServiceIdentity identity, string accountId, AgentCapability capability,
|
||||
string? idempotencyKey, string canonicalParameters, string? initialDetails,
|
||||
Func<CancellationToken, Task<OperationExecutionResult>> action)
|
||||
{
|
||||
security.RequireCurrent(identity, capability.Permission, accountId);
|
||||
if (!capability.Enabled) throw new ServiceException("CapabilityDisabled", 409, capability.DisabledReason ?? "Capability unavailable.");
|
||||
@@ -30,7 +42,7 @@ public sealed class OperationQueue(OperationStore store, ServiceSecurity securit
|
||||
{
|
||||
if (stopping) throw new ServiceException("Unavailable", 503, "Agent is stopping.");
|
||||
var (record, created) = store.Enqueue(identity.PrincipalId, accountId, capability.Operation,
|
||||
idempotencyKey, digest, capability.HasSideEffects, TimeSpan.FromSeconds(capability.TimeoutSeconds), QueueCapacity);
|
||||
idempotencyKey, digest, capability.HasSideEffects, TimeSpan.FromSeconds(capability.TimeoutSeconds), QueueCapacity, initialDetails);
|
||||
if (!created) return record;
|
||||
var cancel = new CancellationTokenSource();
|
||||
cancellations[record.Id] = cancel;
|
||||
@@ -92,10 +104,16 @@ public sealed class OperationQueue(OperationStore store, ServiceSecurity securit
|
||||
started = true;
|
||||
}
|
||||
// Never release this slot with WaitAsync: the actual action must have stopped first.
|
||||
await work.Action(budget.Token);
|
||||
var result = await work.Action(budget.Token);
|
||||
if (result.ErrorCode is not null)
|
||||
{
|
||||
store.Transition(work.Record.Id, result.Unconfirmed ? "Unconfirmed" : "Failed",
|
||||
"execution", result.ErrorCode, result.Details);
|
||||
continue;
|
||||
}
|
||||
budget.Token.ThrowIfCancellationRequested();
|
||||
security.RequireCurrent(work.Identity, work.Permission, work.Record.AccountId);
|
||||
store.Transition(work.Record.Id, "Succeeded", "complete");
|
||||
store.Transition(work.Record.Id, "Succeeded", "complete", details: result.Details);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user