feat(consumers): add PriorityGroupManager and PullConsumer timeout/compiled filters (C5+C6)
This commit is contained in:
102
src/NATS.Server/JetStream/Consumers/PriorityGroupManager.cs
Normal file
102
src/NATS.Server/JetStream/Consumers/PriorityGroupManager.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed class PriorityGroupManager
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, PriorityGroup> _groups = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Register a consumer in a named priority group.
|
||||
/// Lower <paramref name="priority"/> values indicate higher priority.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a consumer from a named priority group.
|
||||
/// </summary>
|
||||
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 _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the consumer ID with the lowest priority number (highest priority)
|
||||
/// in the named group, or <c>null</c> if the group is empty or does not exist.
|
||||
/// When multiple consumers share the same lowest priority, the first registered wins.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> if the given consumer is the current active consumer
|
||||
/// (lowest priority number) in the named group.
|
||||
/// </summary>
|
||||
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<PriorityMember> Members { get; } = [];
|
||||
}
|
||||
|
||||
private record struct PriorityMember(string ConsumerId, int Priority);
|
||||
}
|
||||
Reference in New Issue
Block a user