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
+9 -4
View File
@@ -33,6 +33,7 @@ public enum RemoteNodeStatus
public enum RemoteTaskStatus
{
Pending,
WaitingForClient,
Accepted,
Running,
Succeeded,
@@ -246,7 +247,8 @@ public sealed record RemoteNodeRegistration(
[property: JsonPropertyName("protocol_version")] string ProtocolVersion,
[property: JsonPropertyName("capabilities")] IReadOnlyList<string> Capabilities,
[property: JsonPropertyName("reporting_config_version")] long ReportingConfigVersion,
[property: JsonPropertyName("accounts")] IReadOnlyList<RemoteAccountSummary> Accounts);
[property: JsonPropertyName("accounts")] IReadOnlyList<RemoteAccountSummary> Accounts,
[property: JsonPropertyName("connection_id")] string? ConnectionId = null);
public sealed record RemoteAccountSummary(
[property: JsonPropertyName("account_id")] string AccountId,
@@ -259,13 +261,15 @@ public sealed record RemoteNodeRegistrationResponse(
[property: JsonPropertyName("node_id")] string NodeId,
[property: JsonPropertyName("status")] RemoteNodeStatus Status,
[property: JsonPropertyName("authenticated")] bool Authenticated,
[property: JsonPropertyName("correlation_id")] string CorrelationId);
[property: JsonPropertyName("correlation_id")] string CorrelationId,
[property: JsonPropertyName("connection_id")] string? ConnectionId = null);
public sealed record RemoteHeartbeatResponse(
[property: JsonPropertyName("node_id")] string NodeId,
[property: JsonPropertyName("status")] RemoteNodeStatus Status,
[property: JsonPropertyName("last_heartbeat_at")] DateTimeOffset LastHeartbeatAt,
[property: JsonPropertyName("correlation_id")] string CorrelationId);
[property: JsonPropertyName("correlation_id")] string CorrelationId,
[property: JsonPropertyName("connection_id")] string? ConnectionId = null);
public sealed record RemoteTaskBatch(
[property: JsonPropertyName("tasks")] IReadOnlyList<RemoteTaskEnvelope> Tasks);
@@ -282,7 +286,8 @@ public sealed record RemoteHeartbeat(
[property: JsonPropertyName("queue_length")] int QueueLength,
[property: JsonPropertyName("reporting_config_version")] long ReportingConfigVersion,
[property: JsonPropertyName("correlation_id")] string CorrelationId,
[property: JsonPropertyName("last_error_code")] string? LastErrorCode = null);
[property: JsonPropertyName("last_error_code")] string? LastErrorCode = null,
[property: JsonPropertyName("connection_id")] string? ConnectionId = null);
public sealed record RemoteTaskEnvelope(
[property: JsonPropertyName("task_id")] string TaskId,
@@ -22,6 +22,7 @@ public sealed class RemoteControlClient : IDisposable
private readonly Uri? _baseAddress;
private readonly X509Certificate2? _clientCertificate;
private readonly X509Certificate2? _serverCaCertificate;
private string? _connectionId;
private bool _authenticated;
public RemoteControlClient(RemoteAgentOptions options, HttpClient? httpClient = null)
@@ -57,7 +58,9 @@ public sealed class RemoteControlClient : IDisposable
AuthState = RemoteAuthState.Authenticating;
try
{
var response = await SendAsync<RemoteNodeRegistrationResponse>(HttpMethod.Post, "/v1/nodes/register", registration, false, cancellationToken);
var connectionId = registration.ConnectionId ?? NewConnectionId();
var response = await SendAsync<RemoteNodeRegistrationResponse>(HttpMethod.Post, "/v1/nodes/register", registration with { ConnectionId = connectionId }, false, cancellationToken);
_connectionId = response.ConnectionId ?? connectionId;
_authenticated = response.Authenticated;
AuthState = response.Authenticated ? RemoteAuthState.Authenticated : RemoteAuthState.AuthenticationFailed;
LastSuccessAt = DateTimeOffset.UtcNow;
@@ -75,7 +78,7 @@ public sealed class RemoteControlClient : IDisposable
public Task<RemoteHeartbeatResponse> HeartbeatAsync(RemoteHeartbeat heartbeat, CancellationToken cancellationToken = default) =>
SendAuthenticatedAsync<RemoteHeartbeatResponse>(HttpMethod.Post,
$"/v1/nodes/{Escape(heartbeat.NodeId)}/heartbeat", heartbeat, cancellationToken);
$"/v1/nodes/{Escape(heartbeat.NodeId)}/heartbeat", heartbeat with { ConnectionId = heartbeat.ConnectionId ?? _connectionId }, cancellationToken);
public async Task<IReadOnlyList<RemoteTaskEnvelope>> PollTasksAsync(
string accountId,
@@ -205,7 +208,7 @@ public sealed class RemoteControlClient : IDisposable
if (!response.IsSuccessStatusCode)
{
var error = TryDeserializeError(responseBytes, correlationId);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || error.Code == "StaleConnection")
{
_authenticated = false;
AuthState = RemoteAuthState.AuthenticationFailed;
@@ -292,6 +295,7 @@ public sealed class RemoteControlClient : IDisposable
}
private static string Escape(string value) => Uri.EscapeDataString(value);
private static string NewConnectionId() => Guid.NewGuid().ToString("N");
private static string NewCorrelationId() => Guid.NewGuid().ToString("N");
public void Dispose()