Files
git.stella-ops.org/src/Notify/__Libraries/StellaOps.Notify.Queue/NotifyQueueContracts.cs
2025-10-28 15:10:40 +02:00

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));
}