fix(dotnet-client): no ack retry, caller-owned channels, safe session dispose, typed diagnostics, AttachSession
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using Grpc.Core;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Client;
|
||||
@@ -1452,6 +1453,106 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
return _client.InvokeAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a PING diagnostic command to the worker and throws if the reply is a failure.
|
||||
/// </summary>
|
||||
/// <param name="message">Optional message echoed to the worker; defaults to empty.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A task that completes when the worker acknowledges the ping.</returns>
|
||||
public async Task PingAsync(
|
||||
string? message = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
MxCommandReply reply = await InvokeCommandAsync(
|
||||
new MxCommand
|
||||
{
|
||||
Kind = MxCommandKind.Ping,
|
||||
Ping = new PingCommand { Message = message ?? string.Empty },
|
||||
},
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the gateway's lifecycle state for this session.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The current session state.</returns>
|
||||
public async Task<SessionState> GetSessionStateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
MxCommandReply reply = await InvokeCommandAsync(
|
||||
new MxCommand
|
||||
{
|
||||
Kind = MxCommandKind.GetSessionState,
|
||||
GetSessionState = new GetSessionStateCommand(),
|
||||
},
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||
|
||||
return reply.SessionState?.State
|
||||
?? throw CreateMalformed(reply, "GetSessionState");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets diagnostic information about the worker process serving this session.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The worker process id, version, and MXAccess COM identifiers.</returns>
|
||||
public async Task<WorkerInfoReply> GetWorkerInfoAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
MxCommandReply reply = await InvokeCommandAsync(
|
||||
new MxCommand
|
||||
{
|
||||
Kind = MxCommandKind.GetWorkerInfo,
|
||||
GetWorkerInfo = new GetWorkerInfoCommand(),
|
||||
},
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||
|
||||
return reply.WorkerInfo
|
||||
?? throw CreateMalformed(reply, "GetWorkerInfo");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drains events queued in the worker through the diagnostic command path. This removes
|
||||
/// events from the worker queue; it is not a substitute for <see cref="StreamEventsAsync"/>
|
||||
/// and is not retried automatically. The reply is bounded by server-side caps, so call
|
||||
/// repeatedly until it returns an empty list.
|
||||
/// </summary>
|
||||
/// <param name="maxEvents">Maximum events to return; 0 lets the server choose.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The drained events, possibly empty.</returns>
|
||||
public async Task<IReadOnlyList<MxEvent>> DrainEventsAsync(
|
||||
uint maxEvents = 0,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
MxCommandReply reply = await InvokeCommandAsync(
|
||||
new MxCommand
|
||||
{
|
||||
Kind = MxCommandKind.DrainEvents,
|
||||
DrainEvents = new DrainEventsCommand { MaxEvents = maxEvents },
|
||||
},
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||
|
||||
return reply.DrainEvents?.Events.ToArray() ?? [];
|
||||
}
|
||||
|
||||
private static MxGatewayMalformedReplyException CreateMalformed(MxCommandReply reply, string operation)
|
||||
{
|
||||
return new MxGatewayMalformedReplyException(
|
||||
$"{operation} returned a malformed reply: OK reply carried no typed payload",
|
||||
reply.SessionId,
|
||||
reply.CorrelationId,
|
||||
reply.ProtocolStatus,
|
||||
reply.HasHresult ? reply.Hresult : null,
|
||||
reply.Statuses.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Streams events from the worker for this session, optionally starting after a given sequence number.
|
||||
/// </summary>
|
||||
@@ -1505,8 +1606,20 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await CloseAsync().ConfigureAwait(false);
|
||||
_closeLock.Dispose();
|
||||
try
|
||||
{
|
||||
await CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (ex is MxGatewayException or ObjectDisposedException or RpcException)
|
||||
{
|
||||
// Best-effort close: the gateway may be unreachable or the owning client
|
||||
// already disposed. Disposal must not throw or leak the lock; the gateway
|
||||
// reaps the session on its own idle/detach timers.
|
||||
}
|
||||
finally
|
||||
{
|
||||
_closeLock.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private Task<MxCommandReply> InvokeCommandAsync(
|
||||
|
||||
Reference in New Issue
Block a user