diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Accounts/Account.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Accounts/Account.cs index 2e3371f..fa0308b 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Accounts/Account.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Accounts/Account.cs @@ -475,6 +475,29 @@ public sealed class Account : INatsAccount } } + /// + /// Sets account-level message trace destination subject. + /// Mirrors Go (a *Account) setTraceDest(dest string). + /// + internal void SetTraceDest(string dest) => SetMessageTraceDestination(dest); + + /// + /// Returns trace destination and sampling. + /// Mirrors Go (a *Account) getTraceDestAndSampling() (string, int). + /// + 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 /// public override string ToString() => Name; + /// + /// Returns the account name. + /// Mirrors Go (a *Account) String() string. + /// + 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; + /// + /// Returns local non-system, non-leaf client count. Lock must be held. + /// Mirrors Go (a *Account) numLocalConnections() int. + /// + internal int NumLocalConnectionsInternal() => NumLocalConnectionsLocked(); + /// /// Returns all local connections including leaf nodes (minus system clients). /// Mirrors Go (a *Account) numLocalAndLeafConnections() int. @@ -1049,6 +1084,13 @@ public sealed class Account : INatsAccount return _nleafs + _nrleafs >= MaxLeafNodes; } + /// + /// Returns true if total leaf-node count reached the configured maximum. + /// Lock must be held by the caller. + /// Mirrors Go (a *Account) maxTotalLeafNodesReached() bool. + /// + internal bool MaxTotalLeafNodesReachedInternal() => MaxTotalLeafNodesReachedLocked(); + /// /// Returns the total leaf-node count (local + remote). /// Mirrors Go (a *Account) NumLeafNodes() int. @@ -1115,6 +1157,93 @@ public sealed class Account : INatsAccount } } + /// + /// Returns true when there is at least one matching subscription for . + /// Mirrors Go (a *Account) SubscriptionInterest(subject string) bool. + /// + public bool SubscriptionInterest(string subject) => Interest(subject) > 0; + + /// + /// Returns total number of plain and queue subscriptions matching . + /// Mirrors Go (a *Account) Interest(subject string) int. + /// + 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(); + } + } + + /// + /// Increments the leaf-node count for a remote cluster. + /// Mirrors Go (a *Account) registerLeafNodeCluster(cluster string). + /// + internal void RegisterLeafNodeCluster(string cluster) + { + _mu.EnterWriteLock(); + try + { + _leafClusters ??= new Dictionary(StringComparer.Ordinal); + _leafClusters.TryGetValue(cluster, out var current); + _leafClusters[cluster] = current + 1; + } + finally + { + _mu.ExitWriteLock(); + } + } + + /// + /// Returns true when this account already tracks one or more leaf nodes from . + /// Mirrors Go (a *Account) hasLeafNodeCluster(cluster string) bool. + /// + internal bool HasLeafNodeCluster(string cluster) + { + _mu.EnterReadLock(); + try + { + return _leafClusters != null && + _leafClusters.TryGetValue(cluster, out var count) && + count > 0; + } + finally + { + _mu.ExitReadLock(); + } + } + + /// + /// Returns true when the account is leaf-cluster isolated to . + /// Mirrors Go (a *Account) isLeafNodeClusterIsolated(cluster string) bool. + /// + 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 + // ------------------------------------------------------------------------- + + /// + /// Configures an exported service with singleton response semantics. + /// Mirrors Go (a *Account) AddServiceExport(subject string, accounts []*Account) error. + /// + public Exception? AddServiceExport(string subject, IReadOnlyList? accounts = null) => + AddServiceExportWithResponseAndAccountPos(subject, ServiceRespType.Singleton, accounts, 0); + + /// + /// Configures an exported service with singleton response semantics and account-position auth. + /// Mirrors Go (a *Account) addServiceExportWithAccountPos(...). + /// + public Exception? AddServiceExportWithAccountPos(string subject, IReadOnlyList? accounts, uint accountPos) => + AddServiceExportWithResponseAndAccountPos(subject, ServiceRespType.Singleton, accounts, accountPos); + + /// + /// Configures an exported service with explicit response type. + /// Mirrors Go (a *Account) AddServiceExportWithResponse(...). + /// + public Exception? AddServiceExportWithResponse(string subject, ServiceRespType respType, IReadOnlyList? accounts = null) => + AddServiceExportWithResponseAndAccountPos(subject, respType, accounts, 0); + + /// + /// Configures an exported service with explicit response type and account-position auth. + /// Mirrors Go (a *Account) addServiceExportWithResponseAndAccountPos(...). + /// + public Exception? AddServiceExportWithResponseAndAccountPos(string subject, ServiceRespType respType, IReadOnlyList? accounts, uint accountPos) + { + if (!SubscriptionIndex.IsValidSubject(subject)) + return ServerErrors.ErrBadSubject; + + _mu.EnterWriteLock(); + try + { + Exports.Services ??= new Dictionary(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(); + } + } + + /// + /// Enables latency tracking for with default sampling. + /// Mirrors Go (a *Account) TrackServiceExport(service, results string) error. + /// + public Exception? TrackServiceExport(string service, string results) => + TrackServiceExportWithSampling(service, results, ServerConstants.DefaultServiceLatencySampling); + + /// + /// Enables latency tracking for with explicit sampling. + /// Mirrors Go (a *Account) TrackServiceExportWithSampling(...). + /// + 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(); + } + } + + /// + /// Disables latency tracking for the exported service. + /// Mirrors Go (a *Account) UnTrackServiceExport(service string). + /// + 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; } + /// + /// Applies account-based authorization rules to an export descriptor. + /// Mirrors Go setExportAuth(&se.exportAuth, ...). + /// + private static Exception? SetExportAuth(ExportAuth auth, string subject, IReadOnlyList? 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(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 // ------------------------------------------------------------------------- diff --git a/porting.db b/porting.db index 15af1ae..244b04e 100644 Binary files a/porting.db and b/porting.db differ