using System.Text.Json; namespace ZB.MOM.NatsNet.Server; public sealed partial class NatsServer { internal (int JetStreamEnabled, int Total) TrackedJetStreamServers() { if (!IsRunning() || _sys == null) return (-1, -1); var js = 0; var total = 0; foreach (var value in _nodeToInfo.Values) { if (value is not NodeInfo info) continue; if (info.Js) js++; total++; } return (js, total); } internal (JetStream? JetStream, JetStreamCluster? Cluster) GetJetStreamCluster() { if (IsShuttingDown()) return (null, null); var js = GetJetStreamState(); if (js == null) return (null, null); return (js, js.Cluster as JetStreamCluster); } public bool JetStreamIsClustered() { var js = GetJetStreamState(); return js != null && Interlocked.CompareExchange(ref js.Clustered, 0, 0) == 1; } public bool JetStreamIsLeader() { var (_, cluster) = GetJetStreamCluster(); return cluster?.IsLeader() == true; } public bool JetStreamIsCurrent() { var js = GetJetStreamState(); if (js == null) return false; js.Lock.EnterReadLock(); try { if (js.Cluster is not JetStreamCluster cluster) return true; return cluster.Meta?.Current() == true; } finally { js.Lock.ExitReadLock(); } } public Exception? JetStreamSnapshotMeta() { var js = GetJetStreamState(); if (js == null) return ToException(JsApiErrors.NewJSNotEnabledError()); js.Lock.EnterReadLock(); JetStreamCluster? cluster; IRaftNode? meta; try { cluster = js.Cluster as JetStreamCluster; meta = cluster?.Meta; if (cluster == null || meta == null) return ToException(JsApiErrors.NewJSClusterNotActiveError()); if (!cluster.IsLeader()) return ToException(JsApiErrors.NewJSClusterNotLeaderError()); } finally { js.Lock.ExitReadLock(); } var snapshot = JsonSerializer.SerializeToUtf8Bytes(cluster!.Streams); meta!.InstallSnapshot(snapshot, force: false); return null; } public Exception? JetStreamStepdownStream(string account, string stream) { var (_, cluster) = GetJetStreamCluster(); if (cluster == null) return ToException(JsApiErrors.NewJSClusterNotActiveError()); var (acc, error) = LookupAccount(account); if (error != null) return error; if (acc == null) return new InvalidOperationException("account not found"); var (_, streamError) = acc.LookupStream(stream); if (streamError != null) return streamError; if (cluster.Streams.TryGetValue(account, out var assignments) && assignments.TryGetValue(stream, out var assignment)) { assignment.Group?.Node?.StepDown(); } return null; } public Exception? JetStreamStepdownConsumer(string account, string stream, string consumer) { var (_, cluster) = GetJetStreamCluster(); if (cluster == null) return ToException(JsApiErrors.NewJSClusterNotActiveError()); var (acc, error) = LookupAccount(account); if (error != null) return error; if (acc == null) return new InvalidOperationException("account not found"); var (_, streamError) = acc.LookupStream(stream); if (streamError != null) return streamError; if (!cluster.Streams.TryGetValue(account, out var assignments) || !assignments.TryGetValue(stream, out var assignment) || assignment.Consumers == null || !assignment.Consumers.TryGetValue(consumer, out var consumerAssignment)) { return ToException(JsApiErrors.NewJSConsumerNotFoundError()); } consumerAssignment.Group?.Node?.StepDown(); return null; } public Exception? JetStreamSnapshotStream(string account, string stream) { var (_, cluster) = GetJetStreamCluster(); if (cluster == null) return ToException(JsApiErrors.NewJSClusterNotActiveError()); var (acc, error) = LookupAccount(account); if (error != null) return error; if (acc == null) return new InvalidOperationException("account not found"); var (natsStream, streamError) = acc.LookupStream(stream); if (streamError != null) return streamError; if (natsStream == null) return null; if (cluster.Streams.TryGetValue(account, out var assignments) && assignments.TryGetValue(stream, out var assignment)) { var snapshot = JsonSerializer.SerializeToUtf8Bytes(natsStream.State()); assignment.Group?.Node?.InstallSnapshot(snapshot, force: false); } return null; } public string[] JetStreamClusterPeers() { var js = GetJetStreamState(); if (js == null) return []; js.Lock.EnterReadLock(); try { if (js.Cluster is not JetStreamCluster cluster || cluster.Meta == null || !cluster.IsLeader()) return []; var names = new List(); foreach (var peer in cluster.Meta.Peers()) { if (!_nodeToInfo.TryGetValue(peer.Id, out var value) || value is not NodeInfo info) continue; if (info.Offline || !info.Js || info.Stats == null) continue; names.Add(info.Name); } return [.. names]; } finally { js.Lock.ExitReadLock(); } } public bool JetStreamIsStreamLeader(string account, string stream) { var (js, cluster) = GetJetStreamCluster(); if (js == null || cluster == null) return false; js.Lock.EnterReadLock(); try { return cluster.IsStreamLeader(account, stream); } finally { js.Lock.ExitReadLock(); } } public bool JetStreamIsStreamCurrent(string account, string stream) { var js = GetJetStreamState(); if (js == null) return false; js.Lock.EnterReadLock(); try { return (js.Cluster as JetStreamCluster)?.IsStreamCurrent(account, stream) ?? false; } finally { js.Lock.ExitReadLock(); } } public bool JetStreamIsConsumerLeader(string account, string stream, string consumer) { var (js, cluster) = GetJetStreamCluster(); if (js == null || cluster == null) return false; js.Lock.EnterReadLock(); try { return cluster.IsConsumerLeader(account, stream, consumer); } finally { js.Lock.ExitReadLock(); } } internal Exception? EnableJetStreamClustering() { if (!IsRunning()) return null; var js = GetJetStream(); if (js == null) return ToException(JsApiErrors.NewJSNotEnabledForAccountError()); if (js.IsClustered()) return null; return js.SetupMetaGroup(); } public bool JetStreamIsStreamAssigned(string account, string stream) { var (js, cluster) = GetJetStreamCluster(); if (js == null || cluster == null) return false; var (acc, _) = LookupAccount(account); if (acc == null) return false; js.Lock.EnterReadLock(); try { return cluster.IsStreamAssigned(acc, stream); } finally { js.Lock.ExitReadLock(); } } private static Exception ToException(JsApiError error) => new(error.Description ?? "jetstream error"); }