Resolve Client.Dotnet-001, -002, -003 code-review findings
Client.Dotnet-001: MapRpcException typed only Unauthenticated and PermissionDenied; every other gRPC status collapsed to an untyped exception with the status code discarded. Added a nullable StatusCode to MxGatewayException, extracted the duplicated mappers into a shared RpcExceptionMapper that records the code for every status, and documented it. Client.Dotnet-002: the production retry branch (MxGatewayException wrapping RpcException) was never exercised. FakeGatewayTransport gained a MapTransportExceptions mode that runs thrown RpcExceptions through RpcExceptionMapper exactly as the production transport does. Client.Dotnet-003: MxGatewaySession.DisposeAsync disposed _closeLock while a concurrent CloseAsync could be parked in WaitAsync. DisposeAsync now drains in-flight CloseAsync callers before disposing the semaphore; the client's _disposed flag is accessed via Interlocked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,10 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
{
|
||||
private readonly MxGatewayClient _client;
|
||||
private readonly SemaphoreSlim _closeLock = new(1, 1);
|
||||
private readonly object _disposeGate = new();
|
||||
private CloseSessionReply? _closeReply;
|
||||
private int _activeCloseCount;
|
||||
private bool _closeLockDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new session backed by the given MXAccess gateway client.
|
||||
@@ -46,23 +49,42 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
return _closeReply;
|
||||
}
|
||||
|
||||
await _closeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
// Register as an in-flight closer under the dispose gate. DisposeAsync waits for
|
||||
// _activeCloseCount to drain before disposing the close lock, so the semaphore is
|
||||
// guaranteed to outlive every WaitAsync started here.
|
||||
lock (_disposeGate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_closeLockDisposed, this);
|
||||
_activeCloseCount++;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_closeReply is not null)
|
||||
await _closeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (_closeReply is not null)
|
||||
{
|
||||
return _closeReply;
|
||||
}
|
||||
|
||||
_closeReply = await _client.CloseSessionRawAsync(
|
||||
new CloseSessionRequest { SessionId = SessionId },
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return _closeReply;
|
||||
}
|
||||
|
||||
_closeReply = await _client.CloseSessionRawAsync(
|
||||
new CloseSessionRequest { SessionId = SessionId },
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return _closeReply;
|
||||
finally
|
||||
{
|
||||
_closeLock.Release();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_closeLock.Release();
|
||||
lock (_disposeGate)
|
||||
{
|
||||
_activeCloseCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -658,7 +680,32 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
||||
/// </summary>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
lock (_disposeGate)
|
||||
{
|
||||
if (_closeLockDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await CloseAsync().ConfigureAwait(false);
|
||||
|
||||
// Wait for every concurrent CloseAsync caller to leave the close lock before
|
||||
// disposing it; once _closeReply is set those callers return without awaiting.
|
||||
while (true)
|
||||
{
|
||||
lock (_disposeGate)
|
||||
{
|
||||
if (_activeCloseCount == 0)
|
||||
{
|
||||
_closeLockDisposed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Yield();
|
||||
}
|
||||
|
||||
_closeLock.Dispose();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user