feat: add -ERR response infrastructure with SendErrAsync and SendErrAndCloseAsync
This commit is contained in:
@@ -29,6 +29,7 @@ public sealed class NatsClient : IDisposable
|
|||||||
private readonly ServerInfo _serverInfo;
|
private readonly ServerInfo _serverInfo;
|
||||||
private readonly NatsParser _parser;
|
private readonly NatsParser _parser;
|
||||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||||
|
private CancellationTokenSource? _clientCts;
|
||||||
private readonly Dictionary<string, Subscription> _subs = new();
|
private readonly Dictionary<string, Subscription> _subs = new();
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
@@ -58,15 +59,16 @@ public sealed class NatsClient : IDisposable
|
|||||||
|
|
||||||
public async Task RunAsync(CancellationToken ct)
|
public async Task RunAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
_clientCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||||
var pipe = new Pipe();
|
var pipe = new Pipe();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Send INFO
|
// Send INFO
|
||||||
await SendInfoAsync(ct);
|
await SendInfoAsync(_clientCts.Token);
|
||||||
|
|
||||||
// Start read pump and command processing in parallel
|
// Start read pump and command processing in parallel
|
||||||
var fillTask = FillPipeAsync(pipe.Writer, ct);
|
var fillTask = FillPipeAsync(pipe.Writer, _clientCts.Token);
|
||||||
var processTask = ProcessCommandsAsync(pipe.Reader, ct);
|
var processTask = ProcessCommandsAsync(pipe.Reader, _clientCts.Token);
|
||||||
|
|
||||||
await Task.WhenAny(fillTask, processTask);
|
await Task.WhenAny(fillTask, processTask);
|
||||||
}
|
}
|
||||||
@@ -81,6 +83,14 @@ public sealed class NatsClient : IDisposable
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
Router?.RemoveClient(this);
|
Router?.RemoveClient(this);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_socket.Shutdown(SocketShutdown.Both);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Client {ClientId} socket shutdown error", Id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +292,36 @@ public sealed class NatsClient : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SendErrAsync(string message)
|
||||||
|
{
|
||||||
|
var errLine = Encoding.ASCII.GetBytes($"-ERR '{message}'\r\n");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await WriteAsync(errLine, _clientCts?.Token ?? CancellationToken.None);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Expected during shutdown
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Client {ClientId} failed to send -ERR", Id);
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Client {ClientId} failed to send -ERR (disposed)", Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendErrAndCloseAsync(string message)
|
||||||
|
{
|
||||||
|
await SendErrAsync(message);
|
||||||
|
if (_clientCts is { } cts)
|
||||||
|
await cts.CancelAsync();
|
||||||
|
else
|
||||||
|
_socket.Close();
|
||||||
|
}
|
||||||
|
|
||||||
public void RemoveAllSubscriptions(SubList subList)
|
public void RemoveAllSubscriptions(SubList subList)
|
||||||
{
|
{
|
||||||
foreach (var sub in _subs.Values)
|
foreach (var sub in _subs.Values)
|
||||||
@@ -291,6 +331,7 @@ public sealed class NatsClient : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
_clientCts?.Dispose();
|
||||||
_stream.Dispose();
|
_stream.Dispose();
|
||||||
_socket.Dispose();
|
_socket.Dispose();
|
||||||
_writeLock.Dispose();
|
_writeLock.Dispose();
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ public static class NatsProtocol
|
|||||||
public static readonly byte[] MsgPrefix = "MSG "u8.ToArray();
|
public static readonly byte[] MsgPrefix = "MSG "u8.ToArray();
|
||||||
public static readonly byte[] HmsgPrefix = "HMSG "u8.ToArray();
|
public static readonly byte[] HmsgPrefix = "HMSG "u8.ToArray();
|
||||||
public static readonly byte[] ErrPrefix = "-ERR "u8.ToArray();
|
public static readonly byte[] ErrPrefix = "-ERR "u8.ToArray();
|
||||||
|
|
||||||
|
// Standard error messages (matching Go server)
|
||||||
|
public const string ErrMaxConnectionsExceeded = "maximum connections exceeded";
|
||||||
|
public const string ErrStaleConnection = "Stale Connection";
|
||||||
|
public const string ErrMaxPayloadViolation = "Maximum Payload Violation";
|
||||||
|
public const string ErrInvalidPublishSubject = "Invalid Publish Subject";
|
||||||
|
public const string ErrInvalidSubject = "Invalid Subject";
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ServerInfo
|
public sealed class ServerInfo
|
||||||
|
|||||||
@@ -86,4 +86,48 @@ public class ClientTests : IAsyncDisposable
|
|||||||
|
|
||||||
await _cts.CancelAsync();
|
await _cts.CancelAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Client_SendErrAsync_writes_correct_wire_format()
|
||||||
|
{
|
||||||
|
var runTask = _natsClient.RunAsync(_cts.Token);
|
||||||
|
|
||||||
|
// Read INFO first
|
||||||
|
var buf = new byte[4096];
|
||||||
|
using var readCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
|
await _clientSocket.ReceiveAsync(buf, SocketFlags.None, readCts.Token);
|
||||||
|
|
||||||
|
// Trigger SendErrAsync
|
||||||
|
await _natsClient.SendErrAsync("Invalid Subject");
|
||||||
|
|
||||||
|
var n = await _clientSocket.ReceiveAsync(buf, SocketFlags.None, readCts.Token);
|
||||||
|
var response = Encoding.ASCII.GetString(buf, 0, n);
|
||||||
|
|
||||||
|
response.ShouldBe("-ERR 'Invalid Subject'\r\n");
|
||||||
|
|
||||||
|
await _cts.CancelAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Client_SendErrAndCloseAsync_sends_error_then_disconnects()
|
||||||
|
{
|
||||||
|
var runTask = _natsClient.RunAsync(_cts.Token);
|
||||||
|
|
||||||
|
// Read INFO first
|
||||||
|
var buf = new byte[4096];
|
||||||
|
using var readCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
|
await _clientSocket.ReceiveAsync(buf, SocketFlags.None, readCts.Token);
|
||||||
|
|
||||||
|
// Trigger SendErrAndCloseAsync
|
||||||
|
await _natsClient.SendErrAndCloseAsync("maximum connections exceeded");
|
||||||
|
|
||||||
|
var n = await _clientSocket.ReceiveAsync(buf, SocketFlags.None, readCts.Token);
|
||||||
|
var response = Encoding.ASCII.GetString(buf, 0, n);
|
||||||
|
|
||||||
|
response.ShouldBe("-ERR 'maximum connections exceeded'\r\n");
|
||||||
|
|
||||||
|
// Connection should be closed — next read returns 0
|
||||||
|
n = await _clientSocket.ReceiveAsync(buf, SocketFlags.None, readCts.Token);
|
||||||
|
n.ShouldBe(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user