feat(batch19): implement account trace/counter/export core methods

This commit is contained in:
Joseph Doherty
2026-02-28 19:47:10 -05:00
parent 6daa31adf2
commit dcf1df44d1
2 changed files with 319 additions and 0 deletions

View File

@@ -475,6 +475,29 @@ public sealed class Account : INatsAccount
}
}
/// <summary>
/// Sets account-level message trace destination subject.
/// Mirrors Go <c>(a *Account) setTraceDest(dest string)</c>.
/// </summary>
internal void SetTraceDest(string dest) => SetMessageTraceDestination(dest);
/// <summary>
/// Returns trace destination and sampling.
/// Mirrors Go <c>(a *Account) getTraceDestAndSampling() (string, int)</c>.
/// </summary>
internal (string Destination, int Sampling) GetTraceDestAndSampling()
{
_mu.EnterReadLock();
try
{
return (_traceDest, _traceDestSampling);
}
finally
{
_mu.ExitReadLock();
}
}
// -------------------------------------------------------------------------
// Factory
// -------------------------------------------------------------------------
@@ -502,6 +525,12 @@ public sealed class Account : INatsAccount
/// </summary>
public override string ToString() => Name;
/// <summary>
/// Returns the account name.
/// Mirrors Go <c>(a *Account) String() string</c>.
/// </summary>
public string String() => Name;
// -------------------------------------------------------------------------
// Shallow copy for config reload
// -------------------------------------------------------------------------
@@ -966,6 +995,12 @@ public sealed class Account : INatsAccount
internal int NumLocalConnectionsLocked() =>
(_clients?.Count ?? 0) - _sysclients - _nleafs;
/// <summary>
/// Returns local non-system, non-leaf client count. Lock must be held.
/// Mirrors Go <c>(a *Account) numLocalConnections() int</c>.
/// </summary>
internal int NumLocalConnectionsInternal() => NumLocalConnectionsLocked();
/// <summary>
/// Returns all local connections including leaf nodes (minus system clients).
/// Mirrors Go <c>(a *Account) numLocalAndLeafConnections() int</c>.
@@ -1049,6 +1084,13 @@ public sealed class Account : INatsAccount
return _nleafs + _nrleafs >= MaxLeafNodes;
}
/// <summary>
/// Returns true if total leaf-node count reached the configured maximum.
/// Lock must be held by the caller.
/// Mirrors Go <c>(a *Account) maxTotalLeafNodesReached() bool</c>.
/// </summary>
internal bool MaxTotalLeafNodesReachedInternal() => MaxTotalLeafNodesReachedLocked();
/// <summary>
/// Returns the total leaf-node count (local + remote).
/// Mirrors Go <c>(a *Account) NumLeafNodes() int</c>.
@@ -1115,6 +1157,93 @@ public sealed class Account : INatsAccount
}
}
/// <summary>
/// Returns true when there is at least one matching subscription for <paramref name="subject"/>.
/// Mirrors Go <c>(a *Account) SubscriptionInterest(subject string) bool</c>.
/// </summary>
public bool SubscriptionInterest(string subject) => Interest(subject) > 0;
/// <summary>
/// Returns total number of plain and queue subscriptions matching <paramref name="subject"/>.
/// Mirrors Go <c>(a *Account) Interest(subject string) int</c>.
/// </summary>
public int Interest(string subject)
{
_mu.EnterReadLock();
try
{
if (Sublist == null)
return 0;
var (np, nq) = Sublist.NumInterest(subject);
return np + nq;
}
finally
{
_mu.ExitReadLock();
}
}
/// <summary>
/// Increments the leaf-node count for a remote cluster.
/// Mirrors Go <c>(a *Account) registerLeafNodeCluster(cluster string)</c>.
/// </summary>
internal void RegisterLeafNodeCluster(string cluster)
{
_mu.EnterWriteLock();
try
{
_leafClusters ??= new Dictionary<string, ulong>(StringComparer.Ordinal);
_leafClusters.TryGetValue(cluster, out var current);
_leafClusters[cluster] = current + 1;
}
finally
{
_mu.ExitWriteLock();
}
}
/// <summary>
/// Returns true when this account already tracks one or more leaf nodes from <paramref name="cluster"/>.
/// Mirrors Go <c>(a *Account) hasLeafNodeCluster(cluster string) bool</c>.
/// </summary>
internal bool HasLeafNodeCluster(string cluster)
{
_mu.EnterReadLock();
try
{
return _leafClusters != null &&
_leafClusters.TryGetValue(cluster, out var count) &&
count > 0;
}
finally
{
_mu.ExitReadLock();
}
}
/// <summary>
/// Returns true when the account is leaf-cluster isolated to <paramref name="cluster"/>.
/// Mirrors Go <c>(a *Account) isLeafNodeClusterIsolated(cluster string) bool</c>.
/// </summary>
internal bool IsLeafNodeClusterIsolated(string cluster)
{
_mu.EnterReadLock();
try
{
if (string.IsNullOrEmpty(cluster))
return false;
if (_leafClusters == null || _leafClusters.Count > 1)
return false;
return _leafClusters.TryGetValue(cluster, out var count) && count == (ulong)_nleafs;
}
finally
{
_mu.ExitReadLock();
}
}
// -------------------------------------------------------------------------
// Subscription limit error throttle
// -------------------------------------------------------------------------
@@ -1460,6 +1589,167 @@ public sealed class Account : INatsAccount
}
}
// -------------------------------------------------------------------------
// Service export configuration
// -------------------------------------------------------------------------
/// <summary>
/// Configures an exported service with singleton response semantics.
/// Mirrors Go <c>(a *Account) AddServiceExport(subject string, accounts []*Account) error</c>.
/// </summary>
public Exception? AddServiceExport(string subject, IReadOnlyList<Account>? accounts = null) =>
AddServiceExportWithResponseAndAccountPos(subject, ServiceRespType.Singleton, accounts, 0);
/// <summary>
/// Configures an exported service with singleton response semantics and account-position auth.
/// Mirrors Go <c>(a *Account) addServiceExportWithAccountPos(...)</c>.
/// </summary>
public Exception? AddServiceExportWithAccountPos(string subject, IReadOnlyList<Account>? accounts, uint accountPos) =>
AddServiceExportWithResponseAndAccountPos(subject, ServiceRespType.Singleton, accounts, accountPos);
/// <summary>
/// Configures an exported service with explicit response type.
/// Mirrors Go <c>(a *Account) AddServiceExportWithResponse(...)</c>.
/// </summary>
public Exception? AddServiceExportWithResponse(string subject, ServiceRespType respType, IReadOnlyList<Account>? accounts = null) =>
AddServiceExportWithResponseAndAccountPos(subject, respType, accounts, 0);
/// <summary>
/// Configures an exported service with explicit response type and account-position auth.
/// Mirrors Go <c>(a *Account) addServiceExportWithResponseAndAccountPos(...)</c>.
/// </summary>
public Exception? AddServiceExportWithResponseAndAccountPos(string subject, ServiceRespType respType, IReadOnlyList<Account>? accounts, uint accountPos)
{
if (!SubscriptionIndex.IsValidSubject(subject))
return ServerErrors.ErrBadSubject;
_mu.EnterWriteLock();
try
{
Exports.Services ??= new Dictionary<string, ServiceExportEntry>(StringComparer.Ordinal);
if (!Exports.Services.TryGetValue(subject, out var serviceExport) || serviceExport == null)
serviceExport = new ServiceExportEntry();
if (respType != ServiceRespType.Singleton)
serviceExport.ResponseType = respType;
if (accounts != null || accountPos > 0)
{
var authErr = SetExportAuth(serviceExport, subject, accounts, accountPos);
if (authErr != null)
return authErr;
}
serviceExport.Account = this;
serviceExport.ResponseThreshold = ServerConstants.DefaultServiceExportResponseThreshold;
Exports.Services[subject] = serviceExport;
return null;
}
finally
{
_mu.ExitWriteLock();
}
}
/// <summary>
/// Enables latency tracking for <paramref name="service"/> with default sampling.
/// Mirrors Go <c>(a *Account) TrackServiceExport(service, results string) error</c>.
/// </summary>
public Exception? TrackServiceExport(string service, string results) =>
TrackServiceExportWithSampling(service, results, ServerConstants.DefaultServiceLatencySampling);
/// <summary>
/// Enables latency tracking for <paramref name="service"/> with explicit sampling.
/// Mirrors Go <c>(a *Account) TrackServiceExportWithSampling(...)</c>.
/// </summary>
public Exception? TrackServiceExportWithSampling(string service, string results, int sampling)
{
if (sampling != 0 && (sampling < 1 || sampling > 100))
return ServerErrors.ErrBadSampling;
if (!SubscriptionIndex.IsValidPublishSubject(results))
return ServerErrors.ErrBadPublishSubject;
if (IsExportService(results))
return ServerErrors.ErrBadPublishSubject;
_mu.EnterWriteLock();
try
{
if (Exports.Services == null)
return ServerErrors.ErrMissingService;
if (!Exports.Services.TryGetValue(service, out var serviceExport))
return ServerErrors.ErrMissingService;
serviceExport ??= new ServiceExportEntry();
if (serviceExport.ResponseType != ServiceRespType.Singleton)
return ServerErrors.ErrBadServiceType;
serviceExport.Latency = new InternalServiceLatency
{
Sampling = sampling,
Subject = results,
};
Exports.Services[service] = serviceExport;
if (Imports.Services != null)
{
foreach (var imports in Imports.Services.Values)
{
foreach (var import in imports)
{
if (import?.Account?.Name != Name)
continue;
if (SubjectTransform.IsSubsetMatch(SubjectTransform.TokenizeSubject(import.To), service))
import.Latency = serviceExport.Latency;
}
}
}
return null;
}
finally
{
_mu.ExitWriteLock();
}
}
/// <summary>
/// Disables latency tracking for the exported service.
/// Mirrors Go <c>(a *Account) UnTrackServiceExport(service string)</c>.
/// </summary>
public void UnTrackServiceExport(string service)
{
_mu.EnterWriteLock();
try
{
if (Exports.Services == null || !Exports.Services.TryGetValue(service, out var serviceExport) || serviceExport?.Latency == null)
return;
serviceExport.Latency = null;
if (Imports.Services == null)
return;
foreach (var imports in Imports.Services.Values)
{
foreach (var import in imports)
{
if (import?.Account?.Name != Name)
continue;
if (SubjectTransform.IsSubsetMatch(SubjectTransform.TokenizeSubject(import.To), service))
{
import.Latency = null;
import.M1 = null;
}
}
}
}
finally
{
_mu.ExitWriteLock();
}
}
// -------------------------------------------------------------------------
// Export checks
// -------------------------------------------------------------------------
@@ -2146,6 +2436,35 @@ public sealed class Account : INatsAccount
return false;
}
/// <summary>
/// Applies account-based authorization rules to an export descriptor.
/// Mirrors Go <c>setExportAuth(&amp;se.exportAuth, ...)</c>.
/// </summary>
private static Exception? SetExportAuth(ExportAuth auth, string subject, IReadOnlyList<Account>? accounts, uint accountPos)
{
if (!SubscriptionIndex.IsValidSubject(subject))
return ServerErrors.ErrBadSubject;
auth.AccountPosition = accountPos;
if (accounts == null || accounts.Count == 0)
{
auth.Approved = null;
return null;
}
var approved = new Dictionary<string, Account>(accounts.Count, StringComparer.Ordinal);
foreach (var account in accounts)
{
if (account == null)
continue;
approved[account.Name] = account;
}
auth.Approved = approved;
return null;
}
// -------------------------------------------------------------------------
// Export equality helpers
// -------------------------------------------------------------------------