fix: address code quality issues from review

- Make ConnectReceived thread-safe with Volatile.Read/Write (accessed from auth timeout task and command pipeline)
- Include authTimeoutTask in Task.WhenAny to propagate exceptions
- Clear nonce after authentication with CryptographicOperations.ZeroMemory
- Avoid closure allocation on publish permission cache hot path (method group)
- Update AuthTimeout default to 2s to match Go server
This commit is contained in:
Joseph Doherty
2026-02-22 23:07:31 -05:00
parent c40c2cd994
commit 543b185f7e
4 changed files with 17 additions and 6 deletions

View File

@@ -34,7 +34,7 @@ public sealed class ClientPermissions : IDisposable
if (_publish == null)
return true;
return _pubCache.GetOrAdd(subject, s => _publish.IsAllowed(s));
return _pubCache.GetOrAdd(subject, _publish.IsAllowed);
}
public bool IsSubscribeAllowed(string subject, string? queue = null)

View File

@@ -1,6 +1,7 @@
using System.Buffers;
using System.IO.Pipelines;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
@@ -40,9 +41,12 @@ public sealed class NatsClient : IDisposable
public ulong Id { get; }
public ClientOptions? ClientOpts { get; private set; }
public IMessageRouter? Router { get; set; }
public bool ConnectReceived { get; private set; }
public Account? Account { get; private set; }
// Thread-safe: read from auth timeout task on threadpool, written from command pipeline
private int _connectReceived;
public bool ConnectReceived => Volatile.Read(ref _connectReceived) != 0;
// Stats
public long InMsgs;
public long OutMsgs;
@@ -106,7 +110,10 @@ public sealed class NatsClient : IDisposable
var processTask = ProcessCommandsAsync(pipe.Reader, _clientCts.Token);
var pingTask = RunPingTimerAsync(_clientCts.Token);
await Task.WhenAny(fillTask, processTask, pingTask);
if (authTimeoutTask != null)
await Task.WhenAny(fillTask, processTask, pingTask, authTimeoutTask);
else
await Task.WhenAny(fillTask, processTask, pingTask);
}
catch (OperationCanceledException)
{
@@ -258,6 +265,10 @@ public sealed class NatsClient : IDisposable
}
_logger.LogDebug("Client {ClientId} authenticated as {Identity}", Id, result.Identity);
// Clear nonce after use — defense-in-depth against memory dumps
if (_nonce != null)
CryptographicOperations.ZeroMemory(_nonce);
}
// If no account was assigned by auth, assign global account
@@ -267,7 +278,7 @@ public sealed class NatsClient : IDisposable
Account.AddClient(Id);
}
ConnectReceived = true;
Volatile.Write(ref _connectReceived, 1);
_logger.LogDebug("CONNECT received from client {ClientId}, name={ClientName}", Id, ClientOpts?.Name);
}

View File

@@ -26,5 +26,5 @@ public sealed class NatsOptions
public string? NoAuthUser { get; set; }
// Auth timing
public TimeSpan AuthTimeout { get; set; } = TimeSpan.FromSeconds(1);
public TimeSpan AuthTimeout { get; set; } = TimeSpan.FromSeconds(2);
}

View File

@@ -16,6 +16,6 @@ public class AuthConfigTests
opts.Users.ShouldBeNull();
opts.NKeys.ShouldBeNull();
opts.NoAuthUser.ShouldBeNull();
opts.AuthTimeout.ShouldBe(TimeSpan.FromSeconds(1));
opts.AuthTimeout.ShouldBe(TimeSpan.FromSeconds(2));
}
}