using System; using Svrnty.CQRS.Events.Abstractions.Subscriptions; using Svrnty.CQRS.Events.Subscriptions; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Svrnty.CQRS.Events.Abstractions; namespace Svrnty.CQRS.Events.Storage; /// /// In-memory implementation of for tracking active consumers. /// Uses concurrent collections for thread-safe consumer management. /// /// /// /// Thread Safety: /// All operations are thread-safe using . /// /// /// Stale Consumer Cleanup: /// Consumers are automatically marked as stale if they don't send heartbeats. /// Use periodically to clean up. /// /// public class InMemoryConsumerRegistry : IConsumerRegistry { // (subscriptionId, consumerId) -> ConsumerRegistration private readonly ConcurrentDictionary _consumers = new(); /// public Task RegisterConsumerAsync( string subscriptionId, string consumerId, Dictionary? metadata = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(subscriptionId)) throw new ArgumentException("Subscription ID cannot be null or whitespace.", nameof(subscriptionId)); if (string.IsNullOrWhiteSpace(consumerId)) throw new ArgumentException("Consumer ID cannot be null or whitespace.", nameof(consumerId)); var key = GetKey(subscriptionId, consumerId); var now = DateTimeOffset.UtcNow; _consumers.AddOrUpdate( key, // Add new consumer _ => new ConsumerRegistration { SubscriptionId = subscriptionId, ConsumerId = consumerId, RegisteredAt = now, LastHeartbeat = now, Metadata = metadata != null ? new Dictionary(metadata) : null }, // Update existing consumer (heartbeat) (_, existing) => { existing.LastHeartbeat = now; if (metadata != null) { existing.Metadata = new Dictionary(metadata); } return existing; }); return Task.CompletedTask; } /// public Task UnregisterConsumerAsync( string subscriptionId, string consumerId, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(subscriptionId)) throw new ArgumentException("Subscription ID cannot be null or whitespace.", nameof(subscriptionId)); if (string.IsNullOrWhiteSpace(consumerId)) throw new ArgumentException("Consumer ID cannot be null or whitespace.", nameof(consumerId)); var key = GetKey(subscriptionId, consumerId); var removed = _consumers.TryRemove(key, out _); return Task.FromResult(removed); } /// public Task> GetConsumersAsync( string subscriptionId, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(subscriptionId)) throw new ArgumentException("Subscription ID cannot be null or whitespace.", nameof(subscriptionId)); var consumerIds = _consumers.Values .Where(c => c.SubscriptionId == subscriptionId) .Select(c => c.ConsumerId) .ToList(); return Task.FromResult(consumerIds); } /// public Task> GetConsumerInfoAsync( string subscriptionId, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(subscriptionId)) throw new ArgumentException("Subscription ID cannot be null or whitespace.", nameof(subscriptionId)); var consumers = _consumers.Values .Where(c => c.SubscriptionId == subscriptionId) .Select(c => new ConsumerInfo { ConsumerId = c.ConsumerId, SubscriptionId = c.SubscriptionId, RegisteredAt = c.RegisteredAt, LastHeartbeat = c.LastHeartbeat, Metadata = c.Metadata }) .ToList(); return Task.FromResult(consumers); } /// public Task HeartbeatAsync( string subscriptionId, string consumerId, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(subscriptionId)) throw new ArgumentException("Subscription ID cannot be null or whitespace.", nameof(subscriptionId)); if (string.IsNullOrWhiteSpace(consumerId)) throw new ArgumentException("Consumer ID cannot be null or whitespace.", nameof(consumerId)); var key = GetKey(subscriptionId, consumerId); if (_consumers.TryGetValue(key, out var registration)) { registration.LastHeartbeat = DateTimeOffset.UtcNow; return Task.FromResult(true); } return Task.FromResult(false); } /// public Task IsConsumerActiveAsync( string subscriptionId, string consumerId, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(subscriptionId)) throw new ArgumentException("Subscription ID cannot be null or whitespace.", nameof(subscriptionId)); if (string.IsNullOrWhiteSpace(consumerId)) throw new ArgumentException("Consumer ID cannot be null or whitespace.", nameof(consumerId)); var key = GetKey(subscriptionId, consumerId); var exists = _consumers.ContainsKey(key); return Task.FromResult(exists); } /// public Task RemoveStaleConsumersAsync( TimeSpan timeout, CancellationToken cancellationToken = default) { if (timeout <= TimeSpan.Zero) throw new ArgumentException("Timeout must be positive.", nameof(timeout)); var cutoff = DateTimeOffset.UtcNow.Subtract(timeout); var staleConsumers = _consumers .Where(kvp => kvp.Value.LastHeartbeat < cutoff) .Select(kvp => kvp.Key) .ToList(); var removedCount = 0; foreach (var key in staleConsumers) { if (_consumers.TryRemove(key, out _)) { removedCount++; } } return Task.FromResult(removedCount); } /// /// Get the total number of registered consumers across all subscriptions. /// public int GetTotalConsumerCount() { return _consumers.Count; } /// /// Get all subscriptions that have at least one active consumer. /// public List GetActiveSubscriptions() { return _consumers.Values .Select(c => c.SubscriptionId) .Distinct() .ToList(); } private static string GetKey(string subscriptionId, string consumerId) { return $"{subscriptionId}:{consumerId}"; } /// /// Internal consumer registration model. /// private class ConsumerRegistration { public required string SubscriptionId { get; init; } public required string ConsumerId { get; init; } public required DateTimeOffset RegisteredAt { get; init; } public DateTimeOffset LastHeartbeat { get; set; } public Dictionary? Metadata { get; set; } } }