up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
namespace StellaOps.Messaging.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Transport-agnostic distributed cache interface.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The key type.</typeparam>
|
||||
/// <typeparam name="TValue">The value type.</typeparam>
|
||||
public interface IDistributedCache<TKey, TValue>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the provider name for diagnostics (e.g., "valkey", "postgres").
|
||||
/// </summary>
|
||||
string ProviderName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value from the cache.
|
||||
/// </summary>
|
||||
/// <param name="key">The cache key.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The cache result.</returns>
|
||||
ValueTask<CacheResult<TValue>> GetAsync(TKey key, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sets a value in the cache.
|
||||
/// </summary>
|
||||
/// <param name="key">The cache key.</param>
|
||||
/// <param name="value">The value to cache.</param>
|
||||
/// <param name="options">Optional cache entry options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
ValueTask SetAsync(TKey key, TValue value, CacheEntryOptions? options = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a value from the cache.
|
||||
/// </summary>
|
||||
/// <param name="key">The cache key.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>True if the key existed and was removed.</returns>
|
||||
ValueTask<bool> InvalidateAsync(TKey key, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Removes values matching a pattern from the cache.
|
||||
/// </summary>
|
||||
/// <param name="pattern">The key pattern (supports wildcards).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The number of keys invalidated.</returns>
|
||||
ValueTask<long> InvalidateByPatternAsync(string pattern, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value in the cache, using a factory function if the value is not present.
|
||||
/// </summary>
|
||||
/// <param name="key">The cache key.</param>
|
||||
/// <param name="factory">Factory function to create the value if not cached.</param>
|
||||
/// <param name="options">Optional cache entry options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The cached or newly created value.</returns>
|
||||
ValueTask<TValue> GetOrSetAsync(
|
||||
TKey key,
|
||||
Func<CancellationToken, ValueTask<TValue>> factory,
|
||||
CacheEntryOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple string-keyed distributed cache interface.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The value type.</typeparam>
|
||||
public interface IDistributedCache<TValue> : IDistributedCache<string, TValue>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
namespace StellaOps.Messaging.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a leased message from a queue.
|
||||
/// The lease provides exclusive access to process the message.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The message type.</typeparam>
|
||||
public interface IMessageLease<out TMessage> : IAsyncDisposable where TMessage : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique message identifier.
|
||||
/// </summary>
|
||||
string MessageId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the message payload.
|
||||
/// </summary>
|
||||
TMessage Message { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the delivery attempt number (1-based).
|
||||
/// </summary>
|
||||
int Attempt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timestamp when the message was enqueued.
|
||||
/// </summary>
|
||||
DateTimeOffset EnqueuedAt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timestamp when the lease expires.
|
||||
/// </summary>
|
||||
DateTimeOffset LeaseExpiresAt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the consumer name that owns this lease.
|
||||
/// </summary>
|
||||
string Consumer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tenant identifier, if present.
|
||||
/// </summary>
|
||||
string? TenantId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the correlation identifier for tracing, if present.
|
||||
/// </summary>
|
||||
string? CorrelationId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Acknowledges successful processing of the message.
|
||||
/// The message is removed from the queue.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
ValueTask AcknowledgeAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Extends the lease duration.
|
||||
/// </summary>
|
||||
/// <param name="extension">The time to extend the lease by.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
ValueTask RenewAsync(TimeSpan extension, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Releases the lease with the specified disposition.
|
||||
/// </summary>
|
||||
/// <param name="disposition">How to handle the message after release.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
ValueTask ReleaseAsync(ReleaseDisposition disposition, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Moves the message to the dead-letter queue.
|
||||
/// </summary>
|
||||
/// <param name="reason">The reason for dead-lettering.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
ValueTask DeadLetterAsync(string reason, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies how to handle a message when releasing a lease.
|
||||
/// </summary>
|
||||
public enum ReleaseDisposition
|
||||
{
|
||||
/// <summary>
|
||||
/// Retry the message (make it available for redelivery).
|
||||
/// </summary>
|
||||
Retry,
|
||||
|
||||
/// <summary>
|
||||
/// Delay the message before making it available again.
|
||||
/// </summary>
|
||||
Delay,
|
||||
|
||||
/// <summary>
|
||||
/// Abandon the message (do not retry, but don't dead-letter either).
|
||||
/// Implementation may vary by transport.
|
||||
/// </summary>
|
||||
Abandon
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
namespace StellaOps.Messaging.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Transport-agnostic message queue interface.
|
||||
/// Consumers depend only on this abstraction without knowing which transport is used.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The message type.</typeparam>
|
||||
public interface IMessageQueue<TMessage> where TMessage : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the provider name for diagnostics (e.g., "valkey", "nats", "postgres").
|
||||
/// </summary>
|
||||
string ProviderName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the queue/stream name.
|
||||
/// </summary>
|
||||
string QueueName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues a message to the queue.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to enqueue.</param>
|
||||
/// <param name="options">Optional enqueue options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The result of the enqueue operation.</returns>
|
||||
ValueTask<EnqueueResult> EnqueueAsync(
|
||||
TMessage message,
|
||||
EnqueueOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Leases messages from the queue for processing.
|
||||
/// Messages remain invisible to other consumers until acknowledged or lease expires.
|
||||
/// </summary>
|
||||
/// <param name="request">The lease request parameters.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A list of message leases.</returns>
|
||||
ValueTask<IReadOnlyList<IMessageLease<TMessage>>> LeaseAsync(
|
||||
LeaseRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Claims expired leases from other consumers (pending entry list recovery).
|
||||
/// </summary>
|
||||
/// <param name="request">The claim request parameters.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A list of claimed message leases.</returns>
|
||||
ValueTask<IReadOnlyList<IMessageLease<TMessage>>> ClaimExpiredAsync(
|
||||
ClaimRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the approximate number of pending messages in the queue.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The approximate pending message count.</returns>
|
||||
ValueTask<long> GetPendingCountAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace StellaOps.Messaging.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating message queue instances.
|
||||
/// </summary>
|
||||
public interface IMessageQueueFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the provider name for this factory.
|
||||
/// </summary>
|
||||
string ProviderName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a message queue for the specified message type and options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The message type.</typeparam>
|
||||
/// <param name="options">The queue options.</param>
|
||||
/// <returns>A configured message queue instance.</returns>
|
||||
IMessageQueue<TMessage> Create<TMessage>(MessageQueueOptions options) where TMessage : class;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating distributed cache instances.
|
||||
/// </summary>
|
||||
public interface IDistributedCacheFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the provider name for this factory.
|
||||
/// </summary>
|
||||
string ProviderName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a distributed cache for the specified key and value types.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The key type.</typeparam>
|
||||
/// <typeparam name="TValue">The value type.</typeparam>
|
||||
/// <param name="options">The cache options.</param>
|
||||
/// <returns>A configured distributed cache instance.</returns>
|
||||
IDistributedCache<TKey, TValue> Create<TKey, TValue>(CacheOptions options);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a string-keyed distributed cache.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The value type.</typeparam>
|
||||
/// <param name="options">The cache options.</param>
|
||||
/// <returns>A configured distributed cache instance.</returns>
|
||||
IDistributedCache<TValue> Create<TValue>(CacheOptions options);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Messaging.Abstractions;
|
||||
using StellaOps.Messaging.Plugins;
|
||||
|
||||
namespace StellaOps.Messaging.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering messaging services.
|
||||
/// </summary>
|
||||
public static class MessagingServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds messaging services with plugin-based transport discovery.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configuration">The configuration.</param>
|
||||
/// <param name="configure">Optional configuration callback.</param>
|
||||
/// <returns>The service collection.</returns>
|
||||
public static IServiceCollection AddMessagingPlugins(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
Action<MessagingPluginOptions>? configure = null)
|
||||
{
|
||||
var options = new MessagingPluginOptions();
|
||||
configure?.Invoke(options);
|
||||
|
||||
services.AddSingleton<MessagingPluginLoader>();
|
||||
|
||||
var loader = new MessagingPluginLoader();
|
||||
var plugins = loader.LoadFromDirectory(options.PluginDirectory, options.SearchPattern);
|
||||
|
||||
// Also load from assemblies in the current domain that might contain plugins
|
||||
var domainAssemblies = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(a => a.GetName().Name?.StartsWith("StellaOps.Messaging.Transport.") == true);
|
||||
var domainPlugins = loader.LoadFromAssemblies(domainAssemblies);
|
||||
|
||||
var allPlugins = plugins.Concat(domainPlugins)
|
||||
.GroupBy(p => p.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g => g.First())
|
||||
.ToList();
|
||||
|
||||
var registered = loader.RegisterConfiguredTransport(
|
||||
allPlugins,
|
||||
services,
|
||||
configuration,
|
||||
options.ConfigurationSection);
|
||||
|
||||
if (!registered && options.RequireTransport)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"No messaging transport configured. Set '{options.ConfigurationSection}:transport' to one of: {string.Join(", ", allPlugins.Select(p => p.Name))}");
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds messaging services with a specific transport plugin.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPlugin">The transport plugin type.</typeparam>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configuration">The configuration.</param>
|
||||
/// <param name="configSection">The configuration section for the transport.</param>
|
||||
/// <returns>The service collection.</returns>
|
||||
public static IServiceCollection AddMessagingTransport<TPlugin>(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
string configSection = "messaging")
|
||||
where TPlugin : IMessagingTransportPlugin, new()
|
||||
{
|
||||
var plugin = new TPlugin();
|
||||
var context = new MessagingTransportRegistrationContext(
|
||||
services,
|
||||
configuration,
|
||||
$"{configSection}:{plugin.Name}");
|
||||
|
||||
plugin.Register(context);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a message queue for a specific message type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The message type.</typeparam>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="options">The queue options.</param>
|
||||
/// <returns>The service collection.</returns>
|
||||
public static IServiceCollection AddMessageQueue<TMessage>(
|
||||
this IServiceCollection services,
|
||||
MessageQueueOptions options)
|
||||
where TMessage : class
|
||||
{
|
||||
services.AddSingleton(sp =>
|
||||
{
|
||||
var factory = sp.GetRequiredService<IMessageQueueFactory>();
|
||||
return factory.Create<TMessage>(options);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a distributed cache for a specific value type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The value type.</typeparam>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="options">The cache options.</param>
|
||||
/// <returns>The service collection.</returns>
|
||||
public static IServiceCollection AddDistributedCache<TValue>(
|
||||
this IServiceCollection services,
|
||||
CacheOptions options)
|
||||
{
|
||||
services.AddSingleton(sp =>
|
||||
{
|
||||
var factory = sp.GetRequiredService<IDistributedCacheFactory>();
|
||||
return factory.Create<TValue>(options);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
59
src/__Libraries/StellaOps.Messaging/Options/CacheOptions.cs
Normal file
59
src/__Libraries/StellaOps.Messaging/Options/CacheOptions.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
namespace StellaOps.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for a distributed cache.
|
||||
/// </summary>
|
||||
public class CacheOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the key prefix for all cache entries.
|
||||
/// </summary>
|
||||
public string? KeyPrefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default time-to-live for cache entries.
|
||||
/// </summary>
|
||||
public TimeSpan? DefaultTtl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to use sliding expiration.
|
||||
/// If true, TTL is reset on each access.
|
||||
/// </summary>
|
||||
public bool SlidingExpiration { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for individual cache entries.
|
||||
/// </summary>
|
||||
public class CacheEntryOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the absolute expiration time.
|
||||
/// </summary>
|
||||
public DateTimeOffset? AbsoluteExpiration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time-to-live relative to now.
|
||||
/// </summary>
|
||||
public TimeSpan? TimeToLive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to use sliding expiration for this entry.
|
||||
/// </summary>
|
||||
public bool? SlidingExpiration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates options with a specific TTL.
|
||||
/// </summary>
|
||||
public static CacheEntryOptions WithTtl(TimeSpan ttl) => new() { TimeToLive = ttl };
|
||||
|
||||
/// <summary>
|
||||
/// Creates options with absolute expiration.
|
||||
/// </summary>
|
||||
public static CacheEntryOptions ExpiresAt(DateTimeOffset expiration) => new() { AbsoluteExpiration = expiration };
|
||||
|
||||
/// <summary>
|
||||
/// Creates options with sliding expiration.
|
||||
/// </summary>
|
||||
public static CacheEntryOptions Sliding(TimeSpan slidingWindow) => new() { TimeToLive = slidingWindow, SlidingExpiration = true };
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace StellaOps.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for a message queue.
|
||||
/// </summary>
|
||||
public class MessageQueueOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the queue/stream name.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string QueueName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the consumer group name.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string ConsumerGroup { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the consumer name within the group.
|
||||
/// Defaults to machine name + process ID.
|
||||
/// </summary>
|
||||
public string? ConsumerName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the dead-letter queue name.
|
||||
/// If null, dead-lettering may not be supported.
|
||||
/// </summary>
|
||||
public string? DeadLetterQueue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default lease duration for messages.
|
||||
/// </summary>
|
||||
public TimeSpan DefaultLeaseDuration { get; set; } = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of delivery attempts before dead-lettering.
|
||||
/// </summary>
|
||||
public int MaxDeliveryAttempts { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the idempotency window for duplicate detection.
|
||||
/// </summary>
|
||||
public TimeSpan IdempotencyWindow { get; set; } = TimeSpan.FromHours(24);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the approximate maximum queue length (stream trimming).
|
||||
/// Null means no limit.
|
||||
/// </summary>
|
||||
public int? ApproximateMaxLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the initial backoff for retry delays.
|
||||
/// </summary>
|
||||
public TimeSpan RetryInitialBackoff { get; set; } = TimeSpan.FromSeconds(1);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum backoff for retry delays.
|
||||
/// </summary>
|
||||
public TimeSpan RetryMaxBackoff { get; set; } = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the backoff multiplier for exponential backoff.
|
||||
/// </summary>
|
||||
public double RetryBackoffMultiplier { get; set; } = 2.0;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace StellaOps.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// Options for configuring messaging plugin discovery and loading.
|
||||
/// </summary>
|
||||
public class MessagingPluginOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the directory to search for transport plugins.
|
||||
/// </summary>
|
||||
public string PluginDirectory { get; set; } = "plugins/messaging";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the search pattern for plugin assemblies.
|
||||
/// </summary>
|
||||
public string SearchPattern { get; set; } = "StellaOps.Messaging.Transport.*.dll";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the configuration section path for messaging options.
|
||||
/// </summary>
|
||||
public string ConfigurationSection { get; set; } = "messaging";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to throw if no transport is configured.
|
||||
/// </summary>
|
||||
public bool RequireTransport { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Plugin;
|
||||
|
||||
namespace StellaOps.Messaging.Plugins;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin contract for messaging transports.
|
||||
/// Transport plugins implement this interface to provide IMessageQueue and IDistributedCache implementations.
|
||||
/// </summary>
|
||||
public interface IMessagingTransportPlugin : IAvailabilityPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique transport name (e.g., "valkey", "nats", "postgres", "inmemory").
|
||||
/// </summary>
|
||||
new string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Registers transport services into the DI container.
|
||||
/// </summary>
|
||||
/// <param name="context">The registration context.</param>
|
||||
void Register(MessagingTransportRegistrationContext context);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Plugin;
|
||||
using StellaOps.Plugin.Hosting;
|
||||
|
||||
namespace StellaOps.Messaging.Plugins;
|
||||
|
||||
/// <summary>
|
||||
/// Loads and registers messaging transport plugins.
|
||||
/// </summary>
|
||||
public sealed class MessagingPluginLoader
|
||||
{
|
||||
private readonly ILogger<MessagingPluginLoader>? _logger;
|
||||
|
||||
public MessagingPluginLoader(ILogger<MessagingPluginLoader>? logger = null)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discovers and loads messaging transport plugins from the specified directory.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IMessagingTransportPlugin> LoadFromDirectory(
|
||||
string pluginDirectory,
|
||||
string searchPattern = "StellaOps.Messaging.Transport.*.dll")
|
||||
{
|
||||
if (!Directory.Exists(pluginDirectory))
|
||||
{
|
||||
_logger?.LogWarning("Plugin directory does not exist: {Directory}", pluginDirectory);
|
||||
return [];
|
||||
}
|
||||
|
||||
var options = new PluginHostOptions
|
||||
{
|
||||
PluginsDirectory = pluginDirectory,
|
||||
EnsureDirectoryExists = false,
|
||||
RecursiveSearch = false
|
||||
};
|
||||
options.SearchPatterns.Add(searchPattern);
|
||||
|
||||
var result = PluginHost.LoadPlugins(options);
|
||||
var plugins = new List<IMessagingTransportPlugin>();
|
||||
|
||||
foreach (var pluginAssembly in result.Plugins)
|
||||
{
|
||||
var transportPlugins = PluginLoader.LoadPlugins<IMessagingTransportPlugin>(new[] { pluginAssembly.Assembly });
|
||||
plugins.AddRange(transportPlugins);
|
||||
|
||||
foreach (var plugin in transportPlugins)
|
||||
{
|
||||
_logger?.LogDebug("Loaded messaging transport plugin: {Name} from {Assembly}",
|
||||
plugin.Name, pluginAssembly.Assembly.GetName().Name);
|
||||
}
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads messaging transport plugins from the specified assemblies.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IMessagingTransportPlugin> LoadFromAssemblies(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
return PluginLoader.LoadPlugins<IMessagingTransportPlugin>(assemblies);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds and registers the configured transport plugin.
|
||||
/// </summary>
|
||||
/// <param name="plugins">Available plugins.</param>
|
||||
/// <param name="services">Service collection.</param>
|
||||
/// <param name="configuration">Configuration.</param>
|
||||
/// <param name="configSectionPath">Configuration section path (default: "messaging").</param>
|
||||
/// <returns>True if a plugin was registered.</returns>
|
||||
public bool RegisterConfiguredTransport(
|
||||
IReadOnlyList<IMessagingTransportPlugin> plugins,
|
||||
IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
string configSectionPath = "messaging")
|
||||
{
|
||||
var messagingSection = configuration.GetSection(configSectionPath);
|
||||
var transportName = messagingSection.GetValue<string>("transport");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(transportName))
|
||||
{
|
||||
_logger?.LogWarning("No messaging transport configured at {Path}:transport", configSectionPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
var plugin = plugins.FirstOrDefault(p =>
|
||||
string.Equals(p.Name, transportName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (plugin is null)
|
||||
{
|
||||
_logger?.LogError("Messaging transport plugin '{Transport}' not found. Available: {Available}",
|
||||
transportName, string.Join(", ", plugins.Select(p => p.Name)));
|
||||
return false;
|
||||
}
|
||||
|
||||
var transportConfigSection = $"{configSectionPath}:{transportName}";
|
||||
var context = new MessagingTransportRegistrationContext(
|
||||
services,
|
||||
configuration,
|
||||
transportConfigSection);
|
||||
|
||||
plugin.Register(context);
|
||||
|
||||
_logger?.LogInformation("Registered messaging transport: {Transport}", transportName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Messaging.Plugins;
|
||||
|
||||
/// <summary>
|
||||
/// Context provided to transport plugins during registration.
|
||||
/// </summary>
|
||||
public sealed class MessagingTransportRegistrationContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new registration context.
|
||||
/// </summary>
|
||||
public MessagingTransportRegistrationContext(
|
||||
IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
string configurationSection,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
Services = services;
|
||||
Configuration = configuration;
|
||||
ConfigurationSection = configurationSection;
|
||||
LoggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service collection for registering services.
|
||||
/// </summary>
|
||||
public IServiceCollection Services { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration root.
|
||||
/// </summary>
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration section path for this transport (e.g., "messaging:valkey").
|
||||
/// </summary>
|
||||
public string ConfigurationSection { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logger factory for creating loggers during registration.
|
||||
/// </summary>
|
||||
public ILoggerFactory? LoggerFactory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration section for this transport.
|
||||
/// </summary>
|
||||
public IConfigurationSection GetTransportConfiguration() =>
|
||||
Configuration.GetSection(ConfigurationSection);
|
||||
}
|
||||
63
src/__Libraries/StellaOps.Messaging/Results/CacheResult.cs
Normal file
63
src/__Libraries/StellaOps.Messaging/Results/CacheResult.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
namespace StellaOps.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// Result of a cache get operation.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The value type.</typeparam>
|
||||
public readonly struct CacheResult<TValue>
|
||||
{
|
||||
private readonly TValue? _value;
|
||||
|
||||
private CacheResult(TValue? value, bool hasValue)
|
||||
{
|
||||
_value = value;
|
||||
HasValue = hasValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether a value was found in the cache.
|
||||
/// </summary>
|
||||
public bool HasValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cached value.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when no value is present.</exception>
|
||||
public TValue Value => HasValue
|
||||
? _value!
|
||||
: throw new InvalidOperationException("No value present in cache result.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value or a default.
|
||||
/// </summary>
|
||||
/// <param name="defaultValue">The default value to return if not cached.</param>
|
||||
/// <returns>The cached value or the default.</returns>
|
||||
public TValue GetValueOrDefault(TValue defaultValue = default!) =>
|
||||
HasValue ? _value! : defaultValue;
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get the value.
|
||||
/// </summary>
|
||||
/// <param name="value">The cached value, if present.</param>
|
||||
/// <returns>True if a value was present.</returns>
|
||||
public bool TryGetValue(out TValue? value)
|
||||
{
|
||||
value = _value;
|
||||
return HasValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a result with a value.
|
||||
/// </summary>
|
||||
public static CacheResult<TValue> Found(TValue value) => new(value, true);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a result indicating cache miss.
|
||||
/// </summary>
|
||||
public static CacheResult<TValue> Miss() => new(default, false);
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts a value to a found result.
|
||||
/// </summary>
|
||||
public static implicit operator CacheResult<TValue>(TValue value) => Found(value);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace StellaOps.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// Options for enqueue operations.
|
||||
/// </summary>
|
||||
public class EnqueueOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the idempotency key for duplicate detection.
|
||||
/// If null, no duplicate detection is performed.
|
||||
/// </summary>
|
||||
public string? IdempotencyKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the correlation ID for tracing.
|
||||
/// </summary>
|
||||
public string? CorrelationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tenant ID for multi-tenant scenarios.
|
||||
/// </summary>
|
||||
public string? TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the message priority (if supported by transport).
|
||||
/// Higher values indicate higher priority.
|
||||
/// </summary>
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets when the message should become visible (delayed delivery).
|
||||
/// </summary>
|
||||
public DateTimeOffset? VisibleAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets custom headers/metadata for the message.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string>? Headers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates options with an idempotency key.
|
||||
/// </summary>
|
||||
public static EnqueueOptions WithIdempotencyKey(string key) => new() { IdempotencyKey = key };
|
||||
|
||||
/// <summary>
|
||||
/// Creates options for delayed delivery.
|
||||
/// </summary>
|
||||
public static EnqueueOptions DelayedUntil(DateTimeOffset visibleAt) => new() { VisibleAt = visibleAt };
|
||||
|
||||
/// <summary>
|
||||
/// Creates options with correlation ID.
|
||||
/// </summary>
|
||||
public static EnqueueOptions WithCorrelation(string correlationId) => new() { CorrelationId = correlationId };
|
||||
}
|
||||
45
src/__Libraries/StellaOps.Messaging/Results/EnqueueResult.cs
Normal file
45
src/__Libraries/StellaOps.Messaging/Results/EnqueueResult.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
namespace StellaOps.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// Result of an enqueue operation.
|
||||
/// </summary>
|
||||
public readonly struct EnqueueResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the message ID assigned by the queue.
|
||||
/// </summary>
|
||||
public string MessageId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the message was enqueued successfully.
|
||||
/// </summary>
|
||||
public bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this was a duplicate message (idempotency).
|
||||
/// </summary>
|
||||
public bool WasDuplicate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the error message if the operation failed.
|
||||
/// </summary>
|
||||
public string? Error { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a successful result.
|
||||
/// </summary>
|
||||
public static EnqueueResult Succeeded(string messageId, bool wasDuplicate = false) =>
|
||||
new() { MessageId = messageId, Success = true, WasDuplicate = wasDuplicate };
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed result.
|
||||
/// </summary>
|
||||
public static EnqueueResult Failed(string error) =>
|
||||
new() { Success = false, Error = error, MessageId = string.Empty };
|
||||
|
||||
/// <summary>
|
||||
/// Creates a duplicate result.
|
||||
/// </summary>
|
||||
public static EnqueueResult Duplicate(string messageId) =>
|
||||
new() { MessageId = messageId, Success = true, WasDuplicate = true };
|
||||
}
|
||||
58
src/__Libraries/StellaOps.Messaging/Results/LeaseRequest.cs
Normal file
58
src/__Libraries/StellaOps.Messaging/Results/LeaseRequest.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
namespace StellaOps.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// Request parameters for leasing messages.
|
||||
/// </summary>
|
||||
public class LeaseRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of messages to lease.
|
||||
/// </summary>
|
||||
public int BatchSize { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the lease duration for the messages.
|
||||
/// If null, uses the queue's default lease duration.
|
||||
/// </summary>
|
||||
public TimeSpan? LeaseDuration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum time to wait for messages if none are available.
|
||||
/// Zero means don't wait (poll). Null means use transport default.
|
||||
/// </summary>
|
||||
public TimeSpan? WaitTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to only return messages from the pending entry list (redeliveries).
|
||||
/// </summary>
|
||||
public bool PendingOnly { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request parameters for claiming expired leases.
|
||||
/// </summary>
|
||||
public class ClaimRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of messages to claim.
|
||||
/// </summary>
|
||||
public int BatchSize { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum idle time for a message to be claimed.
|
||||
/// Messages must have been idle (not processed) for at least this duration.
|
||||
/// </summary>
|
||||
public TimeSpan MinIdleTime { get; set; } = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the new lease duration for claimed messages.
|
||||
/// If null, uses the queue's default lease duration.
|
||||
/// </summary>
|
||||
public TimeSpan? LeaseDuration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum number of delivery attempts for messages to claim.
|
||||
/// This helps avoid claiming messages that are still being processed for the first time.
|
||||
/// </summary>
|
||||
public int MinDeliveryAttempts { get; set; } = 1;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<RootNamespace>StellaOps.Messaging</RootNamespace>
|
||||
<AssemblyName>StellaOps.Messaging</AssemblyName>
|
||||
<Description>Transport-agnostic messaging abstractions for StellaOps (queues, caching, pub/sub)</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.Plugin/StellaOps.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user