232 lines
7.2 KiB
C#
232 lines
7.2 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Message queued for Notify event processing.
|
|
/// </summary>
|
|
public sealed class NotifyQueueEventMessage
|
|
{
|
|
private readonly NotifyEvent _event;
|
|
private readonly IReadOnlyDictionary<string, string> _attributes;
|
|
|
|
public NotifyQueueEventMessage(
|
|
NotifyEvent @event,
|
|
string stream,
|
|
string? idempotencyKey = null,
|
|
string? partitionKey = null,
|
|
string? traceId = null,
|
|
IReadOnlyDictionary<string, string>? 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<string, string>.Instance
|
|
: new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(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<string, string> Attributes => _attributes;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Message queued for channel delivery execution.
|
|
/// </summary>
|
|
public sealed class NotifyDeliveryQueueMessage
|
|
{
|
|
public const string DefaultStream = "notify:deliveries";
|
|
|
|
private readonly IReadOnlyDictionary<string, string> _attributes;
|
|
|
|
public NotifyDeliveryQueueMessage(
|
|
NotifyDelivery delivery,
|
|
string channelId,
|
|
NotifyChannelType channelType,
|
|
string? stream = null,
|
|
string? traceId = null,
|
|
IReadOnlyDictionary<string, string>? 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<string, string>.Instance
|
|
: new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(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<string, string> 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<TMessage>
|
|
{
|
|
ValueTask<NotifyQueueEnqueueResult> PublishAsync(TMessage message, CancellationToken cancellationToken = default);
|
|
|
|
ValueTask<IReadOnlyList<INotifyQueueLease<TMessage>>> LeaseAsync(NotifyQueueLeaseRequest request, CancellationToken cancellationToken = default);
|
|
|
|
ValueTask<IReadOnlyList<INotifyQueueLease<TMessage>>> ClaimExpiredAsync(NotifyQueueClaimOptions options, CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
public interface INotifyQueueLease<out TMessage>
|
|
{
|
|
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<string, string> 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<NotifyQueueEventMessage>
|
|
{
|
|
}
|
|
|
|
public interface INotifyDeliveryQueue : INotifyQueue<NotifyDeliveryQueueMessage>
|
|
{
|
|
}
|
|
|
|
internal static class EmptyReadOnlyDictionary<TKey, TValue>
|
|
where TKey : notnull
|
|
{
|
|
public static readonly IReadOnlyDictionary<TKey, TValue> Instance =
|
|
new ReadOnlyDictionary<TKey, TValue>(new Dictionary<TKey, TValue>(0, EqualityComparer<TKey>.Default));
|
|
}
|