// Go: consumer.go:500-600 — Priority groups for sticky consumer assignment. // When multiple consumers are in a group, the lowest-priority-numbered consumer // (highest priority) gets messages. If it becomes idle/disconnects, the next // consumer takes over. using System.Collections.Concurrent; namespace NATS.Server.JetStream.Consumers; /// /// Manages named groups of consumers with priority levels. /// Within each group the consumer with the lowest priority number is the /// "active" consumer that receives messages. Thread-safe. /// public sealed class PriorityGroupManager { private readonly ConcurrentDictionary _groups = new(StringComparer.Ordinal); /// /// Register a consumer in a named priority group. /// Lower values indicate higher priority. /// public void Register(string groupName, string consumerId, int priority) { var group = _groups.GetOrAdd(groupName, _ => new PriorityGroup()); lock (group.Lock) { // If the consumer is already registered, update its priority. for (var i = 0; i < group.Members.Count; i++) { if (string.Equals(group.Members[i].ConsumerId, consumerId, StringComparison.Ordinal)) { group.Members[i] = new PriorityMember(consumerId, priority); return; } } group.Members.Add(new PriorityMember(consumerId, priority)); } } /// /// Remove a consumer from a named priority group. /// public void Unregister(string groupName, string consumerId) { if (!_groups.TryGetValue(groupName, out var group)) return; lock (group.Lock) { group.Members.RemoveAll(m => string.Equals(m.ConsumerId, consumerId, StringComparison.Ordinal)); // Clean up empty groups if (group.Members.Count == 0) _groups.TryRemove(groupName, out _); } } /// /// Returns the consumer ID with the lowest priority number (highest priority) /// in the named group, or null if the group is empty or does not exist. /// When multiple consumers share the same lowest priority, the first registered wins. /// public string? GetActiveConsumer(string groupName) { if (!_groups.TryGetValue(groupName, out var group)) return null; lock (group.Lock) { if (group.Members.Count == 0) return null; var active = group.Members[0]; for (var i = 1; i < group.Members.Count; i++) { if (group.Members[i].Priority < active.Priority) active = group.Members[i]; } return active.ConsumerId; } } /// /// Returns true if the given consumer is the current active consumer /// (lowest priority number) in the named group. /// public bool IsActive(string groupName, string consumerId) { var active = GetActiveConsumer(groupName); return active != null && string.Equals(active, consumerId, StringComparison.Ordinal); } private sealed class PriorityGroup { public object Lock { get; } = new(); public List Members { get; } = []; } private record struct PriorityMember(string ConsumerId, int Priority); }