using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Notify.Models;
namespace StellaOps.Notify.Queue;
///
/// Message queued for Notify event processing.
///
public sealed class NotifyQueueEventMessage
{
private readonly NotifyEvent _event;
private readonly IReadOnlyDictionary _attributes;
public NotifyQueueEventMessage(
NotifyEvent @event,
string stream,
string? idempotencyKey = null,
string? partitionKey = null,
string? traceId = null,
IReadOnlyDictionary? attributes = null)
{
_event = @event ?? throw new ArgumentNullException(nameof(@event));
if (string.IsNullOrWhiteSpace(stream))
{
throw new ArgumentException("Stream must be provided.", nameof(stream));
}
Stream = stream;
IdempotencyKey = string.IsNullOrWhiteSpace(idempotencyKey)
? @event.EventId.ToString("N")
: idempotencyKey!;
PartitionKey = string.IsNullOrWhiteSpace(partitionKey) ? null : partitionKey.Trim();
TraceId = string.IsNullOrWhiteSpace(traceId) ? null : traceId.Trim();
_attributes = attributes is null
? EmptyReadOnlyDictionary.Instance
: new ReadOnlyDictionary(new Dictionary(attributes, StringComparer.Ordinal));
}
public NotifyEvent Event => _event;
public string Stream { get; }
public string IdempotencyKey { get; }
public string TenantId => _event.Tenant;
public string? PartitionKey { get; }
public string? TraceId { get; }
public IReadOnlyDictionary Attributes => _attributes;
}
///
/// Message queued for channel delivery execution.
///
public sealed class NotifyDeliveryQueueMessage
{
public const string DefaultStream = "notify:deliveries";
private readonly IReadOnlyDictionary _attributes;
public NotifyDeliveryQueueMessage(
NotifyDelivery delivery,
string channelId,
NotifyChannelType channelType,
string? stream = null,
string? traceId = null,
IReadOnlyDictionary? attributes = null)
{
Delivery = delivery ?? throw new ArgumentNullException(nameof(delivery));
ChannelId = NotifyValidation.EnsureNotNullOrWhiteSpace(channelId, nameof(channelId));
ChannelType = channelType;
Stream = string.IsNullOrWhiteSpace(stream) ? DefaultStream : stream!.Trim();
TraceId = string.IsNullOrWhiteSpace(traceId) ? null : traceId.Trim();
_attributes = attributes is null
? EmptyReadOnlyDictionary.Instance
: new ReadOnlyDictionary(new Dictionary(attributes, StringComparer.Ordinal));
}
public NotifyDelivery Delivery { get; }
public string ChannelId { get; }
public NotifyChannelType ChannelType { get; }
public string Stream { get; }
public string? TraceId { get; }
public string TenantId => Delivery.TenantId;
public string IdempotencyKey => Delivery.DeliveryId;
public string PartitionKey => ChannelId;
public IReadOnlyDictionary Attributes => _attributes;
}
public readonly record struct NotifyQueueEnqueueResult(string MessageId, bool Deduplicated);
public sealed class NotifyQueueLeaseRequest
{
public NotifyQueueLeaseRequest(string consumer, int batchSize, TimeSpan leaseDuration)
{
if (string.IsNullOrWhiteSpace(consumer))
{
throw new ArgumentException("Consumer must be provided.", nameof(consumer));
}
if (batchSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(batchSize), batchSize, "Batch size must be positive.");
}
if (leaseDuration <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(leaseDuration), leaseDuration, "Lease duration must be positive.");
}
Consumer = consumer;
BatchSize = batchSize;
LeaseDuration = leaseDuration;
}
public string Consumer { get; }
public int BatchSize { get; }
public TimeSpan LeaseDuration { get; }
}
public sealed class NotifyQueueClaimOptions
{
public NotifyQueueClaimOptions(string claimantConsumer, int batchSize, TimeSpan minIdleTime)
{
if (string.IsNullOrWhiteSpace(claimantConsumer))
{
throw new ArgumentException("Consumer must be provided.", nameof(claimantConsumer));
}
if (batchSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(batchSize), batchSize, "Batch size must be positive.");
}
if (minIdleTime < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(minIdleTime), minIdleTime, "Minimum idle time cannot be negative.");
}
ClaimantConsumer = claimantConsumer;
BatchSize = batchSize;
MinIdleTime = minIdleTime;
}
public string ClaimantConsumer { get; }
public int BatchSize { get; }
public TimeSpan MinIdleTime { get; }
}
public enum NotifyQueueReleaseDisposition
{
Retry,
Abandon
}
public interface INotifyQueue
{
ValueTask PublishAsync(TMessage message, CancellationToken cancellationToken = default);
ValueTask>> LeaseAsync(NotifyQueueLeaseRequest request, CancellationToken cancellationToken = default);
ValueTask>> ClaimExpiredAsync(NotifyQueueClaimOptions options, CancellationToken cancellationToken = default);
}
public interface INotifyQueueLease
{
string MessageId { get; }
int Attempt { get; }
DateTimeOffset EnqueuedAt { get; }
DateTimeOffset LeaseExpiresAt { get; }
string Consumer { get; }
string Stream { get; }
string TenantId { get; }
string? PartitionKey { get; }
string IdempotencyKey { get; }
string? TraceId { get; }
IReadOnlyDictionary Attributes { get; }
TMessage Message { get; }
Task AcknowledgeAsync(CancellationToken cancellationToken = default);
Task RenewAsync(TimeSpan leaseDuration, CancellationToken cancellationToken = default);
Task ReleaseAsync(NotifyQueueReleaseDisposition disposition, CancellationToken cancellationToken = default);
Task DeadLetterAsync(string reason, CancellationToken cancellationToken = default);
}
public interface INotifyEventQueue : INotifyQueue
{
}
public interface INotifyDeliveryQueue : INotifyQueue
{
}
internal static class EmptyReadOnlyDictionary
where TKey : notnull
{
public static readonly IReadOnlyDictionary Instance =
new ReadOnlyDictionary(new Dictionary(0, EqualityComparer.Default));
}