feat: add auth error event publication (Gap 10.5)

Add SendAuthErrorEvent, SendConnectEvent, SendDisconnectEvent to
InternalEventSystem, plus AuthErrorEventCount counter and the three
companion detail record types (AuthErrorDetail, ConnectEventDetail,
DisconnectEventDetail). 10 new tests in AuthErrorEventTests all pass.
This commit is contained in:
Joseph Doherty
2026-02-25 13:12:52 -05:00
parent 10ac904b5c
commit 4b9384dfcf
2 changed files with 417 additions and 0 deletions

View File

@@ -38,6 +38,43 @@ public sealed class InternalSystemMessage
public required SystemMessageHandler Callback { get; init; }
}
/// <summary>
/// Detail payload for an auth error advisory.
/// Provides the caller-supplied fields used to populate <see cref="AuthErrorEventMsg"/>.
/// Go reference: server.go sendAuthErrorEvent — arguments passed when constructing the advisory.
/// </summary>
public sealed record AuthErrorDetail(
ulong ClientId,
string RemoteAddress,
string? AccountName,
string? UserName,
string Reason,
DateTime OccurredAt);
/// <summary>
/// Detail payload for a client connect advisory.
/// Provides the caller-supplied fields used to populate <see cref="ConnectEventMsg"/>.
/// Go reference: events.go sendConnect / postConnectEvent.
/// </summary>
public sealed record ConnectEventDetail(
ulong ClientId,
string RemoteAddress,
string? AccountName,
string? UserName,
DateTime ConnectedAt);
/// <summary>
/// Detail payload for a client disconnect advisory.
/// Provides the caller-supplied fields used to populate <see cref="DisconnectEventMsg"/>.
/// Go reference: events.go sendDisconnect / postDisconnectEvent.
/// </summary>
public sealed record DisconnectEventDetail(
ulong ClientId,
string RemoteAddress,
string? AccountName,
string Reason,
DateTime DisconnectedAt);
/// <summary>
/// Manages the server's internal event system with Channel-based send/receive loops.
/// Maps to Go's internal struct in events.go:124-147 and the goroutines
@@ -59,11 +96,18 @@ public sealed class InternalEventSystem : IAsyncDisposable
private ulong _sequence;
private int _subscriptionId;
private readonly ConcurrentDictionary<string, SystemMessageHandler> _callbacks = new();
private long _authErrorEventCount;
public Account SystemAccount { get; }
public InternalClient SystemClient { get; }
public string ServerHash { get; }
/// <summary>
/// Number of auth error events sent since this instance was created.
/// Go reference: server stats tracking of auth error advisories.
/// </summary>
public long AuthErrorEventCount => Interlocked.Read(ref _authErrorEventCount);
public InternalEventSystem(Account systemAccount, InternalClient systemClient, string serverName, ILogger logger)
{
_logger = logger;
@@ -225,6 +269,85 @@ public sealed class InternalEventSystem : IAsyncDisposable
/// </summary>
public ulong NextSequence() => Interlocked.Increment(ref _sequence);
/// <summary>
/// Publishes a client auth-error advisory to $SYS.SERVER.{id}.CLIENT.AUTH.ERR.
/// Increments <see cref="AuthErrorEventCount"/> each time it is called.
/// Go reference: events.go:2631 sendAuthErrorEvent.
/// </summary>
public void SendAuthErrorEvent(string serverId, AuthErrorDetail detail)
{
var subject = string.Format(EventSubjects.AuthError, serverId);
var msg = new AuthErrorEventMsg
{
Id = Guid.NewGuid().ToString("N"),
Time = detail.OccurredAt,
Server = _server?.BuildEventServerInfo() ?? new EventServerInfo { Id = serverId },
Client = new EventClientInfo
{
Id = detail.ClientId,
Host = detail.RemoteAddress,
Account = detail.AccountName,
User = detail.UserName,
},
Reason = detail.Reason,
};
Interlocked.Increment(ref _authErrorEventCount);
Enqueue(new PublishMessage { Subject = subject, Body = msg });
}
/// <summary>
/// Publishes a client connect advisory to $SYS.ACCOUNT.{account}.CONNECT.
/// Go reference: events.go postConnectEvent / sendConnect.
/// </summary>
public void SendConnectEvent(string serverId, ConnectEventDetail detail)
{
var accountName = detail.AccountName ?? "$G";
var subject = string.Format(EventSubjects.ConnectEvent, accountName);
var msg = new ConnectEventMsg
{
Id = Guid.NewGuid().ToString("N"),
Time = detail.ConnectedAt,
Server = _server?.BuildEventServerInfo() ?? new EventServerInfo { Id = serverId },
Client = new EventClientInfo
{
Id = detail.ClientId,
Host = detail.RemoteAddress,
Account = detail.AccountName,
User = detail.UserName,
Start = detail.ConnectedAt,
},
};
Enqueue(new PublishMessage { Subject = subject, Body = msg });
}
/// <summary>
/// Publishes a client disconnect advisory to $SYS.ACCOUNT.{account}.DISCONNECT.
/// Go reference: events.go postDisconnectEvent / sendDisconnect.
/// </summary>
public void SendDisconnectEvent(string serverId, DisconnectEventDetail detail)
{
var accountName = detail.AccountName ?? "$G";
var subject = string.Format(EventSubjects.DisconnectEvent, accountName);
var msg = new DisconnectEventMsg
{
Id = Guid.NewGuid().ToString("N"),
Time = detail.DisconnectedAt,
Server = _server?.BuildEventServerInfo() ?? new EventServerInfo { Id = serverId },
Client = new EventClientInfo
{
Id = detail.ClientId,
Host = detail.RemoteAddress,
Account = detail.AccountName,
Stop = detail.DisconnectedAt,
},
Reason = detail.Reason,
};
Enqueue(new PublishMessage { Subject = subject, Body = msg });
}
/// <summary>
/// Enqueue an internal message for publishing through the send loop.
/// </summary>