dotnet-cqrs/Svrnty.CQRS.Events.Abstractions/Subscriptions/IConsumerRegistry.cs

156 lines
6.1 KiB
C#

using System;
using Svrnty.CQRS.Events.Abstractions.Subscriptions;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Svrnty.CQRS.Events.Abstractions.Subscriptions;
/// <summary>
/// Registry for tracking active consumers subscribed to event streams.
/// </summary>
/// <remarks>
/// <para>
/// The consumer registry tracks which consumers are actively listening to which subscriptions.
/// This is different from <see cref="ISubscriptionStore"/> which tracks subscription configurations.
/// </para>
/// <para>
/// <strong>Usage:</strong>
/// - A subscription defines WHAT to listen to (e.g., "user-events with filter X")
/// - A consumer is WHO is listening (e.g., "analytics-service-instance-1")
/// - Multiple consumers can listen to the same subscription (broadcast or consumer group)
/// </para>
/// </remarks>
public interface IConsumerRegistry
{
/// <summary>
/// Register a consumer for a subscription.
/// </summary>
/// <param name="subscriptionId">The subscription ID.</param>
/// <param name="consumerId">The consumer ID.</param>
/// <param name="metadata">Optional metadata about the consumer (e.g., hostname, version).</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the async operation.</returns>
/// <remarks>
/// Registers the consumer as actively listening to the subscription.
/// If the consumer is already registered, updates the last heartbeat timestamp.
/// </remarks>
Task RegisterConsumerAsync(
string subscriptionId,
string consumerId,
Dictionary<string, string>? metadata = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Unregister a consumer from a subscription.
/// </summary>
/// <param name="subscriptionId">The subscription ID.</param>
/// <param name="consumerId">The consumer ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the consumer was unregistered, false if not found.</returns>
/// <remarks>
/// Removes the consumer from the active consumer list.
/// Should be called when a consumer disconnects or stops listening.
/// </remarks>
Task<bool> UnregisterConsumerAsync(
string subscriptionId,
string consumerId,
CancellationToken cancellationToken = default);
/// <summary>
/// Get all active consumers for a subscription.
/// </summary>
/// <param name="subscriptionId">The subscription ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>List of active consumer IDs.</returns>
/// <remarks>
/// Returns consumers that are currently registered and have recent heartbeats.
/// Stale consumers (no heartbeat for timeout period) are automatically excluded.
/// </remarks>
Task<List<string>> GetConsumersAsync(
string subscriptionId,
CancellationToken cancellationToken = default);
/// <summary>
/// Get detailed information about all active consumers for a subscription.
/// </summary>
/// <param name="subscriptionId">The subscription ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>List of consumer information including metadata and timestamps.</returns>
Task<List<ConsumerInfo>> GetConsumerInfoAsync(
string subscriptionId,
CancellationToken cancellationToken = default);
/// <summary>
/// Update the heartbeat timestamp for a consumer.
/// </summary>
/// <param name="subscriptionId">The subscription ID.</param>
/// <param name="consumerId">The consumer ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the heartbeat was updated, false if consumer not found.</returns>
/// <remarks>
/// Consumers should send heartbeats periodically to indicate they're still active.
/// Consumers without recent heartbeats are considered stale and automatically removed.
/// </remarks>
Task<bool> HeartbeatAsync(
string subscriptionId,
string consumerId,
CancellationToken cancellationToken = default);
/// <summary>
/// Check if a specific consumer is currently registered.
/// </summary>
/// <param name="subscriptionId">The subscription ID.</param>
/// <param name="consumerId">The consumer ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the consumer is active, false otherwise.</returns>
Task<bool> IsConsumerActiveAsync(
string subscriptionId,
string consumerId,
CancellationToken cancellationToken = default);
/// <summary>
/// Remove stale consumers that haven't sent heartbeats within the timeout period.
/// </summary>
/// <param name="timeout">Consider consumers stale if no heartbeat for this duration.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Number of stale consumers removed.</returns>
/// <remarks>
/// This should be called periodically by a background service to clean up disconnected consumers.
/// </remarks>
Task<int> RemoveStaleConsumersAsync(
TimeSpan timeout,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Information about a registered consumer.
/// </summary>
public sealed record ConsumerInfo
{
/// <summary>
/// The consumer ID.
/// </summary>
public required string ConsumerId { get; init; }
/// <summary>
/// The subscription ID this consumer is subscribed to.
/// </summary>
public required string SubscriptionId { get; init; }
/// <summary>
/// When the consumer was first registered.
/// </summary>
public required DateTimeOffset RegisteredAt { get; init; }
/// <summary>
/// When the consumer last sent a heartbeat.
/// </summary>
public required DateTimeOffset LastHeartbeat { get; init; }
/// <summary>
/// Optional metadata about the consumer (e.g., hostname, version, process ID).
/// </summary>
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
}