feat: validate single-client broadcast operations
Build web service image / build (push) Successful in 1m9s

This commit is contained in:
2026-09-19 14:33:26 +08:00
parent e321d38fa3
commit c7c0ab273f
62 changed files with 2861 additions and 12651 deletions
+23 -5
View File
@@ -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)
{