From 44af4265c61a51ef69915f7feda88d815b565693 Mon Sep 17 00:00:00 2001 From: Rafael Andrade Date: Fri, 17 Jan 2025 15:15:20 +0000 Subject: [PATCH 01/17] Init Message Scheduler V1 --- src/Paramore.Brighter/CommandProcessor.cs | 167 +++++++++++++++++- .../CommandProcessorBuilder.cs | 30 +++- src/Paramore.Brighter/IAmACommandProcessor.cs | 42 +++++ src/Paramore.Brighter/IAmAMessageScheduler.cs | 5 + .../IAmAMessageSchedulerAsync.cs | 12 ++ .../IAmAMessageSchedulerFactory.cs | 9 + .../IAmAMessageSchedulerSync.cs | 10 ++ .../IAmASchedulerMessageConsumer.cs | 6 + .../IAmASchedulerMessageConsumerAsync.cs | 8 + .../IAmASchedulerMessageConsumerSync.cs | 6 + .../IAmAnOutboxProducerMediator.cs | 2 +- .../InMemoryMessageScheduler.cs | 123 +++++++++++++ .../InMemoryMessageSchedulerFactory.cs | 31 ++++ .../OutboxProducerMediator.cs | 10 +- .../SchedulerMessageConsumer.cs | 35 ++++ 15 files changed, 485 insertions(+), 11 deletions(-) create mode 100644 src/Paramore.Brighter/IAmAMessageScheduler.cs create mode 100644 src/Paramore.Brighter/IAmAMessageSchedulerAsync.cs create mode 100644 src/Paramore.Brighter/IAmAMessageSchedulerFactory.cs create mode 100644 src/Paramore.Brighter/IAmAMessageSchedulerSync.cs create mode 100644 src/Paramore.Brighter/IAmASchedulerMessageConsumer.cs create mode 100644 src/Paramore.Brighter/IAmASchedulerMessageConsumerAsync.cs create mode 100644 src/Paramore.Brighter/IAmASchedulerMessageConsumerSync.cs create mode 100644 src/Paramore.Brighter/InMemoryMessageScheduler.cs create mode 100644 src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs create mode 100644 src/Paramore.Brighter/SchedulerMessageConsumer.cs diff --git a/src/Paramore.Brighter/CommandProcessor.cs b/src/Paramore.Brighter/CommandProcessor.cs index f044cd106c..98b71471a2 100644 --- a/src/Paramore.Brighter/CommandProcessor.cs +++ b/src/Paramore.Brighter/CommandProcessor.cs @@ -37,6 +37,7 @@ THE SOFTWARE. */ using Paramore.Brighter.FeatureSwitch; using Paramore.Brighter.Logging; using Paramore.Brighter.Observability; +using Paramore.Brighter.Tasks; using Polly; using Polly.Registry; using Exception = System.Exception; @@ -61,6 +62,7 @@ public class CommandProcessor : IAmACommandProcessor private readonly IAmAFeatureSwitchRegistry? _featureSwitchRegistry; private readonly IEnumerable? _replySubscriptions; private readonly IAmABrighterTracer? _tracer; + private readonly IAmAMessageSchedulerFactory? _messageSchedulerFactory; //Uses -1 to indicate no outbox and will thus force a throw on a failed publish @@ -117,6 +119,7 @@ public class CommandProcessor : IAmACommandProcessor /// Do we want to insert an inbox handler into pipelines without the attribute. Null (default = no), yes = how to configure /// What is the tracer we will use for telemetry /// When creating a span for operations how noisy should the attributes be + /// TODO: ADD description public CommandProcessor( IAmASubscriberRegistry subscriberRegistry, IAmAHandlerFactory handlerFactory, @@ -125,7 +128,8 @@ public CommandProcessor( IAmAFeatureSwitchRegistry? featureSwitchRegistry = null, InboxConfiguration? inboxConfiguration = null, IAmABrighterTracer? tracer = null, - InstrumentationOptions instrumentationOptions = InstrumentationOptions.All) + InstrumentationOptions instrumentationOptions = InstrumentationOptions.All, + IAmAMessageSchedulerFactory? messageSchedulerFactory = null) { _subscriberRegistry = subscriberRegistry; @@ -144,6 +148,7 @@ public CommandProcessor( _inboxConfiguration = inboxConfiguration; _tracer = tracer; _instrumentationOptions = instrumentationOptions; + _messageSchedulerFactory = messageSchedulerFactory; } /// @@ -162,6 +167,7 @@ public CommandProcessor( /// If we are expecting a response, then we need a channel to listen on /// What is the tracer we will use for telemetry /// When creating a span for operations how noisy should the attributes be + /// TODO: ADD description public CommandProcessor( IAmASubscriberRegistry subscriberRegistry, IAmAHandlerFactory handlerFactory, @@ -173,14 +179,15 @@ public CommandProcessor( IEnumerable? replySubscriptions = null, IAmAChannelFactory? responseChannelFactory = null, IAmABrighterTracer? tracer = null, - InstrumentationOptions instrumentationOptions = InstrumentationOptions.All) - : this(subscriberRegistry, handlerFactory, requestContextFactory, policyRegistry, featureSwitchRegistry, inboxConfiguration) + InstrumentationOptions instrumentationOptions = InstrumentationOptions.All, + IAmAMessageSchedulerFactory? messageSchedulerFactory = null) + : this(subscriberRegistry, handlerFactory, requestContextFactory, policyRegistry, featureSwitchRegistry, inboxConfiguration, messageSchedulerFactory: messageSchedulerFactory) { _responseChannelFactory = responseChannelFactory; _tracer = tracer; _instrumentationOptions = instrumentationOptions; _replySubscriptions = replySubscriptions; - + InitExtServiceBus(bus); } @@ -217,6 +224,158 @@ public CommandProcessor( InitExtServiceBus(mediator); } + /// + public void Scheduler(TimeSpan delay, TRequest request, RequestContext? requestContext = null) + where TRequest : class, IRequest + => Scheduler(delay, request, null, requestContext); + + public void Scheduler(TimeSpan delay, + TRequest request, + IAmABoxTransactionProvider? transactionProvider, + RequestContext? requestContext = null) + where TRequest : class, IRequest + { + if (_messageSchedulerFactory == null) + { + throw new InvalidOperationException("No message scheduler factory set."); + } + + s_logger.LogInformation("Scheduling request: {RequestType} {Id}", request.GetType(), request.Id); + var span = _tracer?.CreateSpan(CommandProcessorSpanOperation.Send, request, requestContext?.Span, options: _instrumentationOptions); + var context = InitRequestContext(span, requestContext); + + var message = s_mediator!.CreateMessageFromRequest(request, context); + var scheduler = _messageSchedulerFactory.Create(s_mediator, transactionProvider); + if (scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(delay, message, context); + } + else if (scheduler is IAmAMessageSchedulerAsync asyncScheduler) + { + BrighterAsyncContext.Run(async () => await asyncScheduler.ScheduleAsync(delay, message, context)); + } + } + + /// + public void Scheduler(DateTimeOffset at, + TRequest request, + RequestContext? requestContext = null) + where TRequest : class, IRequest => Scheduler(at, request, null, requestContext); + + public void Scheduler(DateTimeOffset at, + TRequest request, + IAmABoxTransactionProvider? transactionProvider, + RequestContext? requestContext = null) + where TRequest : class, IRequest + { + if (_messageSchedulerFactory == null) + { + throw new InvalidOperationException("No message scheduler factory set."); + } + + s_logger.LogInformation("Scheduling request: {RequestType} {Id}", request.GetType(), request.Id); + var span = _tracer?.CreateSpan(CommandProcessorSpanOperation.Send, request, requestContext?.Span, options: _instrumentationOptions); + var context = InitRequestContext(span, requestContext); + + var message = s_mediator!.CreateMessageFromRequest(request, context); + var scheduler = _messageSchedulerFactory.Create(s_mediator, transactionProvider); + if (scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(at, message, context); + } + else if (scheduler is IAmAMessageSchedulerAsync asyncScheduler) + { + BrighterAsyncContext.Run(async () => await asyncScheduler.ScheduleAsync(at, message, context)); + } + } + + + /// + public async Task SchedulerAsync(TimeSpan delay, + TRequest request, + RequestContext? requestContext = null, + bool continueOnCapturedContext = true, + CancellationToken cancellationToken = default) + where TRequest : class, IRequest => + await SchedulerAsync(delay, + request, + null, + requestContext, + continueOnCapturedContext, + cancellationToken); + + public async Task SchedulerAsync(TimeSpan delay, + TRequest request, + IAmABoxTransactionProvider? transactionProvider, + RequestContext? requestContext = null, + bool continueOnCapturedContext = true, + CancellationToken cancellationToken = default) + where TRequest : class, IRequest + { + if (_messageSchedulerFactory == null) + { + throw new InvalidOperationException("No message scheduler factory set."); + } + + s_logger.LogInformation("Scheduling request: {RequestType} {Id}", request.GetType(), request.Id); + var span = _tracer?.CreateSpan(CommandProcessorSpanOperation.Send, request, requestContext?.Span, options: _instrumentationOptions); + var context = InitRequestContext(span, requestContext); + + var message = await s_mediator!.CreateMessageFromRequestAsync(request, context, cancellationToken).ConfigureAwait(continueOnCapturedContext); + var scheduler = _messageSchedulerFactory.Create(s_mediator, transactionProvider); + if (scheduler is IAmAMessageSchedulerAsync asyncScheduler) + { + await asyncScheduler.ScheduleAsync(delay, message, context, cancellationToken).ConfigureAwait(continueOnCapturedContext); + } + else if (scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(delay, message, context); + } + } + + /// + public async Task SchedulerAsync(DateTimeOffset at, + TRequest request, + RequestContext? requestContext = null, + bool continueOnCapturedContext = true, + CancellationToken cancellationToken = default) + where TRequest : class, IRequest => + await SchedulerAsync(at, + request, + null, + requestContext, + continueOnCapturedContext, + cancellationToken); + + public async Task SchedulerAsync(DateTimeOffset at, + TRequest request, + IAmABoxTransactionProvider? transactionProvider, + RequestContext? requestContext = null, + bool continueOnCapturedContext = true, + CancellationToken cancellationToken = default) + where TRequest : class, IRequest + { + if (_messageSchedulerFactory == null) + { + throw new InvalidOperationException("No message scheduler factory set."); + } + + s_logger.LogInformation("Scheduling request: {RequestType} {Id}", request.GetType(), request.Id); + var span = _tracer?.CreateSpan(CommandProcessorSpanOperation.Send, request, requestContext?.Span, options: _instrumentationOptions); + var context = InitRequestContext(span, requestContext); + + var message = await s_mediator!.CreateMessageFromRequestAsync(request, context, cancellationToken).ConfigureAwait(continueOnCapturedContext); + var scheduler = _messageSchedulerFactory.Create(s_mediator, transactionProvider); + if (scheduler is IAmAMessageSchedulerAsync asyncScheduler) + { + await asyncScheduler.ScheduleAsync(at, message, context, cancellationToken).ConfigureAwait(continueOnCapturedContext); + } + else if (scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(at, message, context); + } + } + /// /// Sends the specified command. We expect only one handler. The command is handled synchronously. /// diff --git a/src/Paramore.Brighter/CommandProcessorBuilder.cs b/src/Paramore.Brighter/CommandProcessorBuilder.cs index 10217804e3..72d5e32257 100644 --- a/src/Paramore.Brighter/CommandProcessorBuilder.cs +++ b/src/Paramore.Brighter/CommandProcessorBuilder.cs @@ -78,7 +78,13 @@ namespace Paramore.Brighter /// /// /// - public class CommandProcessorBuilder : INeedAHandlers, INeedPolicy, INeedMessaging, INeedInstrumentation, INeedARequestContext, IAmACommandProcessorBuilder + public class CommandProcessorBuilder : INeedAHandlers, + INeedPolicy, + INeedMessaging, + INeedInstrumentation, + INeedARequestContext, + INeedAMessageSchedulerFactory, + IAmACommandProcessorBuilder { private IAmARequestContextFactory? _requestContextFactory; private IAmASubscriberRegistry? _registry; @@ -93,6 +99,7 @@ public class CommandProcessorBuilder : INeedAHandlers, INeedPolicy, INeedMessagi private InboxConfiguration? _inboxConfiguration; private InstrumentationOptions? _instrumetationOptions; private IAmABrighterTracer? _tracer; + private IAmAMessageSchedulerFactory? _messageSchedulerFactory; private CommandProcessorBuilder() { @@ -250,6 +257,12 @@ public IAmACommandProcessorBuilder RequestContextFactory(IAmARequestContextFacto _requestContextFactory = requestContextFactory; return this; } + + public IAmACommandProcessorBuilder MessageSchedulerFactory(IAmAMessageSchedulerFactory messageSchedulerFactory) + { + _messageSchedulerFactory = messageSchedulerFactory; + return this; + } /// /// Builds the from the configuration. @@ -290,7 +303,8 @@ public CommandProcessor Build() featureSwitchRegistry: _featureSwitchRegistry, inboxConfiguration: _inboxConfiguration, tracer: _tracer, - instrumentationOptions: _instrumetationOptions.Value + instrumentationOptions: _instrumetationOptions.Value, + messageSchedulerFactory: _messageSchedulerFactory ); if (_useRequestReplyQueues) @@ -305,12 +319,15 @@ public CommandProcessor Build() replySubscriptions: _replySubscriptions, responseChannelFactory: _responseChannelFactory, tracer: _tracer, - instrumentationOptions: _instrumetationOptions.Value + instrumentationOptions: _instrumetationOptions.Value, + messageSchedulerFactory: _messageSchedulerFactory ); throw new ConfigurationException( "The configuration options chosen cannot be used to construct a command processor"); } + + } #region Progressive interfaces @@ -420,6 +437,12 @@ public interface INeedARequestContext /// IAmACommandProcessorBuilder. IAmACommandProcessorBuilder RequestContextFactory(IAmARequestContextFactory requestContextFactory); } + + // TODO Add doc + public interface INeedAMessageSchedulerFactory + { + IAmACommandProcessorBuilder MessageSchedulerFactory(IAmAMessageSchedulerFactory messageSchedulerFactory); + } /// /// Interface IAmACommandProcessorBuilder @@ -432,5 +455,6 @@ public interface IAmACommandProcessorBuilder /// CommandProcessor. CommandProcessor Build(); } + #endregion } diff --git a/src/Paramore.Brighter/IAmACommandProcessor.cs b/src/Paramore.Brighter/IAmACommandProcessor.cs index e59337f811..07db59f666 100644 --- a/src/Paramore.Brighter/IAmACommandProcessor.cs +++ b/src/Paramore.Brighter/IAmACommandProcessor.cs @@ -39,6 +39,48 @@ namespace Paramore.Brighter /// public interface IAmACommandProcessor { + /// + /// Sends the specified command. + /// + /// + /// The amount of delay to be used before send the message. + /// The command. + /// The context of the request; if null we will start one via a + void Scheduler(TimeSpan delay, TRequest request, RequestContext? requestContext = null) where TRequest : class, IRequest; + + /// + /// Sends the specified command. + /// + /// + /// The that the message should be published. + /// The command. + /// The context of the request; if null we will start one via a + void Scheduler(DateTimeOffset at, TRequest request, RequestContext? requestContext = null) where TRequest : class, IRequest; + + /// + /// Awaitably sends the specified command. + /// + /// + /// The amount of delay to be used before send the message. + /// The command. + /// The context of the request; if null we will start one via a + /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false + /// Allows the sender to cancel the request pipeline. Optional + /// awaitable . + Task SchedulerAsync(TimeSpan delay, TRequest request, RequestContext? requestContext = null, bool continueOnCapturedContext = true, CancellationToken cancellationToken = default) where TRequest : class, IRequest; + + /// + /// Awaitably sends the specified command. + /// + /// + /// The that the message should be published. + /// The command. + /// The context of the request; if null we will start one via a + /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false + /// Allows the sender to cancel the request pipeline. Optional + /// awaitable . + Task SchedulerAsync(DateTimeOffset at, TRequest request, RequestContext? requestContext = null, bool continueOnCapturedContext = true, CancellationToken cancellationToken = default) where TRequest : class, IRequest; + /// /// Sends the specified command. /// diff --git a/src/Paramore.Brighter/IAmAMessageScheduler.cs b/src/Paramore.Brighter/IAmAMessageScheduler.cs new file mode 100644 index 0000000000..588c846d25 --- /dev/null +++ b/src/Paramore.Brighter/IAmAMessageScheduler.cs @@ -0,0 +1,5 @@ +namespace Paramore.Brighter; + +public interface IAmAMessageScheduler +{ +} diff --git a/src/Paramore.Brighter/IAmAMessageSchedulerAsync.cs b/src/Paramore.Brighter/IAmAMessageSchedulerAsync.cs new file mode 100644 index 0000000000..86ea356212 --- /dev/null +++ b/src/Paramore.Brighter/IAmAMessageSchedulerAsync.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter; + +public interface IAmAMessageSchedulerAsync : IAmAMessageScheduler, IDisposable +{ + Task ScheduleAsync(DateTimeOffset at, Message message, RequestContext context, CancellationToken cancellationToken = default); + Task ScheduleAsync(TimeSpan delay, Message message, RequestContext context, CancellationToken cancellationToken = default); + Task CancelSchedulerAsync(string id, CancellationToken cancellationToken = default); +} diff --git a/src/Paramore.Brighter/IAmAMessageSchedulerFactory.cs b/src/Paramore.Brighter/IAmAMessageSchedulerFactory.cs new file mode 100644 index 0000000000..5327fe7c11 --- /dev/null +++ b/src/Paramore.Brighter/IAmAMessageSchedulerFactory.cs @@ -0,0 +1,9 @@ +namespace Paramore.Brighter; + +public interface IAmAMessageSchedulerFactory +{ + IAmAMessageScheduler Create(IAmAnOutboxProducerMediator mediator); + + IAmAMessageScheduler Create(IAmAnOutboxProducerMediator mediator, + IAmABoxTransactionProvider? transactionProvider); +} diff --git a/src/Paramore.Brighter/IAmAMessageSchedulerSync.cs b/src/Paramore.Brighter/IAmAMessageSchedulerSync.cs new file mode 100644 index 0000000000..50f06ccc61 --- /dev/null +++ b/src/Paramore.Brighter/IAmAMessageSchedulerSync.cs @@ -0,0 +1,10 @@ +using System; + +namespace Paramore.Brighter; + +public interface IAmAMessageSchedulerSync : IAmAMessageScheduler, IDisposable +{ + string Schedule(DateTimeOffset at, Message message, RequestContext context); + string Schedule(TimeSpan delay, Message message, RequestContext context); + void CancelScheduler(string id); +} diff --git a/src/Paramore.Brighter/IAmASchedulerMessageConsumer.cs b/src/Paramore.Brighter/IAmASchedulerMessageConsumer.cs new file mode 100644 index 0000000000..c917ae6c88 --- /dev/null +++ b/src/Paramore.Brighter/IAmASchedulerMessageConsumer.cs @@ -0,0 +1,6 @@ +namespace Paramore.Brighter; + +public interface IAmASchedulerMessageConsumer +{ + +} diff --git a/src/Paramore.Brighter/IAmASchedulerMessageConsumerAsync.cs b/src/Paramore.Brighter/IAmASchedulerMessageConsumerAsync.cs new file mode 100644 index 0000000000..77b87a8964 --- /dev/null +++ b/src/Paramore.Brighter/IAmASchedulerMessageConsumerAsync.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Paramore.Brighter; + +public interface IAmASchedulerMessageConsumerAsync : IAmASchedulerMessageConsumer +{ + Task ConsumeAsync(Message message, RequestContext context); +} diff --git a/src/Paramore.Brighter/IAmASchedulerMessageConsumerSync.cs b/src/Paramore.Brighter/IAmASchedulerMessageConsumerSync.cs new file mode 100644 index 0000000000..ea58eed2c7 --- /dev/null +++ b/src/Paramore.Brighter/IAmASchedulerMessageConsumerSync.cs @@ -0,0 +1,6 @@ +namespace Paramore.Brighter; + +public interface IAmASchedulerMessageConsumerSync : IAmASchedulerMessageConsumer +{ + void Consume(Message message, RequestContext context); +} diff --git a/src/Paramore.Brighter/IAmAnOutboxProducerMediator.cs b/src/Paramore.Brighter/IAmAnOutboxProducerMediator.cs index 9d0e34cb3b..0fe7df1d37 100644 --- a/src/Paramore.Brighter/IAmAnOutboxProducerMediator.cs +++ b/src/Paramore.Brighter/IAmAnOutboxProducerMediator.cs @@ -15,7 +15,7 @@ public interface IAmAnOutboxProducerMediator : IDisposable /// Used with RPC to call a remote service via the external bus /// /// The message to send - /// The context of the request pipeline + /// The context of the request pipeline /// The type of the call /// The type of the response void CallViaExternalBus(Message outMessage, RequestContext? requestContext) diff --git a/src/Paramore.Brighter/InMemoryMessageScheduler.cs b/src/Paramore.Brighter/InMemoryMessageScheduler.cs new file mode 100644 index 0000000000..448203aa39 --- /dev/null +++ b/src/Paramore.Brighter/InMemoryMessageScheduler.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Paramore.Brighter.Tasks; + +namespace Paramore.Brighter; + +public class InMemoryMessageScheduler : IAmAMessageSchedulerSync +{ + private readonly SchedulerMessageCollection _messages = new(); + private readonly IAmASchedulerMessageConsumer _consumer; + + private readonly Timer _timer; + + public InMemoryMessageScheduler(IAmASchedulerMessageConsumer consumer, + TimeSpan initialDelay, + TimeSpan period) + { + _consumer = consumer; + _timer = new Timer(Consume, this, initialDelay, period); + } + + private static void Consume(object? state) + { + var scheduler = (InMemoryMessageScheduler)state!; + + var now = DateTimeOffset.UtcNow; + var schedulerMessage = scheduler._messages.Next(now); + while (schedulerMessage != null) + { + if (scheduler._consumer is IAmASchedulerMessageConsumerSync syncConsumer) + { + syncConsumer.Consume(schedulerMessage.Message, schedulerMessage.Context); + } + else if (scheduler._consumer is IAmASchedulerMessageConsumerAsync asyncConsumer) + { + var tmp = schedulerMessage; + BrighterAsyncContext.Run(async () => await asyncConsumer.ConsumeAsync(tmp.Message, tmp.Context)); + } + + // TODO Add log + schedulerMessage = scheduler._messages.Next(now); + } + } + + public string Schedule(DateTimeOffset at, Message message, RequestContext context) + { + var id = Guid.NewGuid().ToString(); + _messages.Add(new SchedulerMessage(id, message, context, at)); + return id; + } + + public string Schedule(TimeSpan delay, Message message, RequestContext context) + => Schedule(DateTimeOffset.UtcNow.Add(delay), message, context); + + public void CancelScheduler(string id) + => _messages.Delete(id); + + public void Dispose() => _timer.Dispose(); + + + private record SchedulerMessage(string Id, Message Message, RequestContext Context, DateTimeOffset At); + + private class SchedulerMessageCollection + { + // It's a sorted list + private readonly object _lock = new(); + private readonly LinkedList _messages = new(); + + public SchedulerMessage? Next(DateTimeOffset now) + { + lock (_lock) + { + var first = _messages.First?.Value; + if (first == null || first.At >= now) + { + return null; + } + + _messages.RemoveFirst(); + return first; + } + } + + public void Add(SchedulerMessage message) + { + lock (_lock) + { + var node = _messages.First; + while (node != null) + { + if (node.Value.At > message.At) + { + _messages.AddBefore(node, message); + return; + } + + node = node.Next; + } + + _messages.AddLast(message); + } + } + + public void Delete(string id) + { + lock (_lock) + { + var node = _messages.First; + while (node != null) + { + if (node.Value.Id == id) + { + _messages.Remove(node); + return; + } + + node = node.Next; + } + } + } + } +} diff --git a/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs b/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs new file mode 100644 index 0000000000..345df932dd --- /dev/null +++ b/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Transactions; + +namespace Paramore.Brighter; + +public class InMemoryMessageSchedulerFactory(TimeSpan initialDelay, TimeSpan period) : IAmAMessageSchedulerFactory +{ + public InMemoryMessageSchedulerFactory() + : this(TimeSpan.Zero, TimeSpan.FromSeconds(1)) + { + } + + private static readonly Dictionary s_schedulers = new(); + + public IAmAMessageScheduler Create(IAmAnOutboxProducerMediator mediator) + => Create(mediator, null); + + public IAmAMessageScheduler Create(IAmAnOutboxProducerMediator mediator, + IAmABoxTransactionProvider? transactionProvider) + { + if (!s_schedulers.TryGetValue(typeof(TTransaction), out var scheduler)) + { + var consumer = new SchedulerMessageConsumer(mediator, transactionProvider); + scheduler = new InMemoryMessageScheduler(consumer, initialDelay, period); + s_schedulers[typeof(TTransaction)] = scheduler; + } + + return scheduler; + } +} diff --git a/src/Paramore.Brighter/OutboxProducerMediator.cs b/src/Paramore.Brighter/OutboxProducerMediator.cs index 523e283213..e5edb0ac35 100644 --- a/src/Paramore.Brighter/OutboxProducerMediator.cs +++ b/src/Paramore.Brighter/OutboxProducerMediator.cs @@ -58,6 +58,7 @@ public class OutboxProducerMediator : IAmAnOutboxProduce private readonly IAmAProducerRegistry _producerRegistry; private readonly InstrumentationOptions _instrumentationOptions; private readonly Dictionary> _outboxBatches = new(); + private readonly IAmAMessageSchedulerFactory? _messageSchedulerFactory; private static readonly SemaphoreSlim s_clearSemaphoreToken = new(1, 1); @@ -104,7 +105,7 @@ public OutboxProducerMediator( IAmAMessageMapperRegistry mapperRegistry, IAmAMessageTransformerFactory messageTransformerFactory, IAmAMessageTransformerFactoryAsync messageTransformerFactoryAsync, - IAmABrighterTracer tracer, + IAmABrighterTracer tracer, IAmAnOutbox? outbox = null, IAmARequestContextFactory? requestContextFactory = null, int outboxTimeout = 300, @@ -112,7 +113,8 @@ public OutboxProducerMediator( TimeSpan? maxOutStandingCheckInterval = null, Dictionary? outBoxBag = null, TimeProvider? timeProvider = null, - InstrumentationOptions instrumentationOptions = InstrumentationOptions.All) + InstrumentationOptions instrumentationOptions = InstrumentationOptions.All, + IAmAMessageSchedulerFactory? messageSchedulerFactory = null) { _producerRegistry = producerRegistry ?? throw new ConfigurationException("Missing Producer Registry for External Bus Services"); @@ -151,6 +153,7 @@ public OutboxProducerMediator( _outBoxBag = outBoxBag ?? new Dictionary(); _instrumentationOptions = instrumentationOptions; _tracer = tracer; + _messageSchedulerFactory = messageSchedulerFactory; ConfigureCallbacks(requestContextFactory.Create()); } @@ -748,7 +751,8 @@ private bool ConfigurePublisherCallbackMaybe(IAmAMessageProducer producer, Reque return false; } - private void Dispatch(IEnumerable posts, RequestContext requestContext, + private void Dispatch(IEnumerable posts, + RequestContext requestContext, Dictionary? args = null) { var parentSpan = requestContext.Span; diff --git a/src/Paramore.Brighter/SchedulerMessageConsumer.cs b/src/Paramore.Brighter/SchedulerMessageConsumer.cs new file mode 100644 index 0000000000..45cc2f96e6 --- /dev/null +++ b/src/Paramore.Brighter/SchedulerMessageConsumer.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading.Tasks; + +namespace Paramore.Brighter; + +public class SchedulerMessageConsumer( + IAmAnOutboxProducerMediator mediator, + IAmABoxTransactionProvider? transactionProvider) : + IAmASchedulerMessageConsumerSync, + IAmASchedulerMessageConsumerAsync +{ + public async Task ConsumeAsync(Message message, RequestContext context) + { + if (!mediator.HasOutbox()) + { + throw new InvalidOperationException("No outbox defined."); + } + + var outbox = (IAmAnOutboxProducerMediator)mediator; + await outbox.AddToOutboxAsync(message, context, transactionProvider); + await outbox.ClearOutboxAsync([message.Id], context); + } + + public void Consume(Message message, RequestContext context) + { + if (!mediator.HasOutbox()) + { + throw new InvalidOperationException("No outbox defined."); + } + + var outbox = (IAmAnOutboxProducerMediator)mediator; + outbox.AddToOutbox(message, context, transactionProvider); + outbox.ClearOutbox([message.Id], context); + } +} From b8d365bced89a2ef1cee9581db8dfce0152b0fdf Mon Sep 17 00:00:00 2001 From: Rafael Andrade Date: Fri, 17 Jan 2025 16:28:31 +0000 Subject: [PATCH 02/17] Init support to scheduler messages --- docker-compose-rmq.yaml | 11 +---------- .../AWSTaskQueue/GreetingsSender/Program.cs | 18 +++++++++++++++++- .../RMQTaskQueue/GreetingsSender/Program.cs | 18 +++++++++++++++++- .../ServiceCollectionExtensions.cs | 4 ++++ .../SnsMessagePublisher.cs | 5 +++++ .../CommandProcessorBuilder.cs | 4 +++- .../OutboxProducerMediator.cs | 5 +---- .../SchedulerMessageConsumer.cs | 5 +++++ 8 files changed, 53 insertions(+), 17 deletions(-) diff --git a/docker-compose-rmq.yaml b/docker-compose-rmq.yaml index a2846c8a1a..5b2b5f43d0 100644 --- a/docker-compose-rmq.yaml +++ b/docker-compose-rmq.yaml @@ -1,16 +1,7 @@ -version: '3' - services: rabbitmq: - image: brightercommand/rabbitmq:latest + image: masstransit/rabbitmq platform: linux/arm64 ports: - "5672:5672" - "15672:15672" - volumes: - - rabbitmq-home:/var/lib/rabbitmq - -volumes: - rabbitmq-home: - driver: local - \ No newline at end of file diff --git a/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs b/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs index 632152f373..d7feba70dc 100644 --- a/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs +++ b/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs @@ -75,7 +75,9 @@ static void Main(string[] args) } ).Create(); - serviceCollection.AddBrighter() + serviceCollection + .AddSingleton(new InMemoryMessageSchedulerFactory()) + .AddBrighter() .UseExternalBus((configure) => { configure.ProducerRegistry = producerRegistry; @@ -87,6 +89,20 @@ static void Main(string[] args) var commandProcessor = serviceProvider.GetService(); commandProcessor.Post(new GreetingEvent("Ian says: Hi there!")); + + // TODO Remove this code: + while (true) + { + Console.WriteLine("Enter a name to greet (Q to quit):"); + var name = Console.ReadLine(); + if (name is "Q" or "q") + { + break; + } + + commandProcessor.Scheduler(TimeSpan.FromSeconds(1), new GreetingEvent($"Ian says: Hi {name}")); + } + commandProcessor.Post(new FarewellEvent("Ian says: See you later!")); } } diff --git a/samples/TaskQueue/RMQTaskQueue/GreetingsSender/Program.cs b/samples/TaskQueue/RMQTaskQueue/GreetingsSender/Program.cs index d671b4b63b..1a1d1eadf1 100644 --- a/samples/TaskQueue/RMQTaskQueue/GreetingsSender/Program.cs +++ b/samples/TaskQueue/RMQTaskQueue/GreetingsSender/Program.cs @@ -74,7 +74,9 @@ static void Main(string[] args) } }).Create(); - serviceCollection.AddBrighter() + serviceCollection + .AddSingleton(new InMemoryMessageSchedulerFactory()) + .AddBrighter() .UseExternalBus((configure) => { configure.ProducerRegistry = producerRegistry; @@ -88,6 +90,20 @@ static void Main(string[] args) var commandProcessor = serviceProvider.GetService(); commandProcessor.Post(new GreetingEvent("Ian says: Hi there!")); + + // TODO Remove this code: + while (true) + { + Console.WriteLine("Enter a name to greet (Q to quit):"); + var name = Console.ReadLine(); + if (name is "Q" or "q") + { + break; + } + + commandProcessor.Scheduler(TimeSpan.FromSeconds(60), new GreetingEvent($"Ian says: Hi {name}")); + } + commandProcessor.Post(new FarewellEvent("Ian says: See you later!")); } } diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index 035e195b73..d9ace9c8a6 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -315,6 +315,10 @@ private static object BuildCommandProcessor(IServiceProvider provider) var requestContextFactory = provider.GetService(); var builder = contextBuilder.RequestContextFactory(requestContextFactory); + + var schedulerMessageFactory = provider.GetService(); + + builder.MessageSchedulerFactory(schedulerMessageFactory); var commandProcessor = builder.Build(); diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessagePublisher.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessagePublisher.cs index caf9bebd0d..84bda410de 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessagePublisher.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessagePublisher.cs @@ -50,6 +50,11 @@ public SnsMessagePublisher(string topicArn, AmazonSimpleNotificationServiceClien var messageString = message.Body.Value; var publishRequest = new PublishRequest(_topicArn, messageString, message.Header.Subject); + if (string.IsNullOrEmpty(message.Header.CorrelationId)) + { + message.Header.CorrelationId = Guid.NewGuid().ToString(); + } + var messageAttributes = new Dictionary { [HeaderNames.Id] = diff --git a/src/Paramore.Brighter/CommandProcessorBuilder.cs b/src/Paramore.Brighter/CommandProcessorBuilder.cs index 72d5e32257..2c8c1adb53 100644 --- a/src/Paramore.Brighter/CommandProcessorBuilder.cs +++ b/src/Paramore.Brighter/CommandProcessorBuilder.cs @@ -441,7 +441,7 @@ public interface INeedARequestContext // TODO Add doc public interface INeedAMessageSchedulerFactory { - IAmACommandProcessorBuilder MessageSchedulerFactory(IAmAMessageSchedulerFactory messageSchedulerFactory); + } /// @@ -449,6 +449,8 @@ public interface INeedAMessageSchedulerFactory /// public interface IAmACommandProcessorBuilder { + IAmACommandProcessorBuilder MessageSchedulerFactory(IAmAMessageSchedulerFactory messageSchedulerFactory); + /// /// Builds this instance. /// diff --git a/src/Paramore.Brighter/OutboxProducerMediator.cs b/src/Paramore.Brighter/OutboxProducerMediator.cs index e5edb0ac35..d4c43d588a 100644 --- a/src/Paramore.Brighter/OutboxProducerMediator.cs +++ b/src/Paramore.Brighter/OutboxProducerMediator.cs @@ -58,7 +58,6 @@ public class OutboxProducerMediator : IAmAnOutboxProduce private readonly IAmAProducerRegistry _producerRegistry; private readonly InstrumentationOptions _instrumentationOptions; private readonly Dictionary> _outboxBatches = new(); - private readonly IAmAMessageSchedulerFactory? _messageSchedulerFactory; private static readonly SemaphoreSlim s_clearSemaphoreToken = new(1, 1); @@ -113,8 +112,7 @@ public OutboxProducerMediator( TimeSpan? maxOutStandingCheckInterval = null, Dictionary? outBoxBag = null, TimeProvider? timeProvider = null, - InstrumentationOptions instrumentationOptions = InstrumentationOptions.All, - IAmAMessageSchedulerFactory? messageSchedulerFactory = null) + InstrumentationOptions instrumentationOptions = InstrumentationOptions.All) { _producerRegistry = producerRegistry ?? throw new ConfigurationException("Missing Producer Registry for External Bus Services"); @@ -153,7 +151,6 @@ public OutboxProducerMediator( _outBoxBag = outBoxBag ?? new Dictionary(); _instrumentationOptions = instrumentationOptions; _tracer = tracer; - _messageSchedulerFactory = messageSchedulerFactory; ConfigureCallbacks(requestContextFactory.Create()); } diff --git a/src/Paramore.Brighter/SchedulerMessageConsumer.cs b/src/Paramore.Brighter/SchedulerMessageConsumer.cs index 45cc2f96e6..c05b97164b 100644 --- a/src/Paramore.Brighter/SchedulerMessageConsumer.cs +++ b/src/Paramore.Brighter/SchedulerMessageConsumer.cs @@ -1,5 +1,7 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Logging; namespace Paramore.Brighter; @@ -9,8 +11,10 @@ public class SchedulerMessageConsumer( IAmASchedulerMessageConsumerSync, IAmASchedulerMessageConsumerAsync { + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger>(); public async Task ConsumeAsync(Message message, RequestContext context) { + s_logger.LogInformation("Publishing scheduler message"); if (!mediator.HasOutbox()) { throw new InvalidOperationException("No outbox defined."); @@ -23,6 +27,7 @@ public async Task ConsumeAsync(Message message, RequestContext context) public void Consume(Message message, RequestContext context) { + s_logger.LogInformation("Publishing scheduler message"); if (!mediator.HasOutbox()) { throw new InvalidOperationException("No outbox defined."); From 6ddfa0c97d511d5e34a96938caad1bdc7230d5bb Mon Sep 17 00:00:00 2001 From: Rafael Andrade Date: Mon, 20 Jan 2025 12:03:23 +0000 Subject: [PATCH 03/17] Improve scheduler --- .../AWSTaskQueue/GreetingsSender/Program.cs | 4 +- .../RMQTaskQueue/GreetingsSender/Program.cs | 2 +- .../ControlBus/ControlBusReceiverBuilder.cs | 3 + src/Paramore.Brighter/CommandProcessor.cs | 173 ++++++------------ src/Paramore.Brighter/IAmACommandProcessor.cs | 4 +- .../IAmAMessageSchedulerAsync.cs | 7 +- .../IAmAMessageSchedulerFactory.cs | 5 +- .../IAmAMessageSchedulerSync.cs | 7 +- .../IAmASchedulerMessageConsumer.cs | 6 - .../IAmASchedulerMessageConsumerAsync.cs | 8 - .../IAmASchedulerMessageConsumerSync.cs | 6 - .../InMemoryMessageScheduler.cs | 43 +++-- .../InMemoryMessageSchedulerFactory.cs | 26 +-- .../CommandProcessorSpanOperation.cs | 3 +- .../Scheduler/Events/SchedulerMessageFired.cs | 17 ++ .../Handlers/SchedulerMessageFiredHandler.cs | 104 +++++++++++ .../SchedulerMessageFiredHandlerAsync.cs | 123 +++++++++++++ .../SchedulerMessageConsumer.cs | 40 ---- 18 files changed, 363 insertions(+), 218 deletions(-) delete mode 100644 src/Paramore.Brighter/IAmASchedulerMessageConsumer.cs delete mode 100644 src/Paramore.Brighter/IAmASchedulerMessageConsumerAsync.cs delete mode 100644 src/Paramore.Brighter/IAmASchedulerMessageConsumerSync.cs create mode 100644 src/Paramore.Brighter/Scheduler/Events/SchedulerMessageFired.cs create mode 100644 src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandler.cs create mode 100644 src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandlerAsync.cs delete mode 100644 src/Paramore.Brighter/SchedulerMessageConsumer.cs diff --git a/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs b/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs index d7feba70dc..1b3add8d41 100644 --- a/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs +++ b/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs @@ -86,7 +86,7 @@ static void Main(string[] args) var serviceProvider = serviceCollection.BuildServiceProvider(); - var commandProcessor = serviceProvider.GetService(); + var commandProcessor = serviceProvider.GetRequiredService(); commandProcessor.Post(new GreetingEvent("Ian says: Hi there!")); @@ -100,7 +100,7 @@ static void Main(string[] args) break; } - commandProcessor.Scheduler(TimeSpan.FromSeconds(1), new GreetingEvent($"Ian says: Hi {name}")); + commandProcessor.SchedulerPost(TimeSpan.FromSeconds(10), new GreetingEvent($"Ian says: Hi {name}")); } commandProcessor.Post(new FarewellEvent("Ian says: See you later!")); diff --git a/samples/TaskQueue/RMQTaskQueue/GreetingsSender/Program.cs b/samples/TaskQueue/RMQTaskQueue/GreetingsSender/Program.cs index 1a1d1eadf1..e6444cbbce 100644 --- a/samples/TaskQueue/RMQTaskQueue/GreetingsSender/Program.cs +++ b/samples/TaskQueue/RMQTaskQueue/GreetingsSender/Program.cs @@ -101,7 +101,7 @@ static void Main(string[] args) break; } - commandProcessor.Scheduler(TimeSpan.FromSeconds(60), new GreetingEvent($"Ian says: Hi {name}")); + commandProcessor.SchedulerPost(TimeSpan.FromSeconds(60), new GreetingEvent($"Ian says: Hi {name}")); } commandProcessor.Post(new FarewellEvent("Ian says: See you later!")); diff --git a/src/Paramore.Brighter.ServiceActivator/ControlBus/ControlBusReceiverBuilder.cs b/src/Paramore.Brighter.ServiceActivator/ControlBus/ControlBusReceiverBuilder.cs index 55f92b51d4..e757e16b7e 100644 --- a/src/Paramore.Brighter.ServiceActivator/ControlBus/ControlBusReceiverBuilder.cs +++ b/src/Paramore.Brighter.ServiceActivator/ControlBus/ControlBusReceiverBuilder.cs @@ -26,6 +26,8 @@ THE SOFTWARE. */ using System.Collections.Generic; using System.Transactions; using Paramore.Brighter.Observability; +using Paramore.Brighter.Scheduler.Events; +using Paramore.Brighter.Scheduler.Handlers; using Paramore.Brighter.ServiceActivator.Ports; using Paramore.Brighter.ServiceActivator.Ports.Commands; using Paramore.Brighter.ServiceActivator.Ports.Handlers; @@ -139,6 +141,7 @@ public Dispatcher Build(string hostName) var subscriberRegistry = new SubscriberRegistry(); subscriberRegistry.Register(); subscriberRegistry.Register(); + subscriberRegistry.RegisterAsync(); var incomingMessageMapperRegistry = new MessageMapperRegistry( new ControlBusMessageMapperFactory(), null diff --git a/src/Paramore.Brighter/CommandProcessor.cs b/src/Paramore.Brighter/CommandProcessor.cs index 98b71471a2..22068c6e79 100644 --- a/src/Paramore.Brighter/CommandProcessor.cs +++ b/src/Paramore.Brighter/CommandProcessor.cs @@ -37,6 +37,7 @@ THE SOFTWARE. */ using Paramore.Brighter.FeatureSwitch; using Paramore.Brighter.Logging; using Paramore.Brighter.Observability; +using Paramore.Brighter.Scheduler.Events; using Paramore.Brighter.Tasks; using Polly; using Polly.Registry; @@ -225,70 +226,49 @@ public CommandProcessor( } /// - public void Scheduler(TimeSpan delay, TRequest request, RequestContext? requestContext = null) - where TRequest : class, IRequest - => Scheduler(delay, request, null, requestContext); - - public void Scheduler(TimeSpan delay, - TRequest request, - IAmABoxTransactionProvider? transactionProvider, - RequestContext? requestContext = null) - where TRequest : class, IRequest - { - if (_messageSchedulerFactory == null) - { - throw new InvalidOperationException("No message scheduler factory set."); - } + public void SchedulerPost(TimeSpan delay, TRequest request, RequestContext? requestContext = null) + where TRequest : class, IRequest + { + if (_messageSchedulerFactory == null) + { + throw new InvalidOperationException("No message scheduler factory set."); + } - s_logger.LogInformation("Scheduling request: {RequestType} {Id}", request.GetType(), request.Id); - var span = _tracer?.CreateSpan(CommandProcessorSpanOperation.Send, request, requestContext?.Span, options: _instrumentationOptions); - var context = InitRequestContext(span, requestContext); - - var message = s_mediator!.CreateMessageFromRequest(request, context); - var scheduler = _messageSchedulerFactory.Create(s_mediator, transactionProvider); - if (scheduler is IAmAMessageSchedulerSync sync) - { - sync.Schedule(delay, message, context); - } - else if (scheduler is IAmAMessageSchedulerAsync asyncScheduler) - { - BrighterAsyncContext.Run(async () => await asyncScheduler.ScheduleAsync(delay, message, context)); - } - } + s_logger.LogInformation("Scheduling request: {RequestType} {Id}", request.GetType(), request.Id); + var scheduler = _messageSchedulerFactory.Create(this); + if (scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(delay, SchedulerFireType.Post, request); + } + else if (scheduler is IAmAMessageSchedulerAsync asyncScheduler) + { + BrighterAsyncContext.Run(async () => await asyncScheduler.ScheduleAsync(delay, SchedulerFireType.Post, request)); + } + } /// - public void Scheduler(DateTimeOffset at, + public void SchedulerPost(DateTimeOffset at, TRequest request, RequestContext? requestContext = null) - where TRequest : class, IRequest => Scheduler(at, request, null, requestContext); - - public void Scheduler(DateTimeOffset at, - TRequest request, - IAmABoxTransactionProvider? transactionProvider, - RequestContext? requestContext = null) where TRequest : class, IRequest { - if (_messageSchedulerFactory == null) + if (_messageSchedulerFactory == null) { throw new InvalidOperationException("No message scheduler factory set."); } s_logger.LogInformation("Scheduling request: {RequestType} {Id}", request.GetType(), request.Id); - var span = _tracer?.CreateSpan(CommandProcessorSpanOperation.Send, request, requestContext?.Span, options: _instrumentationOptions); - var context = InitRequestContext(span, requestContext); - - var message = s_mediator!.CreateMessageFromRequest(request, context); - var scheduler = _messageSchedulerFactory.Create(s_mediator, transactionProvider); - if (scheduler is IAmAMessageSchedulerSync sync) - { - sync.Schedule(at, message, context); - } - else if (scheduler is IAmAMessageSchedulerAsync asyncScheduler) - { - BrighterAsyncContext.Run(async () => await asyncScheduler.ScheduleAsync(at, message, context)); - } + var scheduler = _messageSchedulerFactory.Create(this); + if (scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(at, SchedulerFireType.Post, request); + } + else if (scheduler is IAmAMessageSchedulerAsync asyncScheduler) + { + BrighterAsyncContext.Run(async () => await asyncScheduler.ScheduleAsync(at, SchedulerFireType.Post, request)); + } } - + /// public async Task SchedulerAsync(TimeSpan delay, @@ -296,41 +276,23 @@ public async Task SchedulerAsync(TimeSpan delay, RequestContext? requestContext = null, bool continueOnCapturedContext = true, CancellationToken cancellationToken = default) - where TRequest : class, IRequest => - await SchedulerAsync(delay, - request, - null, - requestContext, - continueOnCapturedContext, - cancellationToken); - - public async Task SchedulerAsync(TimeSpan delay, - TRequest request, - IAmABoxTransactionProvider? transactionProvider, - RequestContext? requestContext = null, - bool continueOnCapturedContext = true, - CancellationToken cancellationToken = default) where TRequest : class, IRequest - { + { if (_messageSchedulerFactory == null) { throw new InvalidOperationException("No message scheduler factory set."); } s_logger.LogInformation("Scheduling request: {RequestType} {Id}", request.GetType(), request.Id); - var span = _tracer?.CreateSpan(CommandProcessorSpanOperation.Send, request, requestContext?.Span, options: _instrumentationOptions); - var context = InitRequestContext(span, requestContext); - - var message = await s_mediator!.CreateMessageFromRequestAsync(request, context, cancellationToken).ConfigureAwait(continueOnCapturedContext); - var scheduler = _messageSchedulerFactory.Create(s_mediator, transactionProvider); - if (scheduler is IAmAMessageSchedulerAsync asyncScheduler) - { - await asyncScheduler.ScheduleAsync(delay, message, context, cancellationToken).ConfigureAwait(continueOnCapturedContext); - } - else if (scheduler is IAmAMessageSchedulerSync sync) - { - sync.Schedule(delay, message, context); - } + var scheduler = _messageSchedulerFactory.Create(this); + if (scheduler is IAmAMessageSchedulerAsync asyncScheduler) + { + await asyncScheduler.ScheduleAsync(delay, SchedulerFireType.Post, request, cancellationToken); + } + else if (scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(delay, SchedulerFireType.Post, request); + } } /// @@ -339,42 +301,25 @@ public async Task SchedulerAsync(DateTimeOffset at, RequestContext? requestContext = null, bool continueOnCapturedContext = true, CancellationToken cancellationToken = default) - where TRequest : class, IRequest => - await SchedulerAsync(at, - request, - null, - requestContext, - continueOnCapturedContext, - cancellationToken); + where TRequest : class, IRequest + { + if (_messageSchedulerFactory == null) + { + throw new InvalidOperationException("No message scheduler factory set."); + } + + s_logger.LogInformation("Scheduling request: {RequestType} {Id}", request.GetType(), request.Id); + var scheduler = _messageSchedulerFactory.Create(this); + if (scheduler is IAmAMessageSchedulerAsync asyncScheduler) + { + await asyncScheduler.ScheduleAsync(at, SchedulerFireType.Post, request, cancellationToken); + } + else if (scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(at, SchedulerFireType.Post, request); + } + } - public async Task SchedulerAsync(DateTimeOffset at, - TRequest request, - IAmABoxTransactionProvider? transactionProvider, - RequestContext? requestContext = null, - bool continueOnCapturedContext = true, - CancellationToken cancellationToken = default) - where TRequest : class, IRequest - { - if (_messageSchedulerFactory == null) - { - throw new InvalidOperationException("No message scheduler factory set."); - } - - s_logger.LogInformation("Scheduling request: {RequestType} {Id}", request.GetType(), request.Id); - var span = _tracer?.CreateSpan(CommandProcessorSpanOperation.Send, request, requestContext?.Span, options: _instrumentationOptions); - var context = InitRequestContext(span, requestContext); - - var message = await s_mediator!.CreateMessageFromRequestAsync(request, context, cancellationToken).ConfigureAwait(continueOnCapturedContext); - var scheduler = _messageSchedulerFactory.Create(s_mediator, transactionProvider); - if (scheduler is IAmAMessageSchedulerAsync asyncScheduler) - { - await asyncScheduler.ScheduleAsync(at, message, context, cancellationToken).ConfigureAwait(continueOnCapturedContext); - } - else if (scheduler is IAmAMessageSchedulerSync sync) - { - sync.Schedule(at, message, context); - } - } /// /// Sends the specified command. We expect only one handler. The command is handled synchronously. diff --git a/src/Paramore.Brighter/IAmACommandProcessor.cs b/src/Paramore.Brighter/IAmACommandProcessor.cs index 07db59f666..9212adc7f0 100644 --- a/src/Paramore.Brighter/IAmACommandProcessor.cs +++ b/src/Paramore.Brighter/IAmACommandProcessor.cs @@ -46,7 +46,7 @@ public interface IAmACommandProcessor /// The amount of delay to be used before send the message. /// The command. /// The context of the request; if null we will start one via a - void Scheduler(TimeSpan delay, TRequest request, RequestContext? requestContext = null) where TRequest : class, IRequest; + void SchedulerPost(TimeSpan delay, TRequest request, RequestContext? requestContext = null) where TRequest : class, IRequest; /// /// Sends the specified command. @@ -55,7 +55,7 @@ public interface IAmACommandProcessor /// The that the message should be published. /// The command. /// The context of the request; if null we will start one via a - void Scheduler(DateTimeOffset at, TRequest request, RequestContext? requestContext = null) where TRequest : class, IRequest; + void SchedulerPost(DateTimeOffset at, TRequest request, RequestContext? requestContext = null) where TRequest : class, IRequest; /// /// Awaitably sends the specified command. diff --git a/src/Paramore.Brighter/IAmAMessageSchedulerAsync.cs b/src/Paramore.Brighter/IAmAMessageSchedulerAsync.cs index 86ea356212..80751b0068 100644 --- a/src/Paramore.Brighter/IAmAMessageSchedulerAsync.cs +++ b/src/Paramore.Brighter/IAmAMessageSchedulerAsync.cs @@ -1,12 +1,15 @@ using System; using System.Threading; using System.Threading.Tasks; +using Paramore.Brighter.Scheduler.Events; namespace Paramore.Brighter; public interface IAmAMessageSchedulerAsync : IAmAMessageScheduler, IDisposable { - Task ScheduleAsync(DateTimeOffset at, Message message, RequestContext context, CancellationToken cancellationToken = default); - Task ScheduleAsync(TimeSpan delay, Message message, RequestContext context, CancellationToken cancellationToken = default); + Task ScheduleAsync(DateTimeOffset at, SchedulerFireType fireType, TRequest request, CancellationToken cancellationToken = default) + where TRequest : class, IRequest; + Task ScheduleAsync(TimeSpan delay, SchedulerFireType fireType, TRequest request, CancellationToken cancellationToken = default) + where TRequest : class, IRequest; Task CancelSchedulerAsync(string id, CancellationToken cancellationToken = default); } diff --git a/src/Paramore.Brighter/IAmAMessageSchedulerFactory.cs b/src/Paramore.Brighter/IAmAMessageSchedulerFactory.cs index 5327fe7c11..beaf2b1567 100644 --- a/src/Paramore.Brighter/IAmAMessageSchedulerFactory.cs +++ b/src/Paramore.Brighter/IAmAMessageSchedulerFactory.cs @@ -2,8 +2,5 @@ public interface IAmAMessageSchedulerFactory { - IAmAMessageScheduler Create(IAmAnOutboxProducerMediator mediator); - - IAmAMessageScheduler Create(IAmAnOutboxProducerMediator mediator, - IAmABoxTransactionProvider? transactionProvider); + IAmAMessageScheduler Create(IAmACommandProcessor processor); } diff --git a/src/Paramore.Brighter/IAmAMessageSchedulerSync.cs b/src/Paramore.Brighter/IAmAMessageSchedulerSync.cs index 50f06ccc61..0deac43136 100644 --- a/src/Paramore.Brighter/IAmAMessageSchedulerSync.cs +++ b/src/Paramore.Brighter/IAmAMessageSchedulerSync.cs @@ -1,10 +1,13 @@ using System; +using Paramore.Brighter.Scheduler.Events; namespace Paramore.Brighter; public interface IAmAMessageSchedulerSync : IAmAMessageScheduler, IDisposable { - string Schedule(DateTimeOffset at, Message message, RequestContext context); - string Schedule(TimeSpan delay, Message message, RequestContext context); + string Schedule(DateTimeOffset at, SchedulerFireType fireType, TRequest request) + where TRequest : class, IRequest; + string Schedule(TimeSpan delay, SchedulerFireType fireType, TRequest request) + where TRequest : class, IRequest; void CancelScheduler(string id); } diff --git a/src/Paramore.Brighter/IAmASchedulerMessageConsumer.cs b/src/Paramore.Brighter/IAmASchedulerMessageConsumer.cs deleted file mode 100644 index c917ae6c88..0000000000 --- a/src/Paramore.Brighter/IAmASchedulerMessageConsumer.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Paramore.Brighter; - -public interface IAmASchedulerMessageConsumer -{ - -} diff --git a/src/Paramore.Brighter/IAmASchedulerMessageConsumerAsync.cs b/src/Paramore.Brighter/IAmASchedulerMessageConsumerAsync.cs deleted file mode 100644 index 77b87a8964..0000000000 --- a/src/Paramore.Brighter/IAmASchedulerMessageConsumerAsync.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Threading.Tasks; - -namespace Paramore.Brighter; - -public interface IAmASchedulerMessageConsumerAsync : IAmASchedulerMessageConsumer -{ - Task ConsumeAsync(Message message, RequestContext context); -} diff --git a/src/Paramore.Brighter/IAmASchedulerMessageConsumerSync.cs b/src/Paramore.Brighter/IAmASchedulerMessageConsumerSync.cs deleted file mode 100644 index ea58eed2c7..0000000000 --- a/src/Paramore.Brighter/IAmASchedulerMessageConsumerSync.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Paramore.Brighter; - -public interface IAmASchedulerMessageConsumerSync : IAmASchedulerMessageConsumer -{ - void Consume(Message message, RequestContext context); -} diff --git a/src/Paramore.Brighter/InMemoryMessageScheduler.cs b/src/Paramore.Brighter/InMemoryMessageScheduler.cs index 448203aa39..58fcc20faa 100644 --- a/src/Paramore.Brighter/InMemoryMessageScheduler.cs +++ b/src/Paramore.Brighter/InMemoryMessageScheduler.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Text.Json; using System.Threading; +using Paramore.Brighter.Scheduler.Events; using Paramore.Brighter.Tasks; namespace Paramore.Brighter; @@ -8,15 +10,15 @@ namespace Paramore.Brighter; public class InMemoryMessageScheduler : IAmAMessageSchedulerSync { private readonly SchedulerMessageCollection _messages = new(); - private readonly IAmASchedulerMessageConsumer _consumer; + private readonly IAmACommandProcessor _processor; private readonly Timer _timer; - public InMemoryMessageScheduler(IAmASchedulerMessageConsumer consumer, + public InMemoryMessageScheduler(IAmACommandProcessor processor, TimeSpan initialDelay, TimeSpan period) { - _consumer = consumer; + _processor = processor; _timer = new Timer(Consume, this, initialDelay, period); } @@ -28,38 +30,43 @@ private static void Consume(object? state) var schedulerMessage = scheduler._messages.Next(now); while (schedulerMessage != null) { - if (scheduler._consumer is IAmASchedulerMessageConsumerSync syncConsumer) + BrighterAsyncContext.Run(async () => await scheduler._processor.SendAsync(new SchedulerMessageFired(schedulerMessage.Id) { - syncConsumer.Consume(schedulerMessage.Message, schedulerMessage.Context); - } - else if (scheduler._consumer is IAmASchedulerMessageConsumerAsync asyncConsumer) - { - var tmp = schedulerMessage; - BrighterAsyncContext.Run(async () => await asyncConsumer.ConsumeAsync(tmp.Message, tmp.Context)); - } + FireType = schedulerMessage.FireType, + MessageType = schedulerMessage.MessageType, + MessageData = schedulerMessage.MessageData, + })); // TODO Add log schedulerMessage = scheduler._messages.Next(now); } } - public string Schedule(DateTimeOffset at, Message message, RequestContext context) + public string Schedule(DateTimeOffset at, SchedulerFireType fireType, TRequest request) + where TRequest : class, IRequest { var id = Guid.NewGuid().ToString(); - _messages.Add(new SchedulerMessage(id, message, context, at)); + _messages.Add(new SchedulerMessage(id, at, fireType, + typeof(TRequest).FullName!, + JsonSerializer.Serialize(request, JsonSerialisationOptions.Options))); return id; } - public string Schedule(TimeSpan delay, Message message, RequestContext context) - => Schedule(DateTimeOffset.UtcNow.Add(delay), message, context); + public string Schedule(TimeSpan delay, SchedulerFireType fireType, TRequest request) + where TRequest : class, IRequest + => Schedule(DateTimeOffset.UtcNow.Add(delay), fireType, request); - public void CancelScheduler(string id) + public void CancelScheduler(string id) => _messages.Delete(id); public void Dispose() => _timer.Dispose(); - - private record SchedulerMessage(string Id, Message Message, RequestContext Context, DateTimeOffset At); + private record SchedulerMessage( + string Id, + DateTimeOffset At, + SchedulerFireType FireType, + string MessageType, + string MessageData); private class SchedulerMessageCollection { diff --git a/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs b/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs index 345df932dd..29a5fbf722 100644 --- a/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs +++ b/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Transactions; namespace Paramore.Brighter; @@ -11,21 +9,25 @@ public InMemoryMessageSchedulerFactory() { } - private static readonly Dictionary s_schedulers = new(); + public IAmAMessageScheduler Create(IAmACommandProcessor processor) + { + return GetOrCreate(processor, initialDelay, period); + } - public IAmAMessageScheduler Create(IAmAnOutboxProducerMediator mediator) - => Create(mediator, null); + private static readonly object s_lock = new(); + private static InMemoryMessageScheduler? s_scheduler; - public IAmAMessageScheduler Create(IAmAnOutboxProducerMediator mediator, - IAmABoxTransactionProvider? transactionProvider) + private static InMemoryMessageScheduler GetOrCreate(IAmACommandProcessor processor, TimeSpan initialDelay, + TimeSpan period) { - if (!s_schedulers.TryGetValue(typeof(TTransaction), out var scheduler)) + if (s_scheduler == null) { - var consumer = new SchedulerMessageConsumer(mediator, transactionProvider); - scheduler = new InMemoryMessageScheduler(consumer, initialDelay, period); - s_schedulers[typeof(TTransaction)] = scheduler; + lock (s_lock) + { + s_scheduler ??= new InMemoryMessageScheduler(processor, initialDelay, period); + } } - return scheduler; + return s_scheduler; } } diff --git a/src/Paramore.Brighter/Observability/CommandProcessorSpanOperation.cs b/src/Paramore.Brighter/Observability/CommandProcessorSpanOperation.cs index 19f29e8b63..2eaa0e8ac8 100644 --- a/src/Paramore.Brighter/Observability/CommandProcessorSpanOperation.cs +++ b/src/Paramore.Brighter/Observability/CommandProcessorSpanOperation.cs @@ -34,5 +34,6 @@ public enum CommandProcessorSpanOperation Publish = 2, // Publish an event Deposit = 3, // Deposit a message in the outbox Clear = 4, // Clear a message from the outbox - Archive = 5 //Archive a message from the outbox + Archive = 5, //Archive a message from the outbox + Scheduler = 6 } diff --git a/src/Paramore.Brighter/Scheduler/Events/SchedulerMessageFired.cs b/src/Paramore.Brighter/Scheduler/Events/SchedulerMessageFired.cs new file mode 100644 index 0000000000..939d1ee809 --- /dev/null +++ b/src/Paramore.Brighter/Scheduler/Events/SchedulerMessageFired.cs @@ -0,0 +1,17 @@ +namespace Paramore.Brighter.Scheduler.Events; + +// TODO Add doc + +public class SchedulerMessageFired(string id) : Event(id) +{ + public SchedulerFireType FireType { get; set; } = SchedulerFireType.Send; + public string MessageType { get; set; } = string.Empty; + public string MessageData { get; set; } = string.Empty; +} + +public enum SchedulerFireType +{ + Send, + Publish, + Post +} diff --git a/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandler.cs b/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandler.cs new file mode 100644 index 0000000000..3b9d0942fd --- /dev/null +++ b/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandler.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Concurrent; +using System.Reflection; +using System.Text.Json; +using Paramore.Brighter.Scheduler.Events; + +namespace Paramore.Brighter.Scheduler.Handlers; + +// public class SchedulerMessageFiredHandler(IAmACommandProcessor processor) : RequestHandler +// { +// private static readonly ConcurrentDictionary s_types = new(); +// +// private static readonly MethodInfo s_sendMethod = typeof(SchedulerMessageFiredHandler) +// .GetMethod(nameof(Send), BindingFlags.Static | BindingFlags.NonPublic)!; +// +// private static readonly MethodInfo s_publishMethod = typeof(SchedulerMessageFiredHandler) +// .GetMethod(nameof(Publish), BindingFlags.Static | BindingFlags.NonPublic)!; +// +// private static readonly MethodInfo s_postMethod = typeof(SchedulerMessageFiredHandler) +// .GetMethod(nameof(Post), BindingFlags.Static | BindingFlags.NonPublic)!; +// +// private static readonly ConcurrentDictionary> s_send = new(); +// private static readonly ConcurrentDictionary> s_publish = new(); +// private static readonly ConcurrentDictionary> s_post = new(); +// +// public override SchedulerMessageFired Handle(SchedulerMessageFired command) +// { +// var type = s_types.GetOrAdd(command.MessageType, CreateType); +// if (command.FireType == SchedulerFireType.Send) +// { +// var send = s_send.GetOrAdd(type, CreateSend); +// send(processor, command.MessageData); +// } +// else if (command.FireType == SchedulerFireType.Publish) +// { +// var publish = s_publish.GetOrAdd(type, CreatePublish); +// publish(processor, command.MessageData); +// } +// else +// { +// var publish = s_publish.GetOrAdd(type, CreatePost); +// publish(processor, command.MessageData); +// } +// +// return base.Handle(command); +// } +// +// private static Type CreateType(string messageType) +// { +// var type = Type.GetType(messageType); +// if (type == null) +// { +// throw new InvalidOperationException($"The message type doesn't exits: '{messageType}'"); +// } +// +// return type; +// } +// +// private static void Send(IAmACommandProcessor commandProcessor, string data) +// where TRequest : class, IRequest +// { +// var request = JsonSerializer.Deserialize(data, JsonSerialisationOptions.Options)!; +// commandProcessor.Send(request); +// } +// +// private static Action CreateSend(Type type) +// { +// var method = s_sendMethod.MakeGenericMethod(type); +// var action = (Action)method +// .CreateDelegate(typeof(Action)); +// return action; +// } +// +// private static void Publish(IAmACommandProcessor commandProcessor, string data) +// where TRequest : class, IRequest +// { +// var request = JsonSerializer.Deserialize(data, JsonSerialisationOptions.Options)!; +// commandProcessor.Publish(request); +// } +// +// private static Action CreatePublish(Type type) +// { +// var method = s_publishMethod.MakeGenericMethod(type); +// var action = (Action)method +// .CreateDelegate(typeof(Action)); +// return action; +// } +// +// +// private static void Post(IAmACommandProcessor commandProcessor, string data) +// where TRequest : class, IRequest +// { +// var request = JsonSerializer.Deserialize(data, JsonSerialisationOptions.Options)!; +// commandProcessor.Post(request); +// } +// +// private static Action CreatePost(Type type) +// { +// var method = s_postMethod.MakeGenericMethod(type); +// var action = (Action)method +// .CreateDelegate(typeof(Action)); +// return action; +// } +// } diff --git a/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandlerAsync.cs b/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandlerAsync.cs new file mode 100644 index 0000000000..c6ced1badf --- /dev/null +++ b/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandlerAsync.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Concurrent; +using System.Reflection; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Paramore.Brighter.Scheduler.Events; + +namespace Paramore.Brighter.Scheduler.Handlers; + +public class SchedulerMessageFiredHandlerAsync(IAmACommandProcessor processor) + : RequestHandlerAsync +{ + private static readonly ConcurrentDictionary s_types = new(); + + private static readonly MethodInfo s_sendMethod = typeof(SchedulerMessageFiredHandlerAsync) + .GetMethod(nameof(SendAsync), BindingFlags.Static | BindingFlags.NonPublic)!; + + private static readonly MethodInfo s_publishMethod = typeof(SchedulerMessageFiredHandlerAsync) + .GetMethod(nameof(PublishAsync), BindingFlags.Static | BindingFlags.NonPublic)!; + + private static readonly MethodInfo s_postMethod = typeof(SchedulerMessageFiredHandlerAsync) + .GetMethod(nameof(PostAsync), BindingFlags.Static | BindingFlags.NonPublic)!; + + private static readonly ConcurrentDictionary> + s_send = new(); + + private static readonly ConcurrentDictionary> + s_publish = new(); + + private static readonly ConcurrentDictionary> + s_post = new(); + + public override async Task HandleAsync(SchedulerMessageFired command, + CancellationToken cancellationToken = default) + { + var type = s_types.GetOrAdd(command.MessageType, CreateType); + if (command.FireType == SchedulerFireType.Send) + { + var send = s_send.GetOrAdd(type, CreateSend); + await send(processor, command.MessageData, cancellationToken); + } + else if (command.FireType == SchedulerFireType.Publish) + { + var publish = s_publish.GetOrAdd(type, CreatePublish); + await publish(processor, command.MessageData, cancellationToken); + } + else + { + var publish = s_post.GetOrAdd(type, CreatePost); + await publish(processor, command.MessageData, cancellationToken); + } + + return await base.HandleAsync(command, cancellationToken); + } + + private static Type CreateType(string messageType) + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var assembly in assemblies) + { + var type = assembly.GetType(messageType); + if (type != null) + { + return type; + } + } + + throw new InvalidOperationException($"The message type could not be found: '{messageType}'"); + } + + private static async Task SendAsync(IAmACommandProcessor commandProcessor, + string data, + CancellationToken cancellationToken = default) + where TRequest : class, IRequest + { + var request = JsonSerializer.Deserialize(data, JsonSerialisationOptions.Options)!; + await commandProcessor.SendAsync(request, cancellationToken: cancellationToken); + } + + private static Func CreateSend(Type type) + { + var method = s_sendMethod.MakeGenericMethod(type); + var action = (Func)method + .CreateDelegate(typeof(Func)); + return action; + } + + private static async Task PublishAsync(IAmACommandProcessor commandProcessor, + string data, + CancellationToken cancellationToken) + where TRequest : class, IRequest + { + var request = JsonSerializer.Deserialize(data, JsonSerialisationOptions.Options)!; + await commandProcessor.PublishAsync(request, cancellationToken: cancellationToken); + } + + private static Func CreatePublish(Type type) + { + var method = s_publishMethod.MakeGenericMethod(type); + var action = (Func)method + .CreateDelegate(typeof(Func)); + return action; + } + + + private static async Task PostAsync(IAmACommandProcessor commandProcessor, + string data, + CancellationToken cancellationToken) + where TRequest : class, IRequest + { + var request = JsonSerializer.Deserialize(data, JsonSerialisationOptions.Options)!; + await commandProcessor.PostAsync(request, requestContext: new RequestContext(), cancellationToken: cancellationToken); + } + + private static Func CreatePost(Type type) + { + var method = s_postMethod.MakeGenericMethod(type); + var action = (Func)method + .CreateDelegate(typeof(Func)); + return action; + } +} diff --git a/src/Paramore.Brighter/SchedulerMessageConsumer.cs b/src/Paramore.Brighter/SchedulerMessageConsumer.cs deleted file mode 100644 index c05b97164b..0000000000 --- a/src/Paramore.Brighter/SchedulerMessageConsumer.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Paramore.Brighter.Logging; - -namespace Paramore.Brighter; - -public class SchedulerMessageConsumer( - IAmAnOutboxProducerMediator mediator, - IAmABoxTransactionProvider? transactionProvider) : - IAmASchedulerMessageConsumerSync, - IAmASchedulerMessageConsumerAsync -{ - private static readonly ILogger s_logger = ApplicationLogging.CreateLogger>(); - public async Task ConsumeAsync(Message message, RequestContext context) - { - s_logger.LogInformation("Publishing scheduler message"); - if (!mediator.HasOutbox()) - { - throw new InvalidOperationException("No outbox defined."); - } - - var outbox = (IAmAnOutboxProducerMediator)mediator; - await outbox.AddToOutboxAsync(message, context, transactionProvider); - await outbox.ClearOutboxAsync([message.Id], context); - } - - public void Consume(Message message, RequestContext context) - { - s_logger.LogInformation("Publishing scheduler message"); - if (!mediator.HasOutbox()) - { - throw new InvalidOperationException("No outbox defined."); - } - - var outbox = (IAmAnOutboxProducerMediator)mediator; - outbox.AddToOutbox(message, context, transactionProvider); - outbox.ClearOutbox([message.Id], context); - } -} From 8fa4a58d654120cd8b1ab240a2fb775dc59195b4 Mon Sep 17 00:00:00 2001 From: Rafael Andrade Date: Mon, 20 Jan 2025 14:14:08 +0000 Subject: [PATCH 04/17] Improve scheduler --- .../InMemoryMessageScheduler.cs | 13 ++- .../Scheduler/Events/SchedulerMessageFired.cs | 1 + .../Handlers/SchedulerMessageFiredHandler.cs | 104 ----------------- .../SchedulerMessageFiredHandlerAsync.cs | 109 ++++++------------ 4 files changed, 45 insertions(+), 182 deletions(-) delete mode 100644 src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandler.cs diff --git a/src/Paramore.Brighter/InMemoryMessageScheduler.cs b/src/Paramore.Brighter/InMemoryMessageScheduler.cs index 58fcc20faa..910e180b21 100644 --- a/src/Paramore.Brighter/InMemoryMessageScheduler.cs +++ b/src/Paramore.Brighter/InMemoryMessageScheduler.cs @@ -30,11 +30,13 @@ private static void Consume(object? state) var schedulerMessage = scheduler._messages.Next(now); while (schedulerMessage != null) { - BrighterAsyncContext.Run(async () => await scheduler._processor.SendAsync(new SchedulerMessageFired(schedulerMessage.Id) + var tmp = schedulerMessage; + BrighterAsyncContext.Run(async () => await scheduler._processor.SendAsync(new SchedulerMessageFired(tmp.Id) { - FireType = schedulerMessage.FireType, - MessageType = schedulerMessage.MessageType, - MessageData = schedulerMessage.MessageData, + FireType = tmp.FireType, + MessageType = tmp.MessageType, + MessageData = tmp.MessageData, + UseAsync = tmp.UseAsync })); // TODO Add log @@ -46,7 +48,7 @@ public string Schedule(DateTimeOffset at, SchedulerFireType fireType, where TRequest : class, IRequest { var id = Guid.NewGuid().ToString(); - _messages.Add(new SchedulerMessage(id, at, fireType, + _messages.Add(new SchedulerMessage(id, at, fireType, false, typeof(TRequest).FullName!, JsonSerializer.Serialize(request, JsonSerialisationOptions.Options))); return id; @@ -65,6 +67,7 @@ private record SchedulerMessage( string Id, DateTimeOffset At, SchedulerFireType FireType, + bool UseAsync, string MessageType, string MessageData); diff --git a/src/Paramore.Brighter/Scheduler/Events/SchedulerMessageFired.cs b/src/Paramore.Brighter/Scheduler/Events/SchedulerMessageFired.cs index 939d1ee809..4684f4e63c 100644 --- a/src/Paramore.Brighter/Scheduler/Events/SchedulerMessageFired.cs +++ b/src/Paramore.Brighter/Scheduler/Events/SchedulerMessageFired.cs @@ -4,6 +4,7 @@ public class SchedulerMessageFired(string id) : Event(id) { + public bool UseAsync { get; set; } public SchedulerFireType FireType { get; set; } = SchedulerFireType.Send; public string MessageType { get; set; } = string.Empty; public string MessageData { get; set; } = string.Empty; diff --git a/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandler.cs b/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandler.cs deleted file mode 100644 index 3b9d0942fd..0000000000 --- a/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandler.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Reflection; -using System.Text.Json; -using Paramore.Brighter.Scheduler.Events; - -namespace Paramore.Brighter.Scheduler.Handlers; - -// public class SchedulerMessageFiredHandler(IAmACommandProcessor processor) : RequestHandler -// { -// private static readonly ConcurrentDictionary s_types = new(); -// -// private static readonly MethodInfo s_sendMethod = typeof(SchedulerMessageFiredHandler) -// .GetMethod(nameof(Send), BindingFlags.Static | BindingFlags.NonPublic)!; -// -// private static readonly MethodInfo s_publishMethod = typeof(SchedulerMessageFiredHandler) -// .GetMethod(nameof(Publish), BindingFlags.Static | BindingFlags.NonPublic)!; -// -// private static readonly MethodInfo s_postMethod = typeof(SchedulerMessageFiredHandler) -// .GetMethod(nameof(Post), BindingFlags.Static | BindingFlags.NonPublic)!; -// -// private static readonly ConcurrentDictionary> s_send = new(); -// private static readonly ConcurrentDictionary> s_publish = new(); -// private static readonly ConcurrentDictionary> s_post = new(); -// -// public override SchedulerMessageFired Handle(SchedulerMessageFired command) -// { -// var type = s_types.GetOrAdd(command.MessageType, CreateType); -// if (command.FireType == SchedulerFireType.Send) -// { -// var send = s_send.GetOrAdd(type, CreateSend); -// send(processor, command.MessageData); -// } -// else if (command.FireType == SchedulerFireType.Publish) -// { -// var publish = s_publish.GetOrAdd(type, CreatePublish); -// publish(processor, command.MessageData); -// } -// else -// { -// var publish = s_publish.GetOrAdd(type, CreatePost); -// publish(processor, command.MessageData); -// } -// -// return base.Handle(command); -// } -// -// private static Type CreateType(string messageType) -// { -// var type = Type.GetType(messageType); -// if (type == null) -// { -// throw new InvalidOperationException($"The message type doesn't exits: '{messageType}'"); -// } -// -// return type; -// } -// -// private static void Send(IAmACommandProcessor commandProcessor, string data) -// where TRequest : class, IRequest -// { -// var request = JsonSerializer.Deserialize(data, JsonSerialisationOptions.Options)!; -// commandProcessor.Send(request); -// } -// -// private static Action CreateSend(Type type) -// { -// var method = s_sendMethod.MakeGenericMethod(type); -// var action = (Action)method -// .CreateDelegate(typeof(Action)); -// return action; -// } -// -// private static void Publish(IAmACommandProcessor commandProcessor, string data) -// where TRequest : class, IRequest -// { -// var request = JsonSerializer.Deserialize(data, JsonSerialisationOptions.Options)!; -// commandProcessor.Publish(request); -// } -// -// private static Action CreatePublish(Type type) -// { -// var method = s_publishMethod.MakeGenericMethod(type); -// var action = (Action)method -// .CreateDelegate(typeof(Action)); -// return action; -// } -// -// -// private static void Post(IAmACommandProcessor commandProcessor, string data) -// where TRequest : class, IRequest -// { -// var request = JsonSerializer.Deserialize(data, JsonSerialisationOptions.Options)!; -// commandProcessor.Post(request); -// } -// -// private static Action CreatePost(Type type) -// { -// var method = s_postMethod.MakeGenericMethod(type); -// var action = (Action)method -// .CreateDelegate(typeof(Action)); -// return action; -// } -// } diff --git a/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandlerAsync.cs b/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandlerAsync.cs index c6ced1badf..689b353f51 100644 --- a/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandlerAsync.cs +++ b/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandlerAsync.cs @@ -13,43 +13,20 @@ public class SchedulerMessageFiredHandlerAsync(IAmACommandProcessor processor) { private static readonly ConcurrentDictionary s_types = new(); - private static readonly MethodInfo s_sendMethod = typeof(SchedulerMessageFiredHandlerAsync) - .GetMethod(nameof(SendAsync), BindingFlags.Static | BindingFlags.NonPublic)!; + private static readonly MethodInfo s_executeAsyncMethod = typeof(SchedulerMessageFiredHandlerAsync) + .GetMethod(nameof(ExecuteAsync), BindingFlags.Static | BindingFlags.NonPublic)!; - private static readonly MethodInfo s_publishMethod = typeof(SchedulerMessageFiredHandlerAsync) - .GetMethod(nameof(PublishAsync), BindingFlags.Static | BindingFlags.NonPublic)!; - - private static readonly MethodInfo s_postMethod = typeof(SchedulerMessageFiredHandlerAsync) - .GetMethod(nameof(PostAsync), BindingFlags.Static | BindingFlags.NonPublic)!; - - private static readonly ConcurrentDictionary> - s_send = new(); - - private static readonly ConcurrentDictionary> - s_publish = new(); - - private static readonly ConcurrentDictionary> - s_post = new(); + private static readonly ConcurrentDictionary> + s_executeAsync = new(); public override async Task HandleAsync(SchedulerMessageFired command, CancellationToken cancellationToken = default) { var type = s_types.GetOrAdd(command.MessageType, CreateType); - if (command.FireType == SchedulerFireType.Send) - { - var send = s_send.GetOrAdd(type, CreateSend); - await send(processor, command.MessageData, cancellationToken); - } - else if (command.FireType == SchedulerFireType.Publish) - { - var publish = s_publish.GetOrAdd(type, CreatePublish); - await publish(processor, command.MessageData, cancellationToken); - } - else - { - var publish = s_post.GetOrAdd(type, CreatePost); - await publish(processor, command.MessageData, cancellationToken); - } + + var execute = s_executeAsync.GetOrAdd(type, CreateExecuteAsync); + await execute(processor, command.MessageData, command.UseAsync, command.FireType, cancellationToken); return await base.HandleAsync(command, cancellationToken); } @@ -69,55 +46,41 @@ private static Type CreateType(string messageType) throw new InvalidOperationException($"The message type could not be found: '{messageType}'"); } - private static async Task SendAsync(IAmACommandProcessor commandProcessor, - string data, - CancellationToken cancellationToken = default) - where TRequest : class, IRequest - { - var request = JsonSerializer.Deserialize(data, JsonSerialisationOptions.Options)!; - await commandProcessor.SendAsync(request, cancellationToken: cancellationToken); - } - - private static Func CreateSend(Type type) - { - var method = s_sendMethod.MakeGenericMethod(type); - var action = (Func)method - .CreateDelegate(typeof(Func)); - return action; - } - - private static async Task PublishAsync(IAmACommandProcessor commandProcessor, + private static ValueTask ExecuteAsync(IAmACommandProcessor commandProcessor, string data, + bool async, + SchedulerFireType fireType, CancellationToken cancellationToken) where TRequest : class, IRequest { var request = JsonSerializer.Deserialize(data, JsonSerialisationOptions.Options)!; - await commandProcessor.PublishAsync(request, cancellationToken: cancellationToken); - } - - private static Func CreatePublish(Type type) - { - var method = s_publishMethod.MakeGenericMethod(type); - var action = (Func)method - .CreateDelegate(typeof(Func)); - return action; - } - - - private static async Task PostAsync(IAmACommandProcessor commandProcessor, - string data, - CancellationToken cancellationToken) - where TRequest : class, IRequest - { - var request = JsonSerializer.Deserialize(data, JsonSerialisationOptions.Options)!; - await commandProcessor.PostAsync(request, requestContext: new RequestContext(), cancellationToken: cancellationToken); + switch (fireType) + { + case SchedulerFireType.Send when async: + return new ValueTask(commandProcessor.SendAsync(request, cancellationToken: cancellationToken)); + case SchedulerFireType.Send: + commandProcessor.Send(request); + return new ValueTask(); + case SchedulerFireType.Publish when async: + return new ValueTask(commandProcessor.PublishAsync(request, cancellationToken: cancellationToken)); + case SchedulerFireType.Publish: + commandProcessor.Publish(request); + return new ValueTask(); + case SchedulerFireType.Post when async: + return new ValueTask(commandProcessor.PostAsync(request, cancellationToken: cancellationToken)); + default: + commandProcessor.Post(request); + return new ValueTask(); + } } - private static Func CreatePost(Type type) + private static Func + CreateExecuteAsync(Type type) { - var method = s_postMethod.MakeGenericMethod(type); - var action = (Func)method - .CreateDelegate(typeof(Func)); - return action; + var method = s_executeAsyncMethod.MakeGenericMethod(type); + var func = (Func)method + .CreateDelegate( + typeof(Func)); + return func; } } From 0c5416332ac85844098484dfa42720fecd35acfa Mon Sep 17 00:00:00 2001 From: Rafael Andrade Date: Mon, 20 Jan 2025 14:48:22 +0000 Subject: [PATCH 05/17] Add ADR --- docs/adr/0024-Scheduling.md | 89 +++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 docs/adr/0024-Scheduling.md diff --git a/docs/adr/0024-Scheduling.md b/docs/adr/0024-Scheduling.md new file mode 100644 index 0000000000..fef0235691 --- /dev/null +++ b/docs/adr/0024-Scheduling.md @@ -0,0 +1,89 @@ +# 24. Scoping dependencies inline with lifetime scope + +Date: 2025-01-20 + +## Status + +Proposed + +## Context + +Adding the ability to schedule message (by providing `TimeSpan` or `DateTimeOffset`) give to user flexibility to `Send`, `Publis` and `Post`. + + + +## Decision + +Giving support to schedule message, it's necessary breaking on `IAmACommandProcessor` by adding these methods: + +```c# +public interface IAmACommandProcessor +{ + string SchedulerSend(TimeSpan delay, TRequest request) where TRequest : class, IRequest; + string SchedulerSend(DateTimeOffset delay, TRequest request) where TRequest : class, IRequest; + Task SchedulerSendAsync(TimeSpan delay, TRequest request, bool continueOnCapturedContext = true, CancellationToken cancellationToken = default) where TRequest : class, IRequest; + Task SchedulerSendAsync(DateTimeOffset delay, TRequest request, bool continueOnCapturedContext = true, CancellationToken cancellationToken = default) where TRequest : class, IRequest; + + string SchedulerPublish(TimeSpan delay, TRequest request) where TRequest : class, IRequest; + string SchedulerPublish(DateTimeOffset delay, TRequest request) where TRequest : class, IRequest; + Task SchedulerPublishAsync(TimeSpan delay, TRequest request, bool continueOnCapturedContext = true, CancellationToken cancellationToken = default) where TRequest : class, IRequest; + Task SchedulerPublishsync(DateTimeOffset delay, TRequest request, bool continueOnCapturedContext = true, CancellationToken cancellationToken = default) where TRequest : class, IRequest; + + string SchedulerPost(TimeSpan delay, TRequest request) where TRequest : class, IRequest; + string SchedulerPost(DateTimeOffset delay, TRequest request) where TRequest : class, IRequest; + Task SchedulerPostAsync(TimeSpan delay, TRequest request, bool continueOnCapturedContext = true, CancellationToken cancellationToken = default) where TRequest : class, IRequest; + Task SchedulerPostAsync(DateTimeOffset delay, TRequest request, bool continueOnCapturedContext = true, CancellationToken cancellationToken = default) where TRequest : class, IRequest; +} +``` + +Scheduling can be break into 2 part (Producer & Consumer): +- Producer -> Producing a message we are going to have a new interface: + +```c# +public interface IAmAMessageScheduler +{ +} + + +public interface IAmAMessageSchedulerAsync : IAmAMessageScheduler, IDisposable +{ + Task ScheduleAsync(DateTimeOffset at, SchedulerFireType fireType, TRequest request, CancellationToken cancellationToken = default) where TRequest : class, IRequest; + Task ScheduleAsync(TimeSpan delay, SchedulerFireType fireType, TRequest request, CancellationToken cancellationToken = default) where TRequest : class, IRequest; + Task CancelSchedulerAsync(string id, CancellationToken cancellationToken = default); +} + + +public interface IAmAMessageSchedulerSync : IAmAMessageScheduler, IDisposable +{ + string Schedule(DateTimeOffset at, SchedulerFireType fireType, TRequest request) where TRequest : class, IRequest; + string Schedule(TimeSpan delay, SchedulerFireType fireType, TRequest request) where TRequest : class, IRequest; + void CancelScheduler(string id); +} +``` + +- Consumer -> To avoid duplication code we are going to introduce a new message and have a handler for that: + +```c# +public class SchedulerMessageFired : Event +{ + ..... +} + + +public class SchedulerMessageFiredHandlerAsync(IAmACommandProcessor processor) : RequestHandlerAsync +{ + .... +} +``` + +So on Scheduler implementation we need to send the SchedulerMessageFired + +```c# +public class JobExecute(IAmACommandProcessor processor) +{ + public async Task ExecuteAsync(Arg arg) + { + await processor.SendAsync(new SchedulerMessageFired{ ... }); + } +} +``` \ No newline at end of file From 42b904cc8619403b4d069752505aaec62802997b Mon Sep 17 00:00:00 2001 From: Rafael Andrade Date: Mon, 20 Jan 2025 14:53:12 +0000 Subject: [PATCH 06/17] rollback unnecessary changes --- docker-compose-rmq.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docker-compose-rmq.yaml b/docker-compose-rmq.yaml index 5b2b5f43d0..a2846c8a1a 100644 --- a/docker-compose-rmq.yaml +++ b/docker-compose-rmq.yaml @@ -1,7 +1,16 @@ +version: '3' + services: rabbitmq: - image: masstransit/rabbitmq + image: brightercommand/rabbitmq:latest platform: linux/arm64 ports: - "5672:5672" - "15672:15672" + volumes: + - rabbitmq-home:/var/lib/rabbitmq + +volumes: + rabbitmq-home: + driver: local + \ No newline at end of file From b377821b4aeca76f5dfdbf6333e6b043d7d95528 Mon Sep 17 00:00:00 2001 From: Rafael Andrade Date: Mon, 3 Feb 2025 17:00:04 +0000 Subject: [PATCH 07/17] Improve Scheduler support --- Brighter.sln | 14 + Directory.Packages.props | 1 + .../RMQTaskQueue/GreetingsSender/Program.cs | 4 +- .../IBrighterBuilder.cs | 1 - .../ServiceCollectionExtensions.cs | 79 ++++-- .../HangfireMessageScheduler.cs | 68 +++++ .../HangfireMessageSchedulerFactory.cs | 13 + ....Brighter.MessageScheduler.Hangfire.csproj | 17 ++ .../SnsMessageProducer.cs | 81 +++--- .../SqsMessageProducer.cs | 23 +- .../AzureServiceBusMessageProducer.cs | 3 + .../KafkaMessageProducer.cs | 215 +++++++++------ .../MQTTMessageProducer.cs | 73 ++++- .../MsSqlMessageProducer.cs | 72 +++-- .../RmqMessageProducer.cs | 17 +- .../RmqMessageProducerFactory.cs | 8 +- .../RedisMessageProducer.cs | 137 +++++---- .../ControlBus/ControlBusReceiverBuilder.cs | 1 + src/Paramore.Brighter/CommandProcessor.cs | 261 +++++++++++------- .../CommandProcessorBuilder.cs | 132 +++++---- .../ControlBusSenderFactory.cs | 9 +- .../ExternalBusConfiguration.cs | 10 + src/Paramore.Brighter/IAmACommandProcessor.cs | 145 +++++----- .../IAmAControlBusSenderFactory.cs | 4 +- src/Paramore.Brighter/IAmAMessageProducer.cs | 5 + .../IAmAMessageProducerAsync.cs | 1 + .../IAmAMessageSchedulerAsync.cs | 54 +++- .../IAmAMessageSchedulerFactory.cs | 8 + .../IAmAMessageSchedulerSync.cs | 46 ++- .../InMemoryMessageScheduler.cs | 173 ++++++------ .../InMemoryMessageSchedulerFactory.cs | 11 +- src/Paramore.Brighter/InMemoryProducer.cs | 3 + .../OutboxProducerMediator.cs | 2 +- .../Scheduler/Events/SchedulerMessageFired.cs | 18 +- .../SchedulerMessageFiredHandlerAsync.cs | 80 +----- 35 files changed, 1134 insertions(+), 655 deletions(-) create mode 100644 src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageScheduler.cs create mode 100644 src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageSchedulerFactory.cs create mode 100644 src/Paramore.Brighter.MessageScheduler.Hangfire/Paramore.Brighter.MessageScheduler.Hangfire.csproj diff --git a/Brighter.sln b/Brighter.sln index ac7d8f0d5a..08cda26753 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -315,6 +315,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Salutation_Sweeper", "sampl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Locking.MsSql", "src\Paramore.Brighter.Locking.MsSql\Paramore.Brighter.Locking.MsSql.csproj", "{758EE237-C722-4A0A-908C-2D08C1E59025}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.MessageScheduler.Hangfire", "src\Paramore.Brighter.MessageScheduler.Hangfire\Paramore.Brighter.MessageScheduler.Hangfire.csproj", "{BF515169-0C70-4027-946F-89686BC552FE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1765,6 +1767,18 @@ Global {758EE237-C722-4A0A-908C-2D08C1E59025}.Release|Mixed Platforms.Build.0 = Release|Any CPU {758EE237-C722-4A0A-908C-2D08C1E59025}.Release|x86.ActiveCfg = Release|Any CPU {758EE237-C722-4A0A-908C-2D08C1E59025}.Release|x86.Build.0 = Release|Any CPU + {BF515169-0C70-4027-946F-89686BC552FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BF515169-0C70-4027-946F-89686BC552FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BF515169-0C70-4027-946F-89686BC552FE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {BF515169-0C70-4027-946F-89686BC552FE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {BF515169-0C70-4027-946F-89686BC552FE}.Debug|x86.ActiveCfg = Debug|Any CPU + {BF515169-0C70-4027-946F-89686BC552FE}.Debug|x86.Build.0 = Debug|Any CPU + {BF515169-0C70-4027-946F-89686BC552FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BF515169-0C70-4027-946F-89686BC552FE}.Release|Any CPU.Build.0 = Release|Any CPU + {BF515169-0C70-4027-946F-89686BC552FE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {BF515169-0C70-4027-946F-89686BC552FE}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {BF515169-0C70-4027-946F-89686BC552FE}.Release|x86.ActiveCfg = Release|Any CPU + {BF515169-0C70-4027-946F-89686BC552FE}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Directory.Packages.props b/Directory.Packages.props index 275e710dad..746b73b62c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -32,6 +32,7 @@ + diff --git a/samples/TaskQueue/RMQTaskQueue/GreetingsSender/Program.cs b/samples/TaskQueue/RMQTaskQueue/GreetingsSender/Program.cs index e6444cbbce..61ea97e52e 100644 --- a/samples/TaskQueue/RMQTaskQueue/GreetingsSender/Program.cs +++ b/samples/TaskQueue/RMQTaskQueue/GreetingsSender/Program.cs @@ -75,7 +75,6 @@ static void Main(string[] args) }).Create(); serviceCollection - .AddSingleton(new InMemoryMessageSchedulerFactory()) .AddBrighter() .UseExternalBus((configure) => { @@ -83,6 +82,7 @@ static void Main(string[] args) configure.MaxOutStandingMessages = 5; configure.MaxOutStandingCheckInterval = TimeSpan.FromMilliseconds(500); }) + .UseMessageScheduler(new InMemoryMessageSchedulerFactory()) .AutoFromAssemblies(); var serviceProvider = serviceCollection.BuildServiceProvider(); @@ -101,7 +101,7 @@ static void Main(string[] args) break; } - commandProcessor.SchedulerPost(TimeSpan.FromSeconds(60), new GreetingEvent($"Ian says: Hi {name}")); + commandProcessor.SchedulerPost(new GreetingEvent($"Ian says: Hi {name}"), TimeSpan.FromSeconds(60)); } commandProcessor.Post(new FarewellEvent("Ian says: See you later!")); diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/IBrighterBuilder.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/IBrighterBuilder.cs index c4e2fd64e9..3638544784 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/IBrighterBuilder.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/IBrighterBuilder.cs @@ -96,7 +96,6 @@ public interface IBrighterBuilder /// IPolicyRegistry PolicyRegistry { get; set; } - /// /// The IoC container to populate /// diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index d9ace9c8a6..af40cd6dcf 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -24,6 +24,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; @@ -32,6 +33,8 @@ THE SOFTWARE. */ using System.Text.Json; using Paramore.Brighter.DynamoDb; using Paramore.Brighter.Observability; +using Paramore.Brighter.Scheduler.Events; +using Paramore.Brighter.Scheduler.Handlers; using Polly.Registry; namespace Paramore.Brighter.Extensions.DependencyInjection @@ -103,9 +106,7 @@ public static IBrighterBuilder BrighterHandlerBuilder(IServiceCollection service if (options.PolicyRegistry == null) policyRegistry = new DefaultPolicy(); else policyRegistry = AddDefaults(options.PolicyRegistry); - services.TryAdd(new ServiceDescriptor(typeof(IAmACommandProcessor), - (serviceProvider) => (IAmACommandProcessor)BuildCommandProcessor(serviceProvider), - options.CommandProcessorLifetime)); + services.TryAdd(new ServiceDescriptor(typeof(IAmACommandProcessor), BuildCommandProcessor, options.CommandProcessorLifetime)); return new ServiceCollectionBrighterBuilder( services, @@ -147,12 +148,19 @@ public static IBrighterBuilder UseExternalBus( var busConfiguration = new ExternalBusConfiguration(); configure?.Invoke(busConfiguration); - + if (busConfiguration.ProducerRegistry == null) + { throw new ConfigurationException("An external bus must have an IAmAProducerRegistry"); + } brighterBuilder.Services.TryAddSingleton(busConfiguration.ProducerRegistry); + if (busConfiguration.MessageSchedulerFactory != null) + { + UseMessageScheduler(brighterBuilder, busConfiguration.MessageSchedulerFactory); + } + //default to using System Transactions if nothing provided, so we always technically can share the outbox transaction Type transactionProvider = busConfiguration.TransactionProvider ?? typeof(CommittableTransactionProvider); @@ -160,8 +168,12 @@ public static IBrighterBuilder UseExternalBus( Type transactionProviderInterface = typeof(IAmABoxTransactionProvider<>); Type transactionType = null; foreach (Type i in transactionProvider.GetInterfaces()) + { if (i.IsGenericType && i.GetGenericTypeDefinition() == transactionProviderInterface) + { transactionType = i.GetGenericArguments()[0]; + } + } if (transactionType == null) throw new ConfigurationException( @@ -179,11 +191,7 @@ public static IBrighterBuilder UseExternalBus( transactionProvider, serviceLifetime); //we always need an outbox in case of producer callbacks - var outbox = busConfiguration.Outbox; - if (outbox == null) - { - outbox = new InMemoryOutbox(TimeProvider.System); - } + var outbox = busConfiguration.Outbox ?? new InMemoryOutbox(TimeProvider.System); //we create the outbox from interfaces from the determined transaction type to prevent the need //to pass generic types as we know the transaction provider type @@ -226,7 +234,20 @@ public static IBrighterBuilder UseExternalBus( return brighterBuilder; } - + + /// + /// An external message scheduler factory + /// + /// The builder. + /// The message scheduler factory + /// + public static IBrighterBuilder UseMessageScheduler(this IBrighterBuilder builder, IAmAMessageSchedulerFactory factory) + { + builder.Services.AddSingleton(factory); + builder.AsyncHandlers(x => x.RegisterAsync()); + + return builder; + } private static INeedInstrumentation AddEventBus( IServiceProvider provider, @@ -236,6 +257,7 @@ private static INeedInstrumentation AddEventBus( var eventBus = provider.GetService(); var eventBusConfiguration = provider.GetService(); var serviceActivatorOptions = provider.GetService(); + var messageSchedulerFactory = eventBusConfiguration.MessageSchedulerFactory ?? provider.GetService(); INeedInstrumentation instrumentationBuilder = null; var hasEventBus = eventBus != null; @@ -250,8 +272,8 @@ private static INeedInstrumentation AddEventBus( eventBus, eventBusConfiguration.ResponseChannelFactory, eventBusConfiguration.ReplyQueueSubscriptions, - serviceActivatorOptions?.InboxConfiguration - ); + serviceActivatorOptions?.InboxConfiguration, + messageSchedulerFactory); } if (hasEventBus && useRpc) @@ -261,7 +283,8 @@ private static INeedInstrumentation AddEventBus( eventBus, eventBusConfiguration.ResponseChannelFactory, eventBusConfiguration.ReplyQueueSubscriptions, - serviceActivatorOptions?.InboxConfiguration + serviceActivatorOptions?.InboxConfiguration, + messageSchedulerFactory ); } @@ -281,7 +304,7 @@ private static IPolicyRegistry AddDefaults(IPolicyRegistry polic return policyRegistry; } - private static object BuildCommandProcessor(IServiceProvider provider) + private static IAmACommandProcessor BuildCommandProcessor(IServiceProvider provider) { var loggerFactory = provider.GetService(); ApplicationLogging.LoggerFactory = loggerFactory; @@ -305,24 +328,22 @@ private static object BuildCommandProcessor(IServiceProvider provider) var messagingBuilder = options.PolicyRegistry == null ? policyBuilder.DefaultPolicy() : policyBuilder.Policies(options.PolicyRegistry); - - INeedInstrumentation instrumentationBuilder = AddEventBus(provider, messagingBuilder, useRequestResponse); - - var tracer = provider.GetService(); - - var contextBuilder = instrumentationBuilder.ConfigureInstrumentation(tracer, options.InstrumentationOptions); - var requestContextFactory = provider.GetService(); + var command = AddEventBus(provider, messagingBuilder, useRequestResponse) + .ConfigureInstrumentation(provider.GetService(), options.InstrumentationOptions) + .RequestContextFactory(provider.GetService()) + .MessageSchedulerFactory(provider.GetService()) + .Build(); - var builder = contextBuilder.RequestContextFactory(requestContextFactory); - - var schedulerMessageFactory = provider.GetService(); - - builder.MessageSchedulerFactory(schedulerMessageFactory); - - var commandProcessor = builder.Build(); + var eventBusConfiguration = provider.GetService(); + var messageSchedulerFactory = eventBusConfiguration.MessageSchedulerFactory ?? provider.GetService(); + var producerRegistry = provider.GetService(); + if (messageSchedulerFactory != null && producerRegistry != null) + { + producerRegistry.Producers.Each(x => x.Scheduler = messageSchedulerFactory.Create(command)); + } - return commandProcessor; + return command; } private static IAmAnOutboxProducerMediator BuildOutBoxProducerMediator(IServiceProvider serviceProvider, diff --git a/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageScheduler.cs b/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageScheduler.cs new file mode 100644 index 0000000000..1371b66bf7 --- /dev/null +++ b/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageScheduler.cs @@ -0,0 +1,68 @@ +using Hangfire; +using Paramore.Brighter.Scheduler.Events; + +namespace Paramore.Brighter.MessageScheduler.Hangfire; + +/// +/// The Hangfire adaptor for . +/// +/// +/// +public class HangfireMessageScheduler( + IAmACommandProcessor processor, + IBackgroundJobClientV2 client, + string? queue) : IAmAMessageSchedulerSync, IAmAMessageSchedulerAsync +{ + /// + public string Schedule(Message message, DateTimeOffset at) + => client.Schedule(queue, () => ConsumeAsync(message), at); + + /// + public string Schedule(Message message, TimeSpan delay) + => client.Schedule(queue, () => ConsumeAsync(message), delay); + + /// + public bool ReScheduler(string schedulerId, DateTimeOffset at) => client.Reschedule(schedulerId, at); + + /// + public bool ReScheduler(string schedulerId, TimeSpan delay) => client.Reschedule(schedulerId, delay); + + /// + public void Cancel(string id) => client.Delete(queue, id); + + private async Task ConsumeAsync(Message message) + => await processor.SendAsync(new SchedulerMessageFired { Id = message.Id, Message = message }); + + /// + public Task ScheduleAsync(Message message, DateTimeOffset at, CancellationToken cancellationToken = default) + => Task.FromResult(Schedule(message, at)); + + /// + public Task ScheduleAsync(Message message, TimeSpan delay, CancellationToken cancellationToken = default) + => Task.FromResult(Schedule(message, delay)); + + /// + public Task ReSchedulerAsync(string schedulerId, DateTimeOffset at, + CancellationToken cancellationToken = default) + => Task.FromResult(ReScheduler(schedulerId, at)); + + /// + public Task ReSchedulerAsync(string schedulerId, TimeSpan delay, + CancellationToken cancellationToken = default) + => Task.FromResult(ReScheduler(schedulerId, delay)); + + /// + public Task CancelAsync(string id, CancellationToken cancellationToken = default) + { + Cancel(id); + return Task.CompletedTask; + } + + /// + public ValueTask DisposeAsync() => new(); + + /// + public void Dispose() + { + } +} diff --git a/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageSchedulerFactory.cs b/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageSchedulerFactory.cs new file mode 100644 index 0000000000..7080112ad9 --- /dev/null +++ b/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageSchedulerFactory.cs @@ -0,0 +1,13 @@ +using Hangfire; + +namespace Paramore.Brighter.MessageScheduler.Hangfire; + +/// +/// The factory +/// +public class HangfireMessageSchedulerFactory(IBackgroundJobClientV2 client, string? queue) : IAmAMessageSchedulerFactory +{ + /// + public IAmAMessageScheduler Create(IAmACommandProcessor processor) + => new HangfireMessageScheduler(processor, client, queue); +} diff --git a/src/Paramore.Brighter.MessageScheduler.Hangfire/Paramore.Brighter.MessageScheduler.Hangfire.csproj b/src/Paramore.Brighter.MessageScheduler.Hangfire/Paramore.Brighter.MessageScheduler.Hangfire.csproj new file mode 100644 index 0000000000..a16bd42cc6 --- /dev/null +++ b/src/Paramore.Brighter.MessageScheduler.Hangfire/Paramore.Brighter.MessageScheduler.Hangfire.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageProducer.cs index 1a8c222882..c1e68c182e 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageProducer.cs @@ -50,6 +50,9 @@ public class SnsMessageProducer : AWSMessagingGateway, IAmAMessageProducerSync, /// public Activity? Span { get; set; } + /// + public IAmAMessageScheduler? Scheduler { get; set; } + /// /// Initializes a new instance of the class. /// @@ -119,38 +122,15 @@ public async Task ConfirmTopicExistsAsync(string? topic = null, /// /// The message. /// Allows cancellation of the Send operation - public async Task SendAsync(Message message, CancellationToken cancellationToken = default) - { - s_logger.LogDebug( - "SNSMessageProducer: Publishing message with topic {Topic} and id {Id} and message: {Request}", - message.Header.Topic, message.Id, message.Body); - - await ConfirmTopicExistsAsync(message.Header.Topic, cancellationToken); - - if (string.IsNullOrEmpty(ChannelAddress)) - throw new InvalidOperationException( - $"Failed to publish message with topic {message.Header.Topic} and id {message.Id} and message: {message.Body} as the topic does not exist"); - - using var client = _clientFactory.CreateSnsClient(); - var publisher = new SnsMessagePublisher(ChannelAddress!, client, - _publication.SnsAttributes?.Type ?? SnsSqsType.Standard); - var messageId = await publisher.PublishAsync(message); - - if (messageId == null) - throw new InvalidOperationException( - $"Failed to publish message with topic {message.Header.Topic} and id {message.Id} and message: {message.Body}"); - - s_logger.LogDebug( - "SNSMessageProducer: Published message with topic {Topic}, Brighter messageId {MessageId} and SNS messageId {SNSMessageId}", - message.Header.Topic, message.Id, messageId); - } + public async Task SendAsync(Message message, CancellationToken cancellationToken = default) + => await SendWithDelayAsync(message, TimeSpan.Zero, cancellationToken); /// /// Sends the specified message. /// Sync over Async /// /// The message. - public void Send(Message message) => BrighterAsyncContext.Run(async () => await SendAsync(message)); + public void Send(Message message) => SendWithDelay(message, TimeSpan.Zero); /// /// Sends the specified message, with a delay. @@ -159,10 +139,7 @@ public async Task SendAsync(Message message, CancellationToken cancellationToken /// The sending delay /// Task. public void SendWithDelay(Message message, TimeSpan? delay = null) - { - // SNS doesn't support publish with delay - Send(message); - } + => BrighterAsyncContext.Run(async () => await SendWithDelayAsync(message, TimeSpan.Zero)); /// /// Sends the specified message, with a delay @@ -173,8 +150,46 @@ public void SendWithDelay(Message message, TimeSpan? delay = null) /// public async Task SendWithDelayAsync(Message message, TimeSpan? delay, CancellationToken cancellationToken = default) - { - // SNS doesn't support publish with delay - await SendAsync(message, cancellationToken); + { + delay ??= TimeSpan.Zero; + if (delay != TimeSpan.Zero) + { + if (Scheduler is IAmAMessageSchedulerAsync async) + { + await async.ScheduleAsync(message, delay.Value, cancellationToken); + return; + } + + if (Scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(message, delay.Value); + return; + } + + s_logger.LogWarning("SNSMessageProducer: no scheduler configured, message will be sent immediately"); + } + + s_logger.LogDebug( + "SNSMessageProducer: Publishing message with topic {Topic} and id {Id} and message: {Request}", + message.Header.Topic, message.Id, message.Body); + + await ConfirmTopicExistsAsync(message.Header.Topic, cancellationToken); + + if (string.IsNullOrEmpty(ChannelAddress)) + throw new InvalidOperationException( + $"Failed to publish message with topic {message.Header.Topic} and id {message.Id} and message: {message.Body} as the topic does not exist"); + + using var client = _clientFactory.CreateSnsClient(); + var publisher = new SnsMessagePublisher(ChannelAddress!, client, + _publication.SnsAttributes?.Type ?? SnsSqsType.Standard); + var messageId = await publisher.PublishAsync(message); + + if (messageId == null) + throw new InvalidOperationException( + $"Failed to publish message with topic {message.Header.Topic} and id {message.Id} and message: {message.Body}"); + + s_logger.LogDebug( + "SNSMessageProducer: Published message with topic {Topic}, Brighter messageId {MessageId} and SNS messageId {SNSMessageId}", + message.Header.Topic, message.Id, messageId); } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageProducer.cs index 63bee0c50d..50ecaad896 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageProducer.cs @@ -50,6 +50,9 @@ public class SqsMessageProducer : AWSMessagingGateway, IAmAMessageProducerAsync, /// public Activity? Span { get; set; } + /// + public IAmAMessageScheduler? Scheduler { get; set; } + /// /// Initialize a new instance of the . /// @@ -131,11 +134,29 @@ public async Task ConfirmQueueExistsAsync(string? queue = null, Cancellati } public async Task SendAsync(Message message, CancellationToken cancellationToken = default) - => await SendWithDelayAsync(message, null, cancellationToken); + => await SendWithDelayAsync(message, TimeSpan.Zero, cancellationToken); public async Task SendWithDelayAsync(Message message, TimeSpan? delay, CancellationToken cancellationToken = default) { + delay ??= TimeSpan.Zero; + if (delay > TimeSpan.FromMinutes(15)) + { + if (Scheduler is IAmAMessageSchedulerAsync async) + { + await async.ScheduleAsync(message, delay.Value, cancellationToken); + return; + } + + if (Scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(message, delay.Value); + return; + } + + s_logger.LogWarning("SQSMessageProducer: no scheduler configured, message will be sent immediately"); + } + s_logger.LogDebug( "SQSMessageProducer: Publishing message with topic {Topic} and id {Id} and message: {Request}", message.Header.Topic, message.Id, message.Body); diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusMessageProducer.cs index 686f57ee5c..efbdb9b21b 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusMessageProducer.cs @@ -65,6 +65,9 @@ public abstract class AzureServiceBusMessageProducer : IAmAMessageProducerSync, /// public Activity? Span { get; set; } + /// + public IAmAMessageScheduler? Scheduler { get; set; } + /// /// An Azure Service Bus Message producer /// diff --git a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageProducer.cs index 65c8b89078..f7f6c8bfe8 100644 --- a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageProducer.cs @@ -27,6 +27,7 @@ THE SOFTWARE. */ using System.Threading.Tasks; using Confluent.Kafka; using Microsoft.Extensions.Logging; +using Paramore.Brighter.Tasks; namespace Paramore.Brighter.MessagingGateway.Kafka { @@ -48,6 +49,9 @@ public class KafkaMessageProducer : KafkaMessagingGateway, IAmAMessageProducerSy /// public Activity Span { get; set; } + /// + public IAmAMessageScheduler Scheduler { get; set; } + private IProducer _producer; private readonly IKafkaMessageHeaderBuilder _headerBuilder; private readonly ProducerConfig _producerConfig; @@ -122,7 +126,6 @@ public KafkaMessageProducer( /// /// Dispose of the producer /// - /// Are we disposing or being called by the GC public void Dispose() { Dispose(true); @@ -133,7 +136,6 @@ public void Dispose() /// /// Dispose of the producer /// - /// Are we disposing or being called by the GC public ValueTask DisposeAsync() { Dispose(true); @@ -192,12 +194,62 @@ public void Init() /// The Kafka client has entered an unrecoverable state public void Send(Message message) { + SendWithDelay(message, TimeSpan.Zero); + } + + /// + /// Sends the specified message. + /// + /// + /// Usage of the Kafka async producer is much slower than the sync producer. This is because the async producer + /// produces a single message and waits for the result before producing the next message. By contrast the synchronous + /// producer queues work and uses a dedicated thread to dispatch + /// + /// The message. + /// Allows cancellation of the in-flight send operation + public async Task SendAsync(Message message, CancellationToken cancellationToken = default) + { + await SendWithDelayAsync(message, TimeSpan.Zero, cancellationToken); + } + + /// + /// Sends the message with the given delay + /// + /// + /// No delay support implemented + /// + /// The message to send + /// The delay to use + public void SendWithDelay(Message message, TimeSpan? delay = null) + { + delay ??= TimeSpan.Zero; + if (delay != TimeSpan.Zero) + { + if (Scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(message, delay.Value); + return; + } + + if (Scheduler is IAmAMessageSchedulerAsync async) + { + BrighterAsyncContext.Run(async () => await async.ScheduleAsync(message, delay.Value)); + return; + } + + s_logger.LogWarning("KafkaMessageProducer: no scheduler configured, message will be sent immediately"); + } + if (message == null) + { throw new ArgumentNullException(nameof(message)); + } if (_hasFatalProducerError) + { throw new ChannelFailureException($"Producer is in unrecoverable state"); - + } + try { s_logger.LogDebug( @@ -206,9 +258,9 @@ public void Send(Message message) message.Header.Topic, message.Body.Value ); - + _publisher.PublishMessage(message, report => PublishResults(report.Status, report.Headers)); - + } catch (ProduceException pe) { @@ -227,7 +279,7 @@ public void Send(Message message) ioe.Message ); throw new ChannelFailureException("Error talking to the broker, see inner exception for details", ioe); - + } catch (ArgumentException ae) { @@ -237,106 +289,99 @@ public void Send(Message message) ae.Message ); throw new ChannelFailureException("Error talking to the broker, see inner exception for details", ae); - + } catch (KafkaException kafkaException) { - s_logger.LogError(kafkaException, $"KafkaMessageProducer: There was an error sending to topic {Topic})"); - + s_logger.LogError(kafkaException, "KafkaMessageProducer: There was an error sending to topic {Topic})", Topic); + if (kafkaException.Error.IsFatal) //this can't be recovered and requires a new producer throw; - + throw new ChannelFailureException("Error connecting to Kafka, see inner exception for details", kafkaException); } } /// - /// Sends the specified message. + /// Sends the message with the given delay /// /// - /// Usage of the Kafka async producer is much slower than the sync producer. This is because the async producer - /// produces a single message and waits for the result before producing the next message. By contrast the synchronous - /// producer queues work and uses a dedicated thread to dispatch + /// No delay support implemented /// - /// The message. - /// Allows cancellation of the in-flight send operation - public async Task SendAsync(Message message, CancellationToken cancellationToken = default) + /// The message to send + /// The delay to use + /// Cancels the send operation + public async Task SendWithDelayAsync(Message message, TimeSpan? delay, CancellationToken cancellationToken = default) { - if (message == null) - throw new ArgumentNullException(nameof(message)); - - if (_hasFatalProducerError) - throw new ChannelFailureException($"Producer is in unrecoverable state"); - try - { - s_logger.LogDebug( - "Sending message to Kafka. Servers {Servers} Topic: {Topic} Body: {Request}", - _producerConfig.BootstrapServers, - message.Header.Topic, - message.Body.Value - ); - - await _publisher.PublishMessageAsync(message, result => PublishResults(result.Status, result.Headers), cancellationToken); - - } - catch (ProduceException pe) - { - s_logger.LogError(pe, - "Error sending message to Kafka servers {Servers} because {ErrorMessage} ", - _producerConfig.BootstrapServers, - pe.Error.Reason - ); - throw new ChannelFailureException("Error talking to the broker, see inner exception for details", pe); - } - catch (InvalidOperationException ioe) - { - s_logger.LogError(ioe, - "Error sending message to Kafka servers {Servers} because {ErrorMessage} ", - _producerConfig.BootstrapServers, - ioe.Message - ); - throw new ChannelFailureException("Error talking to the broker, see inner exception for details", ioe); + delay ??= TimeSpan.Zero; + if (delay != TimeSpan.Zero) + { + if (Scheduler is IAmAMessageSchedulerAsync async) + { + await async.ScheduleAsync(message, delay.Value, cancellationToken); + return; + } + + if (Scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(message, delay.Value); + return; + } + + s_logger.LogWarning("KafkaMessageProducer: no scheduler configured, message will be sent immediately"); + } + + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } - } - catch (ArgumentException ae) - { + if (_hasFatalProducerError) + { + throw new ChannelFailureException("Producer is in unrecoverable state"); + } + + try + { + s_logger.LogDebug( + "Sending message to Kafka. Servers {Servers} Topic: {Topic} Body: {Request}", + _producerConfig.BootstrapServers, + message.Header.Topic, + message.Body.Value + ); + + await _publisher.PublishMessageAsync(message, result => PublishResults(result.Status, result.Headers), cancellationToken); + + } + catch (ProduceException pe) + { + s_logger.LogError(pe, + "Error sending message to Kafka servers {Servers} because {ErrorMessage} ", + _producerConfig.BootstrapServers, + pe.Error.Reason + ); + throw new ChannelFailureException("Error talking to the broker, see inner exception for details", pe); + } + catch (InvalidOperationException ioe) + { + s_logger.LogError(ioe, + "Error sending message to Kafka servers {Servers} because {ErrorMessage} ", + _producerConfig.BootstrapServers, + ioe.Message + ); + throw new ChannelFailureException("Error talking to the broker, see inner exception for details", ioe); + + } + catch (ArgumentException ae) + { s_logger.LogError(ae, "Error sending message to Kafka servers {Servers} because {ErrorMessage} ", _producerConfig.BootstrapServers, ae.Message ); throw new ChannelFailureException("Error talking to the broker, see inner exception for details", ae); - - } - } - - /// - /// Sends the message with the given delay - /// - /// - /// No delay support implemented - /// - /// The message to send - /// The delay to use - public void SendWithDelay(Message message, TimeSpan? delay = null) - { - //TODO: No delay support implemented - Send(message); - } - - /// - /// Sends the message with the given delay - /// - /// - /// No delay support implemented - /// - /// The message to send - /// The delay to use - /// Cancels the send operation - public async Task SendWithDelayAsync(Message message, TimeSpan? delay, CancellationToken cancellationToken = default) - { - //TODO: No delay support implemented - await SendAsync(message); + + } } private void Dispose(bool disposing) diff --git a/src/Paramore.Brighter.MessagingGateway.MQTT/MQTTMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.MQTT/MQTTMessageProducer.cs index f9ae1a9a79..acbf9472ca 100644 --- a/src/Paramore.Brighter.MessagingGateway.MQTT/MQTTMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.MQTT/MQTTMessageProducer.cs @@ -3,6 +3,9 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Logging; +using Paramore.Brighter.Tasks; namespace Paramore.Brighter.MessagingGateway.MQTT { @@ -13,13 +16,20 @@ namespace Paramore.Brighter.MessagingGateway.MQTT /// public class MQTTMessageProducer : IAmAMessageProducer, IAmAMessageProducerAsync, IAmAMessageProducerSync { + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + public int MaxOutStandingMessages { get; set; } = -1; public int MaxOutStandingCheckIntervalMilliSeconds { get; set; } = 0; public Dictionary OutBoxBag { get; set; } = new Dictionary(); + /// public Publication Publication { get; set; } + /// public Activity Span { get; set; } + + /// + public IAmAMessageScheduler Scheduler { get; set; } private MQTTMessagePublisher _mqttMessagePublisher; @@ -52,10 +62,7 @@ public ValueTask DisposeAsync() /// The message. public void Send(Message message) { - if (message == null) - throw new ArgumentNullException(nameof(message)); - - _mqttMessagePublisher.PublishMessage(message); + SendWithDelay(message, TimeSpan.Zero); } /// @@ -66,10 +73,7 @@ public void Send(Message message) /// Task. public async Task SendAsync(Message message, CancellationToken cancellationToken = default) { - if (message == null) - throw new ArgumentNullException(nameof(message)); - - await _mqttMessagePublisher.PublishMessageAsync(message, cancellationToken); + await SendWithDelayAsync(message, TimeSpan.Zero, cancellationToken); } /// @@ -79,8 +83,31 @@ public async Task SendAsync(Message message, CancellationToken cancellationToken /// Delay is not natively supported - don't block with Task.Delay public void SendWithDelay(Message message, TimeSpan? delay = null) { + delay ??= TimeSpan.Zero; + if (delay != TimeSpan.Zero) + { + if (Scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(message, delay.Value); + return; + } + + if (Scheduler is IAmAMessageSchedulerAsync async) + { + BrighterAsyncContext.Run(async () => await async.ScheduleAsync(message, delay.Value)); + return; + } + + s_logger.LogWarning("MQTTMessageProducer: no scheduler configured, message will be sent immediately"); + } + // delay is not natively supported - Send(message); + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + + _mqttMessagePublisher.PublishMessage(message); } /// @@ -91,10 +118,30 @@ public void SendWithDelay(Message message, TimeSpan? delay = null) /// Allows cancellation of the Send operation public async Task SendWithDelayAsync(Message message, TimeSpan? delay, CancellationToken cancellationToken = default) { - // delay is not natively supported - await SendAsync(message, cancellationToken); - } + delay ??= TimeSpan.Zero; + if (delay != TimeSpan.Zero) + { + if (Scheduler is IAmAMessageSchedulerAsync async) + { + await async.ScheduleAsync(message, delay.Value, cancellationToken); + return; + } - + if (Scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(message, delay.Value); + return; + } + + s_logger.LogWarning("MQTTMessageProducer: no scheduler configured, message will be sent immediately"); + } + + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + + await _mqttMessagePublisher.PublishMessageAsync(message, cancellationToken); + } } } diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs index 4382c57c9e..b251e1ae08 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2022 Ian Cooper @@ -19,6 +20,7 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #endregion using System; @@ -29,6 +31,7 @@ THE SOFTWARE. */ using Paramore.Brighter.Logging; using Paramore.Brighter.MessagingGateway.MsSql.SqlQueues; using Paramore.Brighter.MsSql; +using Paramore.Brighter.Tasks; namespace Paramore.Brighter.MessagingGateway.MsSql { @@ -50,6 +53,9 @@ public class MsSqlMessageProducer : IAmAMessageProducerSync, IAmAMessageProducer /// public Activity? Span { get; set; } + /// + public IAmAMessageScheduler? Scheduler { get; set; } + /// /// Initializes a new instance of the class. /// @@ -84,11 +90,7 @@ public MsSqlMessageProducer( /// The message to send. public void Send(Message message) { - var topic = message.Header.Topic; - - s_logger.LogDebug("MsSqlMessageProducer: send message with topic {Topic} and id {Id}", topic, message.Id); - - _sqlQ.Send(message, topic); + SendWithDelay(message, TimeSpan.Zero); } /// @@ -99,12 +101,7 @@ public void Send(Message message) /// A task that represents the asynchronous send operation. public async Task SendAsync(Message message, CancellationToken cancellationToken = default) { - var topic = message.Header.Topic; - - s_logger.LogDebug( - "MsSqlMessageProducer: send async message with topic {Topic} and id {Id}", topic, message.Id); - - await _sqlQ.SendAsync(message, topic, TimeSpan.Zero); + await SendWithDelayAsync(message, TimeSpan.Zero, cancellationToken); } /// @@ -114,9 +111,30 @@ public async Task SendAsync(Message message, CancellationToken cancellationToken /// The message to send. /// The delay to use. public void SendWithDelay(Message message, TimeSpan? delay = null) - { - // No delay support implemented - Send(message); + { + delay ??= TimeSpan.Zero; + if (delay != TimeSpan.Zero) + { + if (Scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(message, delay.Value); + return; + } + + if (Scheduler is IAmAMessageSchedulerAsync async) + { + BrighterAsyncContext.Run(async () => await async.ScheduleAsync(message, delay.Value)); + return; + } + + s_logger.LogWarning("MsSqlMessageProducer: no scheduler configured, message will be sent immediately"); + } + + var topic = message.Header.Topic; + + s_logger.LogDebug("MsSqlMessageProducer: send message with topic {Topic} and id {Id}", topic, message.Id); + + _sqlQ.Send(message, topic); } /// @@ -129,8 +147,30 @@ public void SendWithDelay(Message message, TimeSpan? delay = null) /// A task that represents the asynchronous send operation. public async Task SendWithDelayAsync(Message message, TimeSpan? delay, CancellationToken cancellationToken = default) { - // No delay support implemented - await SendAsync(message, cancellationToken); + delay ??= TimeSpan.Zero; + if (delay != TimeSpan.Zero) + { + if (Scheduler is IAmAMessageSchedulerAsync async) + { + await async.ScheduleAsync(message, delay.Value, cancellationToken); + return; + } + + if (Scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(message, delay.Value); + return; + } + + s_logger.LogWarning("MsSqlMessageProducer: no scheduler configured, message will be sent immediately"); + } + + var topic = message.Header.Topic; + + s_logger.LogDebug( + "MsSqlMessageProducer: send async message with topic {Topic} and id {Id}", topic, message.Id); + + await _sqlQ.SendAsync(message, topic, TimeSpan.Zero, cancellationToken); } /// diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageProducer.cs index 94ad46d056..95c5328344 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageProducer.cs @@ -67,6 +67,11 @@ public class RmqMessageProducer : RmqMessageGateway, IAmAMessageProducerSync, IA /// The OTel Span we are writing Producer events too /// public Activity? Span { get; set; } + + /// + /// The + /// + public IAmAMessageScheduler? Scheduler { get; set; } /// /// Initializes a new instance of the class. @@ -144,13 +149,21 @@ public async Task SendWithDelayAsync(Message message, TimeSpan? delay, Cancellat _pendingConfirmations.TryAdd(await Channel.GetNextPublishSequenceNumberAsync(cancellationToken), message.Id); - if (DelaySupported) + if (delay == TimeSpan.Zero || DelaySupported) { await rmqMessagePublisher.PublishMessageAsync(message, delay.Value, cancellationToken); } + else if(Scheduler is IAmAMessageSchedulerAsync asyncScheduler) + { + await asyncScheduler.ScheduleAsync(message, delay.Value, cancellationToken); + } + else if (Scheduler is IAmAMessageSchedulerSync scheduler) + { + scheduler.Schedule(message, delay.Value); + } else { - //TODO: Replace with a Timer, don't block + s_logger.LogWarning("No scheduler configured, going to ignore delay publish"); await rmqMessagePublisher.PublishMessageAsync(message, TimeSpan.Zero, cancellationToken); } diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageProducerFactory.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageProducerFactory.cs index dbbe564d05..60b21bb35a 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageProducerFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageProducerFactory.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2024 Dominic Hickie @@ -19,6 +20,7 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #endregion using System.Collections.Generic; @@ -37,14 +39,16 @@ public class RmqMessageProducerFactory( : IAmAMessageProducerFactory { /// - public Dictionary Create() + public Dictionary Create() { var producers = new Dictionary(); foreach (var publication in publications) { if (publication.Topic is null || RoutingKey.IsNullOrEmpty(publication.Topic)) + { throw new ConfigurationException($"A RabbitMQ publication must have a topic"); - + } + producers[publication.Topic] = new RmqMessageProducer(connection, publication); } diff --git a/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageProducer.cs index 55510f50b1..ee285d0d44 100644 --- a/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageProducer.cs @@ -31,6 +31,7 @@ THE SOFTWARE. */ using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; using Paramore.Brighter.Observability; +using Paramore.Brighter.Tasks; using ServiceStack.Redis; namespace Paramore.Brighter.MessagingGateway.Redis @@ -69,7 +70,11 @@ public class RedisMessageProducer( /// public Publication Publication { get { return _publication; } } + /// public Activity? Span { get; set; } + + /// + public IAmAMessageScheduler? Scheduler { get; set; } public void Dispose() { @@ -88,33 +93,7 @@ public async ValueTask DisposeAsync() /// /// The message. /// Task. - public void Send(Message message) - { - if (s_pool is null) - throw new ChannelFailureException("RedisMessageProducer: Connection pool has not been initialized"); - - using var client = s_pool.Value.GetClient(); - Topic = message.Header.Topic; - - s_logger.LogDebug("RedisMessageProducer: Preparing to send message"); - - var redisMessage = CreateRedisMessage(message); - - s_logger.LogDebug( - "RedisMessageProducer: Publishing message with topic {Topic} and id {Id} and body: {Request}", - message.Header.Topic, message.Id.ToString(), message.Body.Value - ); - //increment a counter to get the next message id - var nextMsgId = IncrementMessageCounter(client); - //store the message, against that id - StoreMessage(client, redisMessage, nextMsgId); - //If there are subscriber queues, push the message to the subscriber queues - var pushedTo = PushToQueues(client, nextMsgId); - s_logger.LogDebug( - "RedisMessageProducer: Published message with topic {Topic} and id {Id} and body: {Request} to queues: {3}", - message.Header.Topic, message.Id.ToString(), message.Body.Value, string.Join(", ", pushedTo) - ); - } + public void Send(Message message) => SendWithDelay(message, TimeSpan.Zero); /// /// Sends the specified message. @@ -123,11 +102,45 @@ public void Send(Message message) /// A token to cancel the send operation /// Task. public async Task SendAsync(Message message, CancellationToken cancellationToken = default) + { + await SendWithDelayAsync(message, TimeSpan.Zero, cancellationToken); + } + + /// + /// Sends the specified message. + /// + /// + /// No delay support on Redis + /// + /// The message. + /// The sending delay + /// Task. + public void SendWithDelay(Message message, TimeSpan? delay = null) { if (s_pool is null) + { throw new ChannelFailureException("RedisMessageProducer: Connection pool has not been initialized"); + } + + delay ??= TimeSpan.Zero; + if (delay != TimeSpan.Zero) + { + if (Scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(message, delay.Value); + return; + } - await using var client = await s_pool.Value.GetClientAsync(token: cancellationToken); + if (Scheduler is IAmAMessageSchedulerAsync async) + { + BrighterAsyncContext.Run(async () => await async.ScheduleAsync(message, delay.Value)); + return; + } + + s_logger.LogInformation("RedisMessageProducer: No scheduler configured, message will be sent immediately"); + } + + using var client = s_pool.Value.GetClient(); Topic = message.Header.Topic; s_logger.LogDebug("RedisMessageProducer: Preparing to send message"); @@ -139,31 +152,17 @@ public async Task SendAsync(Message message, CancellationToken cancellationToken message.Header.Topic, message.Id.ToString(), message.Body.Value ); //increment a counter to get the next message id - var nextMsgId = await IncrementMessageCounterAsync(client, cancellationToken); + var nextMsgId = IncrementMessageCounter(client); //store the message, against that id - await StoreMessageAsync(client, redisMessage, nextMsgId); + StoreMessage(client, redisMessage, nextMsgId); //If there are subscriber queues, push the message to the subscriber queues - var pushedTo = await PushToQueuesAsync(client, nextMsgId, cancellationToken); + var pushedTo = PushToQueues(client, nextMsgId); s_logger.LogDebug( "RedisMessageProducer: Published message with topic {Topic} and id {Id} and body: {Request} to queues: {3}", message.Header.Topic, message.Id.ToString(), message.Body.Value, string.Join(", ", pushedTo) ); } - - /// - /// Sends the specified message. - /// - /// - /// No delay support on Redis - /// - /// The message. - /// The sending delay - /// Task. - public void SendWithDelay(Message message, TimeSpan? delay = null) - { - Send(message); - } - + /// /// Sends the specified message. /// @@ -172,10 +171,54 @@ public void SendWithDelay(Message message, TimeSpan? delay = null) /// /// The message. /// The sending delay + /// /// Task. public async Task SendWithDelayAsync(Message message, TimeSpan? delay, CancellationToken cancellationToken = default) { - await SendAsync(message, cancellationToken); + if (s_pool is null) + { + throw new ChannelFailureException("RedisMessageProducer: Connection pool has not been initialized"); + } + + delay ??= TimeSpan.Zero; + if (delay != TimeSpan.Zero) + { + if (Scheduler is IAmAMessageSchedulerAsync async) + { + await async.ScheduleAsync(message, delay.Value, cancellationToken); + return; + } + + if (Scheduler is IAmAMessageSchedulerSync sync) + { + sync.Schedule(message, delay.Value); + return; + } + + s_logger.LogInformation("RedisMessageProducer: No scheduler configured, message will be sent immediately"); + } + + await using var client = await s_pool.Value.GetClientAsync(token: cancellationToken); + Topic = message.Header.Topic; + + s_logger.LogDebug("RedisMessageProducer: Preparing to send message"); + + var redisMessage = CreateRedisMessage(message); + + s_logger.LogDebug( + "RedisMessageProducer: Publishing message with topic {Topic} and id {Id} and body: {Request}", + message.Header.Topic, message.Id.ToString(), message.Body.Value + ); + //increment a counter to get the next message id + var nextMsgId = await IncrementMessageCounterAsync(client, cancellationToken); + //store the message, against that id + await StoreMessageAsync(client, redisMessage, nextMsgId); + //If there are subscriber queues, push the message to the subscriber queues + var pushedTo = await PushToQueuesAsync(client, nextMsgId, cancellationToken); + s_logger.LogDebug( + "RedisMessageProducer: Published message with topic {Topic} and id {Id} and body: {Request} to queues: {3}", + message.Header.Topic, message.Id.ToString(), message.Body.Value, string.Join(", ", pushedTo) + ); } private IEnumerable PushToQueues(IRedisClient client, long nextMsgId) diff --git a/src/Paramore.Brighter.ServiceActivator/ControlBus/ControlBusReceiverBuilder.cs b/src/Paramore.Brighter.ServiceActivator/ControlBus/ControlBusReceiverBuilder.cs index e757e16b7e..8d0528d39f 100644 --- a/src/Paramore.Brighter.ServiceActivator/ControlBus/ControlBusReceiverBuilder.cs +++ b/src/Paramore.Brighter.ServiceActivator/ControlBus/ControlBusReceiverBuilder.cs @@ -181,6 +181,7 @@ public Dispatcher Build(string hostName) .ExternalBus(ExternalBusType.FireAndForget, mediator) .ConfigureInstrumentation(null, InstrumentationOptions.None) .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(null) .Build(); // These are the control bus channels, we hardcode them because we want to know they exist, but we use diff --git a/src/Paramore.Brighter/CommandProcessor.cs b/src/Paramore.Brighter/CommandProcessor.cs index 22068c6e79..aa1f4f4aa5 100644 --- a/src/Paramore.Brighter/CommandProcessor.cs +++ b/src/Paramore.Brighter/CommandProcessor.cs @@ -37,7 +37,6 @@ THE SOFTWARE. */ using Paramore.Brighter.FeatureSwitch; using Paramore.Brighter.Logging; using Paramore.Brighter.Observability; -using Paramore.Brighter.Scheduler.Events; using Paramore.Brighter.Tasks; using Polly; using Polly.Registry; @@ -120,7 +119,7 @@ public class CommandProcessor : IAmACommandProcessor /// Do we want to insert an inbox handler into pipelines without the attribute. Null (default = no), yes = how to configure /// What is the tracer we will use for telemetry /// When creating a span for operations how noisy should the attributes be - /// TODO: ADD description + /// The . public CommandProcessor( IAmASubscriberRegistry subscriberRegistry, IAmAHandlerFactory handlerFactory, @@ -168,7 +167,7 @@ public CommandProcessor( /// If we are expecting a response, then we need a channel to listen on /// What is the tracer we will use for telemetry /// When creating a span for operations how noisy should the attributes be - /// TODO: ADD description + /// The . public CommandProcessor( IAmASubscriberRegistry subscriberRegistry, IAmAHandlerFactory handlerFactory, @@ -225,102 +224,6 @@ public CommandProcessor( InitExtServiceBus(mediator); } - /// - public void SchedulerPost(TimeSpan delay, TRequest request, RequestContext? requestContext = null) - where TRequest : class, IRequest - { - if (_messageSchedulerFactory == null) - { - throw new InvalidOperationException("No message scheduler factory set."); - } - - s_logger.LogInformation("Scheduling request: {RequestType} {Id}", request.GetType(), request.Id); - var scheduler = _messageSchedulerFactory.Create(this); - if (scheduler is IAmAMessageSchedulerSync sync) - { - sync.Schedule(delay, SchedulerFireType.Post, request); - } - else if (scheduler is IAmAMessageSchedulerAsync asyncScheduler) - { - BrighterAsyncContext.Run(async () => await asyncScheduler.ScheduleAsync(delay, SchedulerFireType.Post, request)); - } - } - - /// - public void SchedulerPost(DateTimeOffset at, - TRequest request, - RequestContext? requestContext = null) - where TRequest : class, IRequest - { - if (_messageSchedulerFactory == null) - { - throw new InvalidOperationException("No message scheduler factory set."); - } - - s_logger.LogInformation("Scheduling request: {RequestType} {Id}", request.GetType(), request.Id); - var scheduler = _messageSchedulerFactory.Create(this); - if (scheduler is IAmAMessageSchedulerSync sync) - { - sync.Schedule(at, SchedulerFireType.Post, request); - } - else if (scheduler is IAmAMessageSchedulerAsync asyncScheduler) - { - BrighterAsyncContext.Run(async () => await asyncScheduler.ScheduleAsync(at, SchedulerFireType.Post, request)); - } - } - - - /// - public async Task SchedulerAsync(TimeSpan delay, - TRequest request, - RequestContext? requestContext = null, - bool continueOnCapturedContext = true, - CancellationToken cancellationToken = default) - where TRequest : class, IRequest - { - if (_messageSchedulerFactory == null) - { - throw new InvalidOperationException("No message scheduler factory set."); - } - - s_logger.LogInformation("Scheduling request: {RequestType} {Id}", request.GetType(), request.Id); - var scheduler = _messageSchedulerFactory.Create(this); - if (scheduler is IAmAMessageSchedulerAsync asyncScheduler) - { - await asyncScheduler.ScheduleAsync(delay, SchedulerFireType.Post, request, cancellationToken); - } - else if (scheduler is IAmAMessageSchedulerSync sync) - { - sync.Schedule(delay, SchedulerFireType.Post, request); - } - } - - /// - public async Task SchedulerAsync(DateTimeOffset at, - TRequest request, - RequestContext? requestContext = null, - bool continueOnCapturedContext = true, - CancellationToken cancellationToken = default) - where TRequest : class, IRequest - { - if (_messageSchedulerFactory == null) - { - throw new InvalidOperationException("No message scheduler factory set."); - } - - s_logger.LogInformation("Scheduling request: {RequestType} {Id}", request.GetType(), request.Id); - var scheduler = _messageSchedulerFactory.Create(this); - if (scheduler is IAmAMessageSchedulerAsync asyncScheduler) - { - await asyncScheduler.ScheduleAsync(at, SchedulerFireType.Post, request, cancellationToken); - } - else if (scheduler is IAmAMessageSchedulerSync sync) - { - sync.Schedule(at, SchedulerFireType.Post, request); - } - } - - /// /// Sends the specified command. We expect only one handler. The command is handled synchronously. /// @@ -565,6 +468,166 @@ public async Task PublishAsync( } } + /// + public string SchedulerPost(TRequest request, + TimeSpan delay, + RequestContext? requestContext = null, + Dictionary? args = null) where TRequest : class, IRequest + { + if (_messageSchedulerFactory == null) + { + throw new InvalidOperationException("No message scheduler factory defined."); + } + + s_logger.LogInformation("Scheduling a request: {RequestType} {Id}", request.GetType(), request.Id); + + var span = _tracer?.CreateSpan(CommandProcessorSpanOperation.Scheduler, request, requestContext?.Span, options: _instrumentationOptions); + var context = InitRequestContext(span, requestContext); + + try + { + Message message = s_mediator!.CreateMessageFromRequest(request, context); + + var scheduler = _messageSchedulerFactory.Create(this); + return scheduler switch + { + IAmAMessageSchedulerSync sync => sync.Schedule(message, delay), + IAmAMessageSchedulerAsync async => BrighterAsyncContext.Run(async () => await async.ScheduleAsync(message, delay)), + _ => throw new InvalidOperationException("Message scheduler must be sync or async") + }; + } + catch (Exception e) + { + _tracer?.AddExceptionToSpan(span, [e]); + throw; + } + finally + { + _tracer?.EndSpan(span); + } + } + + /// + public string SchedulerPost(TRequest request, + DateTimeOffset at, + RequestContext? requestContext = null, + Dictionary? args = null) where TRequest : class, IRequest + { + if (_messageSchedulerFactory == null) + { + throw new InvalidOperationException("No message scheduler factory defined."); + } + + s_logger.LogInformation("Scheduling a request: {RequestType} {Id}", request.GetType(), request.Id); + + var span = _tracer?.CreateSpan(CommandProcessorSpanOperation.Scheduler, request, requestContext?.Span, options: _instrumentationOptions); + var context = InitRequestContext(span, requestContext); + + try + { + Message message = s_mediator!.CreateMessageFromRequest(request, context); + + var scheduler = _messageSchedulerFactory.Create(this); + return scheduler switch + { + IAmAMessageSchedulerSync sync => sync.Schedule(message, at), + IAmAMessageSchedulerAsync async => BrighterAsyncContext.Run(async () => await async.ScheduleAsync(message, at)), + _ => throw new InvalidOperationException("Message scheduler must be sync or async") + }; + } + catch (Exception e) + { + _tracer?.AddExceptionToSpan(span, [e]); + throw; + } + finally + { + _tracer?.EndSpan(span); + } + } + + /// + public async Task SchedulerPostAsync(TRequest request, + TimeSpan delay, + RequestContext? requestContext = null, + Dictionary? args = null, + bool continueOnCapturedContext = true, + CancellationToken cancellationToken = default) where TRequest : class, IRequest + { + if (_messageSchedulerFactory == null) + { + throw new InvalidOperationException("No message scheduler factory defined."); + } + + s_logger.LogInformation("Scheduling a request: {RequestType} {Id}", request.GetType(), request.Id); + + var span = _tracer?.CreateSpan(CommandProcessorSpanOperation.Scheduler, request, requestContext?.Span, options: _instrumentationOptions); + var context = InitRequestContext(span, requestContext); + + try + { + Message message = await s_mediator!.CreateMessageFromRequestAsync(request, context, cancellationToken); + + var scheduler = _messageSchedulerFactory.Create(this); + return scheduler switch + { + IAmAMessageSchedulerAsync async => await async.ScheduleAsync(message, delay, cancellationToken).ConfigureAwait(continueOnCapturedContext), + IAmAMessageSchedulerSync sync => sync.Schedule(message, delay), + _ => throw new InvalidOperationException("Message scheduler must be sync or async") + }; + } + catch (Exception e) + { + _tracer?.AddExceptionToSpan(span, [e]); + throw; + } + finally + { + _tracer?.EndSpan(span); + } + } + + public async Task SchedulerPostAsync(TRequest request, + DateTimeOffset at, + RequestContext? requestContext = null, + Dictionary? args = null, + bool continueOnCapturedContext = true, + CancellationToken cancellationToken = default) + where TRequest : class, IRequest + { + if (_messageSchedulerFactory == null) + { + throw new InvalidOperationException("No message scheduler factory defined."); + } + + s_logger.LogInformation("Scheduling a request: {RequestType} {Id}", request.GetType(), request.Id); + + var span = _tracer?.CreateSpan(CommandProcessorSpanOperation.Scheduler, request, requestContext?.Span, options: _instrumentationOptions); + var context = InitRequestContext(span, requestContext); + + try + { + Message message = await s_mediator!.CreateMessageFromRequestAsync(request, context, cancellationToken); + + var scheduler = _messageSchedulerFactory.Create(this); + return scheduler switch + { + IAmAMessageSchedulerAsync async => await async.ScheduleAsync(message, at, cancellationToken).ConfigureAwait(continueOnCapturedContext), + IAmAMessageSchedulerSync sync => sync.Schedule(message, at), + _ => throw new InvalidOperationException("Message scheduler must be sync or async") + }; + } + catch (Exception e) + { + _tracer?.AddExceptionToSpan(span, [e]); + throw; + } + finally + { + _tracer?.EndSpan(span); + } + } + /// /// Posts the specified request. The message is placed on a task queue and into a outbox for reposting in the event of failure. /// You will need to configure a service that reads from the task queue to process the message diff --git a/src/Paramore.Brighter/CommandProcessorBuilder.cs b/src/Paramore.Brighter/CommandProcessorBuilder.cs index 2c8c1adb53..5f3f3e08e7 100644 --- a/src/Paramore.Brighter/CommandProcessorBuilder.cs +++ b/src/Paramore.Brighter/CommandProcessorBuilder.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2014 Ian Cooper @@ -78,11 +79,11 @@ namespace Paramore.Brighter /// /// /// - public class CommandProcessorBuilder : INeedAHandlers, + public class CommandProcessorBuilder : INeedAHandlers, INeedPolicy, - INeedMessaging, - INeedInstrumentation, - INeedARequestContext, + INeedMessaging, + INeedInstrumentation, + INeedARequestContext, INeedAMessageSchedulerFactory, IAmACommandProcessorBuilder { @@ -149,11 +150,13 @@ public INeedAHandlers ConfigureFeatureSwitches(IAmAFeatureSwitchRegistry feature public INeedMessaging Policies(IPolicyRegistry policyRegistry) { if (!policyRegistry.ContainsKey(CommandProcessor.RETRYPOLICY)) - throw new ConfigurationException("The policy registry is missing the CommandProcessor.RETRYPOLICY policy which is required"); - + throw new ConfigurationException( + "The policy registry is missing the CommandProcessor.RETRYPOLICY policy which is required"); + if (!policyRegistry.ContainsKey(CommandProcessor.CIRCUITBREAKER)) - throw new ConfigurationException("The policy registry is missing the CommandProcessor.CIRCUITBREAKER policy which is required"); - + throw new ConfigurationException( + "The policy registry is missing the CommandProcessor.CIRCUITBREAKER policy which is required"); + _policyRegistry = policyRegistry; return this; } @@ -178,17 +181,19 @@ public INeedMessaging DefaultPolicy() /// A factory for channels used to handle RPC responses /// If we use a request reply queue how do we subscribe to replies /// What inbox do we use for request-reply + /// The message scheduler factory /// public INeedInstrumentation ExternalBus( - ExternalBusType busType, - IAmAnOutboxProducerMediator bus, - IAmAChannelFactory? responseChannelFactory = null, + ExternalBusType busType, + IAmAnOutboxProducerMediator bus, + IAmAChannelFactory? responseChannelFactory = null, IEnumerable? subscriptions = null, - InboxConfiguration? inboxConfiguration = null + InboxConfiguration? inboxConfiguration = null, + IAmAMessageSchedulerFactory? messageSchedulerFactory = null ) { _inboxConfiguration = inboxConfiguration; - + switch (busType) { case ExternalBusType.None: @@ -201,6 +206,7 @@ public INeedInstrumentation ExternalBus( _useRequestReplyQueues = true; _replySubscriptions = subscriptions; _responseChannelFactory = responseChannelFactory; + _messageSchedulerFactory = messageSchedulerFactory; break; default: throw new ConfigurationException("Bus type not supported"); @@ -208,7 +214,7 @@ public INeedInstrumentation ExternalBus( return this; } - + /// /// Use to indicate that you are not using Task Queues. /// @@ -229,13 +235,14 @@ public INeedInstrumentation NoExternalBus() /// What is the that we will use to instrument the Command Processor /// A that tells us how detailed the instrumentation should be /// - public INeedARequestContext ConfigureInstrumentation(IAmABrighterTracer? tracer, InstrumentationOptions instrumentationOptions) + public INeedARequestContext ConfigureInstrumentation(IAmABrighterTracer? tracer, + InstrumentationOptions instrumentationOptions) { - _tracer = tracer; - _instrumetationOptions = instrumentationOptions; - return this; + _tracer = tracer; + _instrumetationOptions = instrumentationOptions; + return this; } - + /// /// We do not intend to instrument the CommandProcessor /// @@ -252,13 +259,14 @@ public INeedARequestContext NoInstrumentation() /// /// The request context factory. /// IAmACommandProcessorBuilder. - public IAmACommandProcessorBuilder RequestContextFactory(IAmARequestContextFactory requestContextFactory) + public INeedAMessageSchedulerFactory RequestContextFactory(IAmARequestContextFactory requestContextFactory) { _requestContextFactory = requestContextFactory; return this; } - - public IAmACommandProcessorBuilder MessageSchedulerFactory(IAmAMessageSchedulerFactory messageSchedulerFactory) + + /// + public IAmACommandProcessorBuilder MessageSchedulerFactory(IAmAMessageSchedulerFactory? messageSchedulerFactory) { _messageSchedulerFactory = messageSchedulerFactory; return this; @@ -270,51 +278,52 @@ public IAmACommandProcessorBuilder MessageSchedulerFactory(IAmAMessageSchedulerF /// CommandProcessor. public CommandProcessor Build() { - if(_registry == null) + if (_registry == null) throw new ConfigurationException( "A SubscriberRegistry must be provided to construct a command processor"); - if(_handlerFactory == null) + if (_handlerFactory == null) throw new ConfigurationException( "A HandlerFactory must be provided to construct a command processor"); - if(_requestContextFactory == null) + if (_requestContextFactory == null) throw new ConfigurationException( "A RequestContextFactory must be provided to construct a command processor"); - if(_policyRegistry == null) + if (_policyRegistry == null) throw new ConfigurationException( "A PolicyRegistry must be provided to construct a command processor"); - if(_instrumetationOptions == null) + if (_instrumetationOptions == null) throw new ConfigurationException( "InstrumentationOptions must be provided to construct a command processor"); - - if (_bus == null) + + if (_bus == null) { - return new CommandProcessor(subscriberRegistry: _registry, handlerFactory: _handlerFactory, + return new CommandProcessor(subscriberRegistry: _registry, handlerFactory: _handlerFactory, requestContextFactory: _requestContextFactory, policyRegistry: _policyRegistry, - featureSwitchRegistry: _featureSwitchRegistry, instrumentationOptions: _instrumetationOptions.Value); + featureSwitchRegistry: _featureSwitchRegistry, + instrumentationOptions: _instrumetationOptions.Value); } - + if (!_useRequestReplyQueues) return new CommandProcessor( - subscriberRegistry: _registry, + subscriberRegistry: _registry, handlerFactory: _handlerFactory, - requestContextFactory: _requestContextFactory, + requestContextFactory: _requestContextFactory, policyRegistry: _policyRegistry, bus: _bus, - featureSwitchRegistry: _featureSwitchRegistry, + featureSwitchRegistry: _featureSwitchRegistry, inboxConfiguration: _inboxConfiguration, tracer: _tracer, instrumentationOptions: _instrumetationOptions.Value, messageSchedulerFactory: _messageSchedulerFactory ); - + if (_useRequestReplyQueues) return new CommandProcessor( - subscriberRegistry: _registry, + subscriberRegistry: _registry, handlerFactory: _handlerFactory, - requestContextFactory: _requestContextFactory, + requestContextFactory: _requestContextFactory, policyRegistry: _policyRegistry, bus: _bus, - featureSwitchRegistry: _featureSwitchRegistry, + featureSwitchRegistry: _featureSwitchRegistry, inboxConfiguration: _inboxConfiguration, replySubscriptions: _replySubscriptions, responseChannelFactory: _responseChannelFactory, @@ -325,12 +334,11 @@ public CommandProcessor Build() throw new ConfigurationException( "The configuration options chosen cannot be used to construct a command processor"); - } - - + } } #region Progressive interfaces + /// /// Interface INeedAHandlers /// @@ -342,7 +350,7 @@ public interface INeedAHandlers /// The registry. /// INeedPolicy. INeedPolicy Handlers(HandlerConfiguration theRegistry); - + /// /// Configure Feature Switches for the Handlers /// @@ -362,6 +370,7 @@ public interface INeedPolicy /// The policy registry. /// INeedLogging. INeedMessaging Policies(IPolicyRegistry policyRegistry); + /// /// Knows the policy. /// @@ -369,7 +378,7 @@ public interface INeedPolicy INeedMessaging DefaultPolicy(); } - + /// /// Interface INeedMessaging /// Note that a single command builder does not support both task queues and rpc, using the builder @@ -386,14 +395,16 @@ public interface INeedMessaging /// If using RPC the factory for reply channels /// If using RPC, any reply subscriptions /// What is the inbox configuration + /// The message scheduler factory. /// INeedInstrumentation ExternalBus( - ExternalBusType busType, - IAmAnOutboxProducerMediator bus, - IAmAChannelFactory? responseChannelFactory = null, + ExternalBusType busType, + IAmAnOutboxProducerMediator bus, + IAmAChannelFactory? responseChannelFactory = null, IEnumerable? subscriptions = null, - InboxConfiguration? inboxConfiguration = null - ); + InboxConfiguration? inboxConfiguration = null, + IAmAMessageSchedulerFactory? messageSchedulerFactory = null + ); /// /// We don't send messages out of process @@ -416,8 +427,9 @@ public interface INeedInstrumentation /// InstrumentationOptions.All - all of the above /// /// INeedARequestContext - INeedARequestContext ConfigureInstrumentation(IAmABrighterTracer? tracer, InstrumentationOptions instrumentationOptions); - + INeedARequestContext ConfigureInstrumentation(IAmABrighterTracer? tracer, + InstrumentationOptions instrumentationOptions); + /// /// We don't need instrumentation of the CommandProcessor /// @@ -425,6 +437,7 @@ public interface INeedInstrumentation INeedARequestContext NoInstrumentation(); } + /// /// Interface INeedARequestContext /// @@ -435,28 +448,31 @@ public interface INeedARequestContext /// /// The request context factory. /// IAmACommandProcessorBuilder. - IAmACommandProcessorBuilder RequestContextFactory(IAmARequestContextFactory requestContextFactory); + INeedAMessageSchedulerFactory RequestContextFactory(IAmARequestContextFactory requestContextFactory); } - // TODO Add doc public interface INeedAMessageSchedulerFactory { - + /// + /// The . + /// + /// + /// + IAmACommandProcessorBuilder MessageSchedulerFactory(IAmAMessageSchedulerFactory? messageSchedulerFactory); } - + + /// /// Interface IAmACommandProcessorBuilder /// public interface IAmACommandProcessorBuilder { - IAmACommandProcessorBuilder MessageSchedulerFactory(IAmAMessageSchedulerFactory messageSchedulerFactory); - /// /// Builds this instance. /// /// CommandProcessor. CommandProcessor Build(); } - + #endregion } diff --git a/src/Paramore.Brighter/ControlBusSenderFactory.cs b/src/Paramore.Brighter/ControlBusSenderFactory.cs index 20d9317f29..d930e0fad4 100644 --- a/src/Paramore.Brighter/ControlBusSenderFactory.cs +++ b/src/Paramore.Brighter/ControlBusSenderFactory.cs @@ -40,8 +40,14 @@ public class ControlBusSenderFactory : IAmAControlBusSenderFactory /// Creates the specified configuration. /// /// The outbox for outgoing messages to the control bus + /// + /// + /// /// IAmAControlBusSender. - public IAmAControlBusSender Create(IAmAnOutbox outbox, IAmAProducerRegistry producerRegistry, BrighterTracer tracer) + public IAmAControlBusSender Create(IAmAnOutbox outbox, + IAmAProducerRegistry producerRegistry, + BrighterTracer tracer, + IAmAMessageSchedulerFactory? messageSchedulerFactory = null) where T : Message { var mapper = new MessageMapperRegistry( @@ -65,6 +71,7 @@ public IAmAControlBusSender Create(IAmAnOutbox outbox, IAmAProd .ExternalBus(ExternalBusType.FireAndForget, mediator) .ConfigureInstrumentation(null, InstrumentationOptions.None) .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(messageSchedulerFactory) .Build() ); } diff --git a/src/Paramore.Brighter/ExternalBusConfiguration.cs b/src/Paramore.Brighter/ExternalBusConfiguration.cs index d46741b972..69a04afae9 100644 --- a/src/Paramore.Brighter/ExternalBusConfiguration.cs +++ b/src/Paramore.Brighter/ExternalBusConfiguration.cs @@ -118,6 +118,11 @@ public interface IAmExternalBusConfiguration /// IAmARequestContextFactory? RequestContextFactory { get; set; } + /// + /// The Message Scheduler Factory. + /// + IAmAMessageSchedulerFactory? MessageSchedulerFactory { get; set; } + /// /// The transaction provider for the outbox /// @@ -239,6 +244,11 @@ public class ExternalBusConfiguration : IAmExternalBusConfiguration /// public IAmAMessageTransformerFactory? TransformerFactory { get; set; } + /// + /// The Message Scheduler Factory. + /// + public IAmAMessageSchedulerFactory? MessageSchedulerFactory { get; set; } + /// /// The transaction provider for the outbox /// NOTE: Must implement IAmABoxTransactionProvider< > diff --git a/src/Paramore.Brighter/IAmACommandProcessor.cs b/src/Paramore.Brighter/IAmACommandProcessor.cs index 9212adc7f0..56fd31ea3b 100644 --- a/src/Paramore.Brighter/IAmACommandProcessor.cs +++ b/src/Paramore.Brighter/IAmACommandProcessor.cs @@ -39,54 +39,12 @@ namespace Paramore.Brighter /// public interface IAmACommandProcessor { - /// - /// Sends the specified command. - /// - /// - /// The amount of delay to be used before send the message. - /// The command. - /// The context of the request; if null we will start one via a - void SchedulerPost(TimeSpan delay, TRequest request, RequestContext? requestContext = null) where TRequest : class, IRequest; - - /// - /// Sends the specified command. - /// - /// - /// The that the message should be published. - /// The command. - /// The context of the request; if null we will start one via a - void SchedulerPost(DateTimeOffset at, TRequest request, RequestContext? requestContext = null) where TRequest : class, IRequest; - - /// - /// Awaitably sends the specified command. - /// - /// - /// The amount of delay to be used before send the message. - /// The command. - /// The context of the request; if null we will start one via a - /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false - /// Allows the sender to cancel the request pipeline. Optional - /// awaitable . - Task SchedulerAsync(TimeSpan delay, TRequest request, RequestContext? requestContext = null, bool continueOnCapturedContext = true, CancellationToken cancellationToken = default) where TRequest : class, IRequest; - - /// - /// Awaitably sends the specified command. - /// - /// - /// The that the message should be published. - /// The command. - /// The context of the request; if null we will start one via a - /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false - /// Allows the sender to cancel the request pipeline. Optional - /// awaitable . - Task SchedulerAsync(DateTimeOffset at, TRequest request, RequestContext? requestContext = null, bool continueOnCapturedContext = true, CancellationToken cancellationToken = default) where TRequest : class, IRequest; - /// /// Sends the specified command. /// /// /// The command. - /// The context of the request; if null we will start one via a + /// The context of the request; if null we will start one via a void Send(TRequest command, RequestContext? requestContext = null) where TRequest : class, IRequest; /// @@ -94,7 +52,8 @@ public interface IAmACommandProcessor /// /// /// The command. - /// The context of the request; if null we will start one via a /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false + /// The context of the request; if null we will start one via a + /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// Allows the sender to cancel the request pipeline. Optional /// awaitable . Task SendAsync(TRequest command, RequestContext? requestContext = null, bool continueOnCapturedContext = true, CancellationToken cancellationToken = default) where TRequest : class, IRequest; @@ -103,7 +62,8 @@ public interface IAmACommandProcessor /// Publishes the specified event. Throws an aggregate exception on failure of a pipeline but executes remaining /// /// - /// The context of the request; if null we will start one via a /// The event. + /// The context of the request; if null we will start one via a + /// The event. void Publish(TRequest @event, RequestContext? requestContext = null) where TRequest : class, IRequest; /// @@ -111,7 +71,7 @@ public interface IAmACommandProcessor /// /// /// The event. - /// The context of the request; if null we will start one via a + /// The context of the request; if null we will start one via a /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// Allows the sender to cancel the request pipeline. Optional /// awaitable . @@ -127,7 +87,67 @@ Task PublishAsync( /// /// The type of the request /// The request. - /// The context of the request; if null we will start one via a + /// The amount of delay before post the message. + /// The context of the request; if null we will start one via a + /// For transports or outboxes that require additional parameters such as topic, provide an optional arg + string SchedulerPost(TRequest request, TimeSpan delay, RequestContext? requestContext= null, Dictionary? args = null) where TRequest : class, IRequest; + + /// + /// Posts the specified request. + /// + /// The type of the request + /// The request. + /// The date-time the message should be post. + /// The context of the request; if null we will start one via a + /// For transports or outboxes that require additional parameters such as topic, provide an optional arg + string SchedulerPost(TRequest request, DateTimeOffset at, RequestContext? requestContext= null, Dictionary? args = null) where TRequest : class, IRequest; + + /// + /// Posts the specified request with async/await support. + /// + /// The type of the request + /// The request. + /// The amount of delay before post the message. + /// The context of the request; if null we will start one via a + /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false + /// For transports or outboxes that require additional parameters such as topic, provide an optional arg + /// Allows the sender to cancel the request pipeline. Optional + /// awaitable . + Task SchedulerPostAsync( + TRequest request, + TimeSpan delay, + RequestContext? requestContext = null, + Dictionary? args = null, + bool continueOnCapturedContext = true, + CancellationToken cancellationToken = default + ) where TRequest : class, IRequest; + + /// + /// Posts the specified request with async/await support. + /// + /// The type of the request + /// The request. + /// The date-time the message should be post. + /// The context of the request; if null we will start one via a + /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false + /// For transports or outboxes that require additional parameters such as topic, provide an optional arg + /// Allows the sender to cancel the request pipeline. Optional + /// awaitable . + Task SchedulerPostAsync( + TRequest request, + DateTimeOffset at, + RequestContext? requestContext = null, + Dictionary? args = null, + bool continueOnCapturedContext = true, + CancellationToken cancellationToken = default + ) where TRequest : class, IRequest; + + /// + /// Posts the specified request. + /// + /// The type of the request + /// The request. + /// The context of the request; if null we will start one via a /// For transports or outboxes that require additional parameters such as topic, provide an optional arg void Post(TRequest request, RequestContext? requestContext= null, Dictionary? args = null) where TRequest : class, IRequest; @@ -136,7 +156,7 @@ Task PublishAsync( /// /// The type of the request /// The request. - /// The context of the request; if null we will start one via a + /// The context of the request; if null we will start one via a /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// For transports or outboxes that require additional parameters such as topic, provide an optional arg /// Allows the sender to cancel the request pipeline. Optional @@ -157,7 +177,7 @@ Task PostAsync( /// Pass deposited message to /// /// The request to save to the outbox - /// The context of the request; if null we will start one via a + /// The context of the request; if null we will start one via a /// For transports or outboxes that require additional parameters such as topic, provide an optional arg /// The type of the request /// @@ -172,7 +192,7 @@ Task PostAsync( /// /// The request to save to the outbox /// If using an Outbox, the transaction provider for the Outbox - /// The context of the request; if null we will start one via a + /// The context of the request; if null we will start one via a /// For transports or outboxes that require additional parameters such as topic, provide an optional arg /// The id of the deposit batch, if this isn't set items will be added to the outbox as they come in and not as a batch /// The type of the request @@ -194,7 +214,7 @@ string DepositPost( /// Pass deposited message to /// /// The requests to save to the outbox - /// The context of the request; if null we will start one via a + /// The context of the request; if null we will start one via a /// For transports or outboxes that require additional parameters such as topic, provide an optional arg /// The type of the request /// The Id of the Message that has been deposited. @@ -205,11 +225,11 @@ string DepositPost( /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ normally you include the /// call to DepositPostBox within the scope of the transaction to write corresponding entity state to your /// database, that you want to signal via the request to downstream consumers - /// Pass deposited message to + /// Pass deposited message to /// /// The requests to save to the outbox /// If using an Outbox, the transaction provider for the Outbox - /// The context of the request; if null we will start one via a + /// The context of the request; if null we will start one via a /// For transports or outboxes that require additional parameters such as topic, provide an optional arg /// The type of the request /// The type of transaction used by the outbox @@ -229,7 +249,7 @@ string[] DepositPost( /// Pass deposited message to /// /// The request to save to the outbox - /// The context of the request; if null we will start one via a + /// The context of the request; if null we will start one via a /// For outboxes that require additional parameters such as topic, provide an optional arg /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// The Cancellation Token. @@ -253,7 +273,7 @@ Task DepositPostAsync( /// /// The request to save to the outbox /// If using an Outbox, the transaction provider for the Outbox - /// The context of the request; if null we will start one via a + /// The context of the request; if null we will start one via a /// For transports or outboxes that require additional parameters such as topic, provide an optional arg /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// The Cancellation Token. @@ -279,12 +299,11 @@ Task DepositPostAsync( /// Pass deposited message to /// /// The requests to save to the outbox - /// The context of the request; if null we will start one via a + /// The context of the request; if null we will start one via a /// For transports or outboxes that require additional parameters such as topic, provide an optional arg /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// The Cancellation Token. /// The type of the request - /// The type of transaction used by the outbox /// Task DepositPostAsync( IEnumerable requests, @@ -303,7 +322,7 @@ Task DepositPostAsync( /// /// The requests to save to the outbox /// If using an Outbox, the transaction provider for the Outbox - /// The context of the request; if null we will start one via a + /// The context of the request; if null we will start one via a /// For transports or outboxes that require additional parameters such as topic, provide an optional arg /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// The Cancellation Token. @@ -321,19 +340,19 @@ Task DepositPostAsync( /// /// Flushes the message box message given by to the broker. - /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ + /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ /// /// The ids to flush - /// The context of the request; if null we will start one via a + /// The context of the request; if null we will start one via a /// For transports or outboxes that require additional parameters such as topic, provide an optional arg void ClearOutbox(string[] ids, RequestContext? requestContext = null, Dictionary? args = null); /// /// Flushes the message box message given by to the broker. - /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ + /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ /// /// The ids to flush - /// The context of the request; if null we will start one via a + /// The context of the request; if null we will start one via a /// For transports or outboxes that require additional parameters such as topic, provide an optional arg /// /// @@ -353,7 +372,7 @@ Task ClearOutboxAsync( /// Because the operation blocks, there is a mandatory timeout /// /// What message do we want a reply to - /// The context of the request; if null we will start one via a + /// The context of the request; if null we will start one via a /// The call blocks, so we must time out; defaults to 500 ms if null /// TResponse? Call(T request, RequestContext? requestContext = null, TimeSpan? timeOut = null) diff --git a/src/Paramore.Brighter/IAmAControlBusSenderFactory.cs b/src/Paramore.Brighter/IAmAControlBusSenderFactory.cs index e11cadffbe..9384fc8693 100644 --- a/src/Paramore.Brighter/IAmAControlBusSenderFactory.cs +++ b/src/Paramore.Brighter/IAmAControlBusSenderFactory.cs @@ -40,11 +40,13 @@ public interface IAmAControlBusSenderFactory { /// The outbox to record outbound messages on the control bus /// The list of producers to send with /// The tracer lets us create open telemetry information + /// The message scheduler factory. /// IAmAControlBusSender. IAmAControlBusSender Create( IAmAnOutbox outbox, IAmAProducerRegistry producerRegistry, - BrighterTracer tracer + BrighterTracer tracer, + IAmAMessageSchedulerFactory? messageSchedulerFactory = null ) where T: Message; } diff --git a/src/Paramore.Brighter/IAmAMessageProducer.cs b/src/Paramore.Brighter/IAmAMessageProducer.cs index 15a3b30204..7cd50abf0e 100644 --- a/src/Paramore.Brighter/IAmAMessageProducer.cs +++ b/src/Paramore.Brighter/IAmAMessageProducer.cs @@ -39,5 +39,10 @@ public interface IAmAMessageProducer /// Allows us to set a to let a Producer participate in our telemetry /// Activity? Span { get; set; } + + /// + /// The external message scheduler + /// + IAmAMessageScheduler? Scheduler { get; set; } } } diff --git a/src/Paramore.Brighter/IAmAMessageProducerAsync.cs b/src/Paramore.Brighter/IAmAMessageProducerAsync.cs index 9b5e16125e..e4bdeb2692 100644 --- a/src/Paramore.Brighter/IAmAMessageProducerAsync.cs +++ b/src/Paramore.Brighter/IAmAMessageProducerAsync.cs @@ -45,6 +45,7 @@ public interface IAmAMessageProducerAsync : IAmAMessageProducer, IAsyncDisposabl /// Sends the specified message. /// /// The message. + /// A cancellation token to end the operation Task SendAsync(Message message, CancellationToken cancellationToken = default); /// diff --git a/src/Paramore.Brighter/IAmAMessageSchedulerAsync.cs b/src/Paramore.Brighter/IAmAMessageSchedulerAsync.cs index 80751b0068..40cd4a06c5 100644 --- a/src/Paramore.Brighter/IAmAMessageSchedulerAsync.cs +++ b/src/Paramore.Brighter/IAmAMessageSchedulerAsync.cs @@ -1,15 +1,55 @@ using System; using System.Threading; using System.Threading.Tasks; -using Paramore.Brighter.Scheduler.Events; namespace Paramore.Brighter; -public interface IAmAMessageSchedulerAsync : IAmAMessageScheduler, IDisposable +/// +/// The async API for message scheduler (like in-memory, Hang fire and others) +/// +public interface IAmAMessageSchedulerAsync : IAmAMessageScheduler, IAsyncDisposable { - Task ScheduleAsync(DateTimeOffset at, SchedulerFireType fireType, TRequest request, CancellationToken cancellationToken = default) - where TRequest : class, IRequest; - Task ScheduleAsync(TimeSpan delay, SchedulerFireType fireType, TRequest request, CancellationToken cancellationToken = default) - where TRequest : class, IRequest; - Task CancelSchedulerAsync(string id, CancellationToken cancellationToken = default); + /// + /// Scheduler a message to be executed the provided . + /// + /// The . + /// The date-time of when a message should be placed. + /// A cancellation token to end the operation + /// The scheduler id. + Task ScheduleAsync(Message message, DateTimeOffset at, CancellationToken cancellationToken = default); + + /// + /// Scheduler a message to be executed the provided . + /// + /// The . + /// The of delay before place the message. + /// A cancellation token to end the operation + /// The scheduler id. + Task ScheduleAsync(Message message, TimeSpan delay, CancellationToken cancellationToken = default); + + /// + /// ReScheduler a message. + /// + /// The scheduler id. + /// The date-time of when a message should be placed. + /// A cancellation token to end the operation + /// true if it could be re-scheduler, otherwise false. + Task ReSchedulerAsync(string schedulerId, DateTimeOffset at, CancellationToken cancellationToken = default); + + /// + /// ReScheduler a message. + /// + /// The scheduler id. + /// The of delay before place the message. + /// A cancellation token to end the operation + /// true if it could be re-scheduler, otherwise false. + Task ReSchedulerAsync(string schedulerId, TimeSpan delay, CancellationToken cancellationToken = default); + + /// + /// Cancel the scheduler message + /// + /// The scheduler id. + /// A cancellation token to end the operation + /// + Task CancelAsync(string id, CancellationToken cancellationToken = default); } diff --git a/src/Paramore.Brighter/IAmAMessageSchedulerFactory.cs b/src/Paramore.Brighter/IAmAMessageSchedulerFactory.cs index beaf2b1567..a30ecedf52 100644 --- a/src/Paramore.Brighter/IAmAMessageSchedulerFactory.cs +++ b/src/Paramore.Brighter/IAmAMessageSchedulerFactory.cs @@ -1,6 +1,14 @@ namespace Paramore.Brighter; +/// +/// The factory. +/// public interface IAmAMessageSchedulerFactory { + /// + /// Get or create a new instance of + /// + /// + /// The . IAmAMessageScheduler Create(IAmACommandProcessor processor); } diff --git a/src/Paramore.Brighter/IAmAMessageSchedulerSync.cs b/src/Paramore.Brighter/IAmAMessageSchedulerSync.cs index 0deac43136..44d14dc3be 100644 --- a/src/Paramore.Brighter/IAmAMessageSchedulerSync.cs +++ b/src/Paramore.Brighter/IAmAMessageSchedulerSync.cs @@ -1,13 +1,47 @@ using System; -using Paramore.Brighter.Scheduler.Events; namespace Paramore.Brighter; +/// +/// The API for message scheduler (like in-memory, Hang fire and others) +/// public interface IAmAMessageSchedulerSync : IAmAMessageScheduler, IDisposable { - string Schedule(DateTimeOffset at, SchedulerFireType fireType, TRequest request) - where TRequest : class, IRequest; - string Schedule(TimeSpan delay, SchedulerFireType fireType, TRequest request) - where TRequest : class, IRequest; - void CancelScheduler(string id); + /// + /// Scheduler a message to be executed the provided . + /// + /// The . + /// The date-time of when a message should be placed. + /// The scheduler id. + string Schedule(Message message, DateTimeOffset at); + + /// + /// Scheduler a message to be executed the provided . + /// + /// The . + /// The of delay before place the message. + /// The scheduler id. + string Schedule(Message message, TimeSpan delay); + + /// + /// ReScheduler a message. + /// + /// The scheduler id. + /// The date-time of when a message should be placed. + /// true if it could be re-scheduler, otherwise false. + bool ReScheduler(string schedulerId, DateTimeOffset at); + + /// + /// ReScheduler a message. + /// + /// The scheduler id. + /// The of delay before place the message. + /// true if it could be re-scheduler, otherwise false. + bool ReScheduler(string schedulerId, TimeSpan delay); + + /// + /// Cancel the scheduler message + /// + /// The scheduler id. + void Cancel(string id); } diff --git a/src/Paramore.Brighter/InMemoryMessageScheduler.cs b/src/Paramore.Brighter/InMemoryMessageScheduler.cs index 910e180b21..78e63718fe 100644 --- a/src/Paramore.Brighter/InMemoryMessageScheduler.cs +++ b/src/Paramore.Brighter/InMemoryMessageScheduler.cs @@ -1,133 +1,114 @@ using System; -using System.Collections.Generic; -using System.Text.Json; +using System.Collections.Concurrent; using System.Threading; +using System.Threading.Tasks; using Paramore.Brighter.Scheduler.Events; using Paramore.Brighter.Tasks; namespace Paramore.Brighter; -public class InMemoryMessageScheduler : IAmAMessageSchedulerSync +public class InMemoryMessageScheduler(IAmACommandProcessor processor, TimeProvider timeProvider) + : IAmAMessageSchedulerSync, IAmAMessageSchedulerAsync { - private readonly SchedulerMessageCollection _messages = new(); - private readonly IAmACommandProcessor _processor; + private readonly ConcurrentDictionary _timers = new(); - private readonly Timer _timer; + /// + public string Schedule(Message message, DateTimeOffset at) + => Schedule(message, at - DateTimeOffset.UtcNow); - public InMemoryMessageScheduler(IAmACommandProcessor processor, - TimeSpan initialDelay, - TimeSpan period) + /// + public string Schedule(Message message, TimeSpan delay) { - _processor = processor; - _timer = new Timer(Consume, this, initialDelay, period); + var id = Guid.NewGuid().ToString(); + _timers[id] = timeProvider.CreateTimer(_ => Execute(id, message), null, delay, TimeSpan.Zero); + return id; } - private static void Consume(object? state) + /// + public bool ReScheduler(string schedulerId, DateTimeOffset at) { - var scheduler = (InMemoryMessageScheduler)state!; - - var now = DateTimeOffset.UtcNow; - var schedulerMessage = scheduler._messages.Next(now); - while (schedulerMessage != null) + if (_timers.TryGetValue(schedulerId, out var timer)) { - var tmp = schedulerMessage; - BrighterAsyncContext.Run(async () => await scheduler._processor.SendAsync(new SchedulerMessageFired(tmp.Id) - { - FireType = tmp.FireType, - MessageType = tmp.MessageType, - MessageData = tmp.MessageData, - UseAsync = tmp.UseAsync - })); - - // TODO Add log - schedulerMessage = scheduler._messages.Next(now); + timer.Change(at - DateTimeOffset.UtcNow, TimeSpan.Zero); + return true; } + + return false; } - public string Schedule(DateTimeOffset at, SchedulerFireType fireType, TRequest request) - where TRequest : class, IRequest + /// + public bool ReScheduler(string schedulerId, TimeSpan delay) + => ReScheduler(schedulerId, DateTimeOffset.UtcNow.Add(delay)); + + /// + public void Cancel(string id) { - var id = Guid.NewGuid().ToString(); - _messages.Add(new SchedulerMessage(id, at, fireType, false, - typeof(TRequest).FullName!, - JsonSerializer.Serialize(request, JsonSerialisationOptions.Options))); - return id; + if (_timers.TryRemove(id, out var timer)) + { + timer.Dispose(); + } } - public string Schedule(TimeSpan delay, SchedulerFireType fireType, TRequest request) - where TRequest : class, IRequest - => Schedule(DateTimeOffset.UtcNow.Add(delay), fireType, request); + /// + public Task ScheduleAsync(Message message, DateTimeOffset at, CancellationToken cancellationToken = default) + => Task.FromResult(Schedule(message, at)); - public void CancelScheduler(string id) - => _messages.Delete(id); + /// + public Task ScheduleAsync(Message message, TimeSpan delay, CancellationToken cancellationToken = default) + => Task.FromResult(Schedule(message, delay)); - public void Dispose() => _timer.Dispose(); + /// + public Task ReSchedulerAsync(string schedulerId, DateTimeOffset at, + CancellationToken cancellationToken = default) + => Task.FromResult(ReScheduler(schedulerId, at)); - private record SchedulerMessage( - string Id, - DateTimeOffset At, - SchedulerFireType FireType, - bool UseAsync, - string MessageType, - string MessageData); + /// + public Task ReSchedulerAsync(string schedulerId, TimeSpan delay, + CancellationToken cancellationToken = default) + => Task.FromResult(ReScheduler(schedulerId, delay)); - private class SchedulerMessageCollection + /// + public async Task CancelAsync(string id, CancellationToken cancellationToken = default) { - // It's a sorted list - private readonly object _lock = new(); - private readonly LinkedList _messages = new(); + if (_timers.TryRemove(id, out var timer)) + { + await timer.DisposeAsync(); + } + } - public SchedulerMessage? Next(DateTimeOffset now) + /// + public void Dispose() + { + foreach (var timer in _timers.Values) { - lock (_lock) - { - var first = _messages.First?.Value; - if (first == null || first.At >= now) - { - return null; - } - - _messages.RemoveFirst(); - return first; - } + timer.Dispose(); } + + _timers.Clear(); + } - public void Add(SchedulerMessage message) + /// + public async ValueTask DisposeAsync() + { + foreach (var timer in _timers.Values) { - lock (_lock) - { - var node = _messages.First; - while (node != null) - { - if (node.Value.At > message.At) - { - _messages.AddBefore(node, message); - return; - } - - node = node.Next; - } - - _messages.AddLast(message); - } + await timer.DisposeAsync(); } - public void Delete(string id) + _timers.Clear(); + } + + private void Execute(string id, Message message) + { + BrighterAsyncContext.Run(async () => await processor.SendAsync(new SchedulerMessageFired + { + Id = id, + Message = message + })); + + if (_timers.TryRemove(id, out var timer)) { - lock (_lock) - { - var node = _messages.First; - while (node != null) - { - if (node.Value.Id == id) - { - _messages.Remove(node); - return; - } - - node = node.Next; - } - } + timer.Dispose(); } } } diff --git a/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs b/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs index 29a5fbf722..2804e086ae 100644 --- a/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs +++ b/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs @@ -2,29 +2,28 @@ namespace Paramore.Brighter; -public class InMemoryMessageSchedulerFactory(TimeSpan initialDelay, TimeSpan period) : IAmAMessageSchedulerFactory +public class InMemoryMessageSchedulerFactory(TimeProvider timerProvider) : IAmAMessageSchedulerFactory { public InMemoryMessageSchedulerFactory() - : this(TimeSpan.Zero, TimeSpan.FromSeconds(1)) + : this(TimeProvider.System) { } public IAmAMessageScheduler Create(IAmACommandProcessor processor) { - return GetOrCreate(processor, initialDelay, period); + return GetOrCreate(processor, timerProvider); } private static readonly object s_lock = new(); private static InMemoryMessageScheduler? s_scheduler; - private static InMemoryMessageScheduler GetOrCreate(IAmACommandProcessor processor, TimeSpan initialDelay, - TimeSpan period) + private static InMemoryMessageScheduler GetOrCreate(IAmACommandProcessor processor, TimeProvider timeProvider) { if (s_scheduler == null) { lock (s_lock) { - s_scheduler ??= new InMemoryMessageScheduler(processor, initialDelay, period); + s_scheduler ??= new InMemoryMessageScheduler(processor, timeProvider); } } diff --git a/src/Paramore.Brighter/InMemoryProducer.cs b/src/Paramore.Brighter/InMemoryProducer.cs index 6b88658f8f..ecd7d989d3 100644 --- a/src/Paramore.Brighter/InMemoryProducer.cs +++ b/src/Paramore.Brighter/InMemoryProducer.cs @@ -56,6 +56,9 @@ public class InMemoryProducer(IAmABus bus, TimeProvider timeProvider) /// public Activity? Span { get; set; } + /// + public IAmAMessageScheduler? Scheduler { get; set; } + /// /// What action should we take on confirmation that a message has been published to a broker /// diff --git a/src/Paramore.Brighter/OutboxProducerMediator.cs b/src/Paramore.Brighter/OutboxProducerMediator.cs index d4c43d588a..a51382a4a3 100644 --- a/src/Paramore.Brighter/OutboxProducerMediator.cs +++ b/src/Paramore.Brighter/OutboxProducerMediator.cs @@ -131,7 +131,7 @@ public OutboxProducerMediator( throw new ConfigurationException( "A Command Processor with an external bus must have a message transformer factory"); - _timeProvider = (timeProvider is null) ? TimeProvider.System : timeProvider; + _timeProvider = timeProvider ?? TimeProvider.System; _lastOutStandingMessageCheckAt = _timeProvider.GetUtcNow(); _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); diff --git a/src/Paramore.Brighter/Scheduler/Events/SchedulerMessageFired.cs b/src/Paramore.Brighter/Scheduler/Events/SchedulerMessageFired.cs index 4684f4e63c..3b998b7f88 100644 --- a/src/Paramore.Brighter/Scheduler/Events/SchedulerMessageFired.cs +++ b/src/Paramore.Brighter/Scheduler/Events/SchedulerMessageFired.cs @@ -1,18 +1,10 @@ -namespace Paramore.Brighter.Scheduler.Events; +using System; -// TODO Add doc +namespace Paramore.Brighter.Scheduler.Events; -public class SchedulerMessageFired(string id) : Event(id) -{ - public bool UseAsync { get; set; } - public SchedulerFireType FireType { get; set; } = SchedulerFireType.Send; - public string MessageType { get; set; } = string.Empty; - public string MessageData { get; set; } = string.Empty; -} +// TODO Add doc -public enum SchedulerFireType +public class SchedulerMessageFired() : Event(Guid.NewGuid().ToString()) { - Send, - Publish, - Post + public Message Message { get; set; } = new(); } diff --git a/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandlerAsync.cs b/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandlerAsync.cs index 689b353f51..3f9342870b 100644 --- a/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandlerAsync.cs +++ b/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandlerAsync.cs @@ -1,86 +1,20 @@ -using System; -using System.Collections.Concurrent; -using System.Reflection; -using System.Text.Json; -using System.Threading; +using System.Threading; using System.Threading.Tasks; +using System.Transactions; using Paramore.Brighter.Scheduler.Events; namespace Paramore.Brighter.Scheduler.Handlers; -public class SchedulerMessageFiredHandlerAsync(IAmACommandProcessor processor) +public class SchedulerMessageFiredHandlerAsync( + IAmAnOutboxProducerMediator mediator) : RequestHandlerAsync { - private static readonly ConcurrentDictionary s_types = new(); - - private static readonly MethodInfo s_executeAsyncMethod = typeof(SchedulerMessageFiredHandlerAsync) - .GetMethod(nameof(ExecuteAsync), BindingFlags.Static | BindingFlags.NonPublic)!; - - private static readonly ConcurrentDictionary> - s_executeAsync = new(); - public override async Task HandleAsync(SchedulerMessageFired command, CancellationToken cancellationToken = default) { - var type = s_types.GetOrAdd(command.MessageType, CreateType); - - var execute = s_executeAsync.GetOrAdd(type, CreateExecuteAsync); - await execute(processor, command.MessageData, command.UseAsync, command.FireType, cancellationToken); - + var context = new RequestContext(); + await mediator.AddToOutboxAsync(command.Message, context, cancellationToken: cancellationToken); + await mediator.ClearOutboxAsync([command.Message.Id], context, cancellationToken: cancellationToken); return await base.HandleAsync(command, cancellationToken); } - - private static Type CreateType(string messageType) - { - var assemblies = AppDomain.CurrentDomain.GetAssemblies(); - foreach (var assembly in assemblies) - { - var type = assembly.GetType(messageType); - if (type != null) - { - return type; - } - } - - throw new InvalidOperationException($"The message type could not be found: '{messageType}'"); - } - - private static ValueTask ExecuteAsync(IAmACommandProcessor commandProcessor, - string data, - bool async, - SchedulerFireType fireType, - CancellationToken cancellationToken) - where TRequest : class, IRequest - { - var request = JsonSerializer.Deserialize(data, JsonSerialisationOptions.Options)!; - switch (fireType) - { - case SchedulerFireType.Send when async: - return new ValueTask(commandProcessor.SendAsync(request, cancellationToken: cancellationToken)); - case SchedulerFireType.Send: - commandProcessor.Send(request); - return new ValueTask(); - case SchedulerFireType.Publish when async: - return new ValueTask(commandProcessor.PublishAsync(request, cancellationToken: cancellationToken)); - case SchedulerFireType.Publish: - commandProcessor.Publish(request); - return new ValueTask(); - case SchedulerFireType.Post when async: - return new ValueTask(commandProcessor.PostAsync(request, cancellationToken: cancellationToken)); - default: - commandProcessor.Post(request); - return new ValueTask(); - } - } - - private static Func - CreateExecuteAsync(Type type) - { - var method = s_executeAsyncMethod.MakeGenericMethod(type); - var func = (Func)method - .CreateDelegate( - typeof(Func)); - return func; - } } From 3cfa7f3d3d409b56460321943fd526e70a674bd6 Mon Sep 17 00:00:00 2001 From: Rafael Andrade Date: Tue, 4 Feb 2025 10:15:18 +0000 Subject: [PATCH 08/17] Improve scheduler messages --- .../AWSTaskQueue/GreetingsSender/Program.cs | 8 +-- .../ServiceCollectionExtensions.cs | 8 ++- .../HangfireMessageScheduler.cs | 2 +- .../ControlBus/ControlBusReceiverBuilder.cs | 3 -- .../InMemoryMessageScheduler.cs | 51 ++++++++++++++----- .../InMemoryMessageSchedulerFactory.cs | 26 ++++++++-- src/Paramore.Brighter/OnSchedulerConflict.cs | 14 +++++ .../OutboxProducerMediator.cs | 17 ++++++- .../Scheduler/Events/FireSchedulerMessage.cs | 14 +++++ .../Scheduler/Events/SchedulerMessageFired.cs | 10 ---- .../SchedulerMessageFiredHandlerAsync.cs | 20 -------- 11 files changed, 113 insertions(+), 60 deletions(-) create mode 100644 src/Paramore.Brighter/OnSchedulerConflict.cs create mode 100644 src/Paramore.Brighter/Scheduler/Events/FireSchedulerMessage.cs delete mode 100644 src/Paramore.Brighter/Scheduler/Events/SchedulerMessageFired.cs delete mode 100644 src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandlerAsync.cs diff --git a/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs b/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs index 1b3add8d41..7a562a6ecc 100644 --- a/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs +++ b/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs @@ -53,7 +53,7 @@ static void Main(string[] args) if (new CredentialProfileStoreChain().TryGetAWSCredentials("default", out var credentials)) { - var serviceURL = Environment.GetEnvironmentVariable("LOCALSTACK_SERVICE_URL"); + var serviceURL = "http://localhost:4566/"; // Environment.GetEnvironmentVariable("LOCALSTACK_SERVICE_URL"); var region = string.IsNullOrWhiteSpace(serviceURL) ? RegionEndpoint.EUWest1 : RegionEndpoint.USEast1; var awsConnection = new AWSMessagingGatewayConnection(credentials, region, cfg => { @@ -76,9 +76,9 @@ static void Main(string[] args) ).Create(); serviceCollection - .AddSingleton(new InMemoryMessageSchedulerFactory()) .AddBrighter() - .UseExternalBus((configure) => + .UseMessageScheduler(new InMemoryMessageSchedulerFactory()) + .UseExternalBus(configure => { configure.ProducerRegistry = producerRegistry; }) @@ -100,7 +100,7 @@ static void Main(string[] args) break; } - commandProcessor.SchedulerPost(TimeSpan.FromSeconds(10), new GreetingEvent($"Ian says: Hi {name}")); + commandProcessor.SchedulerPost(new GreetingEvent($"Ian says: Hi {name}"), TimeSpan.FromSeconds(10)); } commandProcessor.Post(new FarewellEvent("Ian says: See you later!")); diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index af40cd6dcf..ec0e7c9e53 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -33,8 +33,6 @@ THE SOFTWARE. */ using System.Text.Json; using Paramore.Brighter.DynamoDb; using Paramore.Brighter.Observability; -using Paramore.Brighter.Scheduler.Events; -using Paramore.Brighter.Scheduler.Handlers; using Polly.Registry; namespace Paramore.Brighter.Extensions.DependencyInjection @@ -244,8 +242,6 @@ public static IBrighterBuilder UseExternalBus( public static IBrighterBuilder UseMessageScheduler(this IBrighterBuilder builder, IAmAMessageSchedulerFactory factory) { builder.Services.AddSingleton(factory); - builder.AsyncHandlers(x => x.RegisterAsync()); - return builder; } @@ -340,7 +336,9 @@ private static IAmACommandProcessor BuildCommandProcessor(IServiceProvider provi var producerRegistry = provider.GetService(); if (messageSchedulerFactory != null && producerRegistry != null) { - producerRegistry.Producers.Each(x => x.Scheduler = messageSchedulerFactory.Create(command)); + producerRegistry + .Producers + .Each(x => x.Scheduler ??= messageSchedulerFactory.Create(command)); } return command; diff --git a/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageScheduler.cs b/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageScheduler.cs index 1371b66bf7..1af25e215d 100644 --- a/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageScheduler.cs +++ b/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageScheduler.cs @@ -31,7 +31,7 @@ public string Schedule(Message message, TimeSpan delay) public void Cancel(string id) => client.Delete(queue, id); private async Task ConsumeAsync(Message message) - => await processor.SendAsync(new SchedulerMessageFired { Id = message.Id, Message = message }); + => await processor.PostAsync(new FireSchedulerMessage { Id = message.Id, Message = message }); /// public Task ScheduleAsync(Message message, DateTimeOffset at, CancellationToken cancellationToken = default) diff --git a/src/Paramore.Brighter.ServiceActivator/ControlBus/ControlBusReceiverBuilder.cs b/src/Paramore.Brighter.ServiceActivator/ControlBus/ControlBusReceiverBuilder.cs index 8d0528d39f..42f772379d 100644 --- a/src/Paramore.Brighter.ServiceActivator/ControlBus/ControlBusReceiverBuilder.cs +++ b/src/Paramore.Brighter.ServiceActivator/ControlBus/ControlBusReceiverBuilder.cs @@ -26,8 +26,6 @@ THE SOFTWARE. */ using System.Collections.Generic; using System.Transactions; using Paramore.Brighter.Observability; -using Paramore.Brighter.Scheduler.Events; -using Paramore.Brighter.Scheduler.Handlers; using Paramore.Brighter.ServiceActivator.Ports; using Paramore.Brighter.ServiceActivator.Ports.Commands; using Paramore.Brighter.ServiceActivator.Ports.Handlers; @@ -141,7 +139,6 @@ public Dispatcher Build(string hostName) var subscriberRegistry = new SubscriberRegistry(); subscriberRegistry.Register(); subscriberRegistry.Register(); - subscriberRegistry.RegisterAsync(); var incomingMessageMapperRegistry = new MessageMapperRegistry( new ControlBusMessageMapperFactory(), null diff --git a/src/Paramore.Brighter/InMemoryMessageScheduler.cs b/src/Paramore.Brighter/InMemoryMessageScheduler.cs index 78e63718fe..ee322e6192 100644 --- a/src/Paramore.Brighter/InMemoryMessageScheduler.cs +++ b/src/Paramore.Brighter/InMemoryMessageScheduler.cs @@ -7,7 +7,10 @@ namespace Paramore.Brighter; -public class InMemoryMessageScheduler(IAmACommandProcessor processor, TimeProvider timeProvider) +public class InMemoryMessageScheduler(IAmACommandProcessor processor, + TimeProvider timeProvider, + Func getOrCreateSchedulerId, + OnSchedulerConflict onConflict) : IAmAMessageSchedulerSync, IAmAMessageSchedulerAsync { private readonly ConcurrentDictionary _timers = new(); @@ -19,27 +22,37 @@ public string Schedule(Message message, DateTimeOffset at) /// public string Schedule(Message message, TimeSpan delay) { - var id = Guid.NewGuid().ToString(); + var id = getOrCreateSchedulerId(message); + if (_timers.TryGetValue(id, out var timer)) + { + if (onConflict == OnSchedulerConflict.Throw) + { + throw new InvalidOperationException($"scheduler with '{id}' id already exists"); + } + + timer.Dispose(); + } + _timers[id] = timeProvider.CreateTimer(_ => Execute(id, message), null, delay, TimeSpan.Zero); return id; } /// public bool ReScheduler(string schedulerId, DateTimeOffset at) + => ReScheduler(schedulerId, at - DateTimeOffset.UtcNow); + + /// + public bool ReScheduler(string schedulerId, TimeSpan delay) { - if (_timers.TryGetValue(schedulerId, out var timer)) + if(_timers.TryGetValue(schedulerId, out var timer)) { - timer.Change(at - DateTimeOffset.UtcNow, TimeSpan.Zero); + timer.Change(delay, TimeSpan.Zero); return true; } - + return false; } - /// - public bool ReScheduler(string schedulerId, TimeSpan delay) - => ReScheduler(schedulerId, DateTimeOffset.UtcNow.Add(delay)); - /// public void Cancel(string id) { @@ -54,8 +67,22 @@ public Task ScheduleAsync(Message message, DateTimeOffset at, Cancellati => Task.FromResult(Schedule(message, at)); /// - public Task ScheduleAsync(Message message, TimeSpan delay, CancellationToken cancellationToken = default) - => Task.FromResult(Schedule(message, delay)); + public async Task ScheduleAsync(Message message, TimeSpan delay, CancellationToken cancellationToken = default) + { + var id = getOrCreateSchedulerId(message); + if (_timers.TryGetValue(id, out var timer)) + { + if (onConflict == OnSchedulerConflict.Throw) + { + throw new InvalidOperationException($"scheduler with '{id}' id already exists"); + } + + await timer.DisposeAsync(); + } + + _timers[id] = timeProvider.CreateTimer(_ => Execute(id, message), null, delay, TimeSpan.Zero); + return id; + } /// public Task ReSchedulerAsync(string schedulerId, DateTimeOffset at, @@ -100,7 +127,7 @@ public async ValueTask DisposeAsync() private void Execute(string id, Message message) { - BrighterAsyncContext.Run(async () => await processor.SendAsync(new SchedulerMessageFired + BrighterAsyncContext.Run(async () => await processor.PostAsync(new FireSchedulerMessage { Id = id, Message = message diff --git a/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs b/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs index 2804e086ae..e6fc3453b2 100644 --- a/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs +++ b/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs @@ -2,8 +2,25 @@ namespace Paramore.Brighter; +/// +/// The factory +/// +/// The . public class InMemoryMessageSchedulerFactory(TimeProvider timerProvider) : IAmAMessageSchedulerFactory { + /// + /// Get or create a scheduler id + /// + /// + /// The default approach is to use the message id. + /// + public Func GetOrCreateSchedulerId { get; set; } = message => message.Id; + + /// + /// The action be executed on conflict during scheduler message + /// + public OnSchedulerConflict OnConflict { get; set; } = OnSchedulerConflict.Throw; + public InMemoryMessageSchedulerFactory() : this(TimeProvider.System) { @@ -11,22 +28,23 @@ public InMemoryMessageSchedulerFactory() public IAmAMessageScheduler Create(IAmACommandProcessor processor) { - return GetOrCreate(processor, timerProvider); + return GetOrCreate(processor); } private static readonly object s_lock = new(); private static InMemoryMessageScheduler? s_scheduler; - private static InMemoryMessageScheduler GetOrCreate(IAmACommandProcessor processor, TimeProvider timeProvider) + private InMemoryMessageScheduler GetOrCreate(IAmACommandProcessor processor) { if (s_scheduler == null) { lock (s_lock) - { - s_scheduler ??= new InMemoryMessageScheduler(processor, timeProvider); + { + s_scheduler ??= new InMemoryMessageScheduler(processor, timerProvider, GetOrCreateSchedulerId, OnConflict); } } return s_scheduler; } } + diff --git a/src/Paramore.Brighter/OnSchedulerConflict.cs b/src/Paramore.Brighter/OnSchedulerConflict.cs new file mode 100644 index 0000000000..05948e38ac --- /dev/null +++ b/src/Paramore.Brighter/OnSchedulerConflict.cs @@ -0,0 +1,14 @@ +namespace Paramore.Brighter; + +/// +/// Action that should be executed when there is a conflict during create a scheduler +/// +public enum OnSchedulerConflict +{ + Throw, + + /// + /// Overwrite the previous scheduler + /// + Overwrite +} diff --git a/src/Paramore.Brighter/OutboxProducerMediator.cs b/src/Paramore.Brighter/OutboxProducerMediator.cs index a51382a4a3..a481816f3c 100644 --- a/src/Paramore.Brighter/OutboxProducerMediator.cs +++ b/src/Paramore.Brighter/OutboxProducerMediator.cs @@ -31,6 +31,7 @@ THE SOFTWARE. */ using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; using Paramore.Brighter.Observability; +using Paramore.Brighter.Scheduler.Events; using Polly; using Polly.Registry; @@ -425,6 +426,13 @@ public async Task ClearOutstandingFromOutboxAsync(int amountToClear, public Message CreateMessageFromRequest(TRequest request, RequestContext requestContext) where TRequest : class, IRequest { + // The fired scheduler message is a special case + // Because the message is in the raw form already, just waiting to be fired + if (request is FireSchedulerMessage scheduler) + { + return scheduler.Message; + } + var message = MapMessage(request, requestContext); return message; } @@ -443,7 +451,14 @@ public async Task CreateMessageFromRequestAsync( CancellationToken cancellationToken ) where TRequest : class, IRequest { - Message message = await MapMessageAsync(request, requestContext, cancellationToken); + // The fired scheduler message is a special case + // Because the message is in the raw form already, just waiting to be fired + if (request is FireSchedulerMessage schedulerMessage) + { + return schedulerMessage.Message; + } + + var message = await MapMessageAsync(request, requestContext, cancellationToken); return message; } diff --git a/src/Paramore.Brighter/Scheduler/Events/FireSchedulerMessage.cs b/src/Paramore.Brighter/Scheduler/Events/FireSchedulerMessage.cs new file mode 100644 index 0000000000..2f3e4ff471 --- /dev/null +++ b/src/Paramore.Brighter/Scheduler/Events/FireSchedulerMessage.cs @@ -0,0 +1,14 @@ +using System; + +namespace Paramore.Brighter.Scheduler.Events; + +/// +/// A command to fire a scheduler message +/// +public class FireSchedulerMessage() : Command(Guid.NewGuid().ToString()) +{ + /// + /// The message that will be fire + /// + public Message Message { get; init; } = new(); +} diff --git a/src/Paramore.Brighter/Scheduler/Events/SchedulerMessageFired.cs b/src/Paramore.Brighter/Scheduler/Events/SchedulerMessageFired.cs deleted file mode 100644 index 3b998b7f88..0000000000 --- a/src/Paramore.Brighter/Scheduler/Events/SchedulerMessageFired.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Paramore.Brighter.Scheduler.Events; - -// TODO Add doc - -public class SchedulerMessageFired() : Event(Guid.NewGuid().ToString()) -{ - public Message Message { get; set; } = new(); -} diff --git a/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandlerAsync.cs b/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandlerAsync.cs deleted file mode 100644 index 3f9342870b..0000000000 --- a/src/Paramore.Brighter/Scheduler/Handlers/SchedulerMessageFiredHandlerAsync.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using System.Transactions; -using Paramore.Brighter.Scheduler.Events; - -namespace Paramore.Brighter.Scheduler.Handlers; - -public class SchedulerMessageFiredHandlerAsync( - IAmAnOutboxProducerMediator mediator) - : RequestHandlerAsync -{ - public override async Task HandleAsync(SchedulerMessageFired command, - CancellationToken cancellationToken = default) - { - var context = new RequestContext(); - await mediator.AddToOutboxAsync(command.Message, context, cancellationToken: cancellationToken); - await mediator.ClearOutboxAsync([command.Message.Id], context, cancellationToken: cancellationToken); - return await base.HandleAsync(command, cancellationToken); - } -} From cd0ec7f1049a5d66c80ccb0888f215c9f6f15ed3 Mon Sep 17 00:00:00 2001 From: Rafael Andrade Date: Tue, 4 Feb 2025 10:44:54 +0000 Subject: [PATCH 09/17] Fixes unit tests --- .../Fakes/FakeMessageProducer.cs | 2 + ..._Limit_Total_Writes_To_OutBox_In_Window.cs | 1 + .../When_Posting_With_A_Default_Policy.cs | 1 + .../FakeErroringMessageProducerSync.cs | 3 +- ..._A_Handler_Is_Feature_Switch_Config_Off.cs | 1 + ...n_A_Handler_Is_Feature_Switch_Config_On.cs | 1 + ...Feature_Switch_Missing_Config_Exception.cs | 1 + ...Feature_Switch_Missing_Config_SilentOff.cs | 1 + ..._Feature_Switch_Missing_Config_SilentOn.cs | 1 + .../When_A_Handler_Is_Feature_Switch_Off.cs | 1 + .../When_A_Handler_Is_Feature_Switch_On.cs | 1 + .../When_No_Feature_Switch_Config.cs | 1 + .../TestDoubles/SpyCommandProcessor.cs | 37 ++++++++++++++++++- .../When_building_a_dispatcher.cs | 1 + .../When_building_a_dispatcher_async.cs | 1 + ...uilding_a_dispatcher_with_named_gateway.cs | 1 + ...g_a_dispatcher_with_named_gateway_async.cs | 1 + 17 files changed, 54 insertions(+), 2 deletions(-) diff --git a/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeMessageProducer.cs b/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeMessageProducer.cs index df947fb31f..c377060699 100644 --- a/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeMessageProducer.cs +++ b/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeMessageProducer.cs @@ -11,6 +11,8 @@ public class FakeMessageProducer : IAmAMessageProducerAsync, IAmAMessageProducer public List SentMessages { get; } = new List(); public Publication Publication { get; } public Activity Span { get; set; } + public IAmAMessageScheduler? Scheduler { get; set; } + public Task SendAsync(Message message, CancellationToken cancellationToken = default) { Send(message); diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Post/When_Posting_Fails_Limit_Total_Writes_To_OutBox_In_Window.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Post/When_Posting_Fails_Limit_Total_Writes_To_OutBox_In_Window.cs index 871ee78e84..dffe70b735 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Post/When_Posting_Fails_Limit_Total_Writes_To_OutBox_In_Window.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Post/When_Posting_Fails_Limit_Total_Writes_To_OutBox_In_Window.cs @@ -79,6 +79,7 @@ public PostFailureLimitCommandTests() .ExternalBus(ExternalBusType.FireAndForget, externalBus) .ConfigureInstrumentation(new BrighterTracer(TimeProvider.System), InstrumentationOptions.All) .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(null) .Build(); } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Post/When_Posting_With_A_Default_Policy.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Post/When_Posting_With_A_Default_Policy.cs index 06a832047f..c36643c243 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Post/When_Posting_With_A_Default_Policy.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Post/When_Posting_With_A_Default_Policy.cs @@ -87,6 +87,7 @@ public PostCommandTests() .ExternalBus(ExternalBusType.FireAndForget, externalBus) .ConfigureInstrumentation(new BrighterTracer(TimeProvider.System), InstrumentationOptions.All) .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(null) .Build(); } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeErroringMessageProducerSync.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeErroringMessageProducerSync.cs index 57a80661bc..f584359205 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeErroringMessageProducerSync.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeErroringMessageProducerSync.cs @@ -11,7 +11,8 @@ public class FakeErroringMessageProducerSync : IAmAMessageProducerSync public Publication Publication { get; } = new(); public Activity Span { get; set; } - + public IAmAMessageScheduler? Scheduler { get; set; } + public void Dispose() { } public void Send(Message message) diff --git a/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Config_Off.cs b/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Config_Off.cs index fd9b5bfe67..744c5eb98d 100644 --- a/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Config_Off.cs +++ b/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Config_Off.cs @@ -76,6 +76,7 @@ public CommandProcessorWithFeatureSwitchOffByConfigInPipelineTests() .NoExternalBus() .ConfigureInstrumentation(new BrighterTracer(TimeProvider.System), InstrumentationOptions.All) .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(null) .Build(); } diff --git a/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Config_On.cs b/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Config_On.cs index 9e2d64fcc6..193fe46ab8 100644 --- a/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Config_On.cs +++ b/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Config_On.cs @@ -76,6 +76,7 @@ public CommandProcessorWithFeatureSwitchOnByConfigInPipelineTests() .NoExternalBus() .ConfigureInstrumentation(new BrighterTracer(TimeProvider.System), InstrumentationOptions.All) .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(null) .Build(); } diff --git a/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Missing_Config_Exception.cs b/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Missing_Config_Exception.cs index fa32ac271f..b79997e2ad 100644 --- a/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Missing_Config_Exception.cs +++ b/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Missing_Config_Exception.cs @@ -74,6 +74,7 @@ public FeatureSwitchByConfigMissingConfigStrategyExceptionTests() .NoExternalBus() .ConfigureInstrumentation(new BrighterTracer(TimeProvider.System), InstrumentationOptions.All) .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(null) .Build(); } diff --git a/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Missing_Config_SilentOff.cs b/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Missing_Config_SilentOff.cs index 28b04b22c5..053c738b43 100644 --- a/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Missing_Config_SilentOff.cs +++ b/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Missing_Config_SilentOff.cs @@ -74,6 +74,7 @@ public FeatureSwitchByConfigMissingConfigStrategySilentOffTests() .NoExternalBus() .ConfigureInstrumentation(new BrighterTracer(TimeProvider.System), InstrumentationOptions.All) .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(null) .Build(); } diff --git a/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Missing_Config_SilentOn.cs b/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Missing_Config_SilentOn.cs index e74e91cd1d..e0bc0bc539 100644 --- a/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Missing_Config_SilentOn.cs +++ b/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Missing_Config_SilentOn.cs @@ -74,6 +74,7 @@ public FeatureSwitchByConfigMissingConfigStrategySilentOnTests() .NoExternalBus() .ConfigureInstrumentation(new BrighterTracer(TimeProvider.System), InstrumentationOptions.All) .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(null) .Build(); } diff --git a/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Off.cs b/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Off.cs index 7fcd6887bb..e1a4bcce24 100644 --- a/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Off.cs +++ b/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_Off.cs @@ -65,6 +65,7 @@ public CommandProcessorWithFeatureSwitchOffInPipelineTests() .NoExternalBus() .ConfigureInstrumentation(new BrighterTracer(TimeProvider.System), InstrumentationOptions.All) .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(null) .Build(); } diff --git a/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_On.cs b/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_On.cs index bb28d7db48..3435d262b2 100644 --- a/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_On.cs +++ b/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_A_Handler_Is_Feature_Switch_On.cs @@ -65,6 +65,7 @@ public CommandProcessorWithFeatureSwitchOnInPipelineTests() .NoExternalBus() .ConfigureInstrumentation(new BrighterTracer(TimeProvider.System), InstrumentationOptions.All) .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(null) .Build(); } diff --git a/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_No_Feature_Switch_Config.cs b/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_No_Feature_Switch_Config.cs index 9291e192e5..b715cb6ded 100644 --- a/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_No_Feature_Switch_Config.cs +++ b/tests/Paramore.Brighter.Core.Tests/FeatureSwitch/When_No_Feature_Switch_Config.cs @@ -67,6 +67,7 @@ public CommandProcessorWithNullFeatureSwitchConfig() .NoExternalBus() .ConfigureInstrumentation(new BrighterTracer(), InstrumentationOptions.All) .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(null) .Build(); } diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/SpyCommandProcessor.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/SpyCommandProcessor.cs index 5b431c1899..d9e022f3cf 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/SpyCommandProcessor.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/SpyCommandProcessor.cs @@ -44,7 +44,9 @@ enum CommandType DepositAsync, Clear, ClearAsync, - Call + Call, + Scheduler, + SchedulerAsync } public class ClearParams @@ -57,6 +59,7 @@ public class ClearParams internal class SpyCommandProcessor : IAmACommandProcessor { private readonly Queue _requests = new Queue(); + private readonly Queue _scheduler = new Queue(); private readonly Dictionary _postBox = new(); public IList Commands { get; } = new List(); @@ -103,6 +106,38 @@ public virtual async Task PublishAsync( await completionSource.Task; } + public string SchedulerPost(TRequest request, TimeSpan delay, RequestContext? requestContext = null, + Dictionary? args = null) where TRequest : class, IRequest + { + _scheduler.Enqueue(request); + Commands.Add(CommandType.Scheduler); + return Guid.NewGuid().ToString(); + } + + public string SchedulerPost(TRequest request, DateTimeOffset at, RequestContext? requestContext = null, + Dictionary? args = null) where TRequest : class, IRequest + { + _scheduler.Enqueue(request); + Commands.Add(CommandType.Scheduler); + return Guid.NewGuid().ToString(); + } + + public Task SchedulerPostAsync(TRequest request, TimeSpan delay, RequestContext? requestContext = null, + Dictionary? args = null, bool continueOnCapturedContext = true, CancellationToken cancellationToken = default) where TRequest : class, IRequest + { + _scheduler.Enqueue(request); + Commands.Add(CommandType.SchedulerAsync); + return Task.FromResult(Guid.NewGuid().ToString()); + } + + public Task SchedulerPostAsync(TRequest request, DateTimeOffset at, RequestContext? requestContext = null, + Dictionary? args = null, bool continueOnCapturedContext = true, CancellationToken cancellationToken = default) where TRequest : class, IRequest + { + _scheduler.Enqueue(request); + Commands.Add(CommandType.SchedulerAsync); + return Task.FromResult(Guid.NewGuid().ToString()); + } + public virtual void Post(TRequest request, RequestContext requestContext = null, Dictionary args = null) where TRequest : class, IRequest { diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher.cs b/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher.cs index 17a947532d..3876aaeb33 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher.cs @@ -62,6 +62,7 @@ public DispatchBuilderTests() .NoExternalBus() .ConfigureInstrumentation(tracer, instrumentationOptions) .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(null) .Build(); _builder = DispatchBuilder.StartNew() diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher_async.cs b/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher_async.cs index 9ab6f5682e..64ef0f85e6 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher_async.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher_async.cs @@ -62,6 +62,7 @@ public DispatchBuilderTestsAsync() .NoExternalBus() .ConfigureInstrumentation(tracer, instrumentationOptions) .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(null) .Build(); _builder = DispatchBuilder.StartNew() diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher_with_named_gateway.cs b/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher_with_named_gateway.cs index 80c2398ca9..d087c883a1 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher_with_named_gateway.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher_with_named_gateway.cs @@ -57,6 +57,7 @@ public DispatchBuilderWithNamedGateway() .NoExternalBus() .ConfigureInstrumentation(tracer, instrumentationOptions) .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(null) .Build(); _builder = DispatchBuilder.StartNew() diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher_with_named_gateway_async.cs b/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher_with_named_gateway_async.cs index 87125640be..36237eca29 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher_with_named_gateway_async.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher_with_named_gateway_async.cs @@ -58,6 +58,7 @@ public DispatchBuilderWithNamedGatewayAsync() .NoExternalBus() .ConfigureInstrumentation(tracer, instrumentationOptions) .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(null) .Build(); _builder = DispatchBuilder.StartNew() From aff924003d6c070665b8a3a17e295624f53fbb44 Mon Sep 17 00:00:00 2001 From: Rafael Andrade Date: Tue, 4 Feb 2025 14:11:47 +0000 Subject: [PATCH 10/17] Add Unit test --- .../HangfireMessageScheduler.cs | 17 +- src/Paramore.Brighter/CommandProcessor.cs | 6 +- .../InMemoryMessageScheduler.cs | 56 +++--- .../InMemoryMessageSchedulerFactory.cs | 22 +-- .../Observability/BrighterSpanExtensions.cs | 1 + ...ting_A_Message_To_The_Command_Processor.cs | 2 +- ...And_There_Is_No_Message_Mapper_Registry.cs | 116 +++++++++++ ...ere_Is_No_Message_Mapper_Registry_Async.cs | 108 ++++++++++ ...d_There_Is_No_Message_Scheduler_Factory.cs | 115 +++++++++++ ...e_Is_No_Message_Scheduler_Factory_Async.cs | 116 +++++++++++ ...ling_A_Message_To_The_Command_Processor.cs | 139 +++++++++++++ ..._Message_To_The_Command_Processor_Async.cs | 135 +++++++++++++ .../When_Scheduling_With_A_Default_Policy.cs | 138 +++++++++++++ ...Scheduling_A_Request_A_Span_Is_Exported.cs | 187 ++++++++++++++++++ ...ling_A_Request_A_Span_Is_Exported_Async.cs | 187 ++++++++++++++++++ 15 files changed, 1292 insertions(+), 53 deletions(-) create mode 100644 tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_And_There_Is_No_Message_Mapper_Registry.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_And_There_Is_No_Message_Mapper_Registry_Async.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_And_There_Is_No_Message_Scheduler_Factory.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_And_There_Is_No_Message_Scheduler_Factory_Async.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_To_The_Command_Processor.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_To_The_Command_Processor_Async.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_With_A_Default_Policy.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/Observability/CommandProcessor/Scheduler/When_Scheduling_A_Request_A_Span_Is_Exported.cs create mode 100644 tests/Paramore.Brighter.Core.Tests/Observability/CommandProcessor/Scheduler/When_Scheduling_A_Request_A_Span_Is_Exported_Async.cs diff --git a/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageScheduler.cs b/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageScheduler.cs index 1af25e215d..d280b6da59 100644 --- a/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageScheduler.cs +++ b/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageScheduler.cs @@ -15,11 +15,11 @@ public class HangfireMessageScheduler( { /// public string Schedule(Message message, DateTimeOffset at) - => client.Schedule(queue, () => ConsumeAsync(message), at); + => client.Schedule(queue, () => ConsumeAsync(message, false), at); /// public string Schedule(Message message, TimeSpan delay) - => client.Schedule(queue, () => ConsumeAsync(message), delay); + => client.Schedule(queue, () => ConsumeAsync(message, true), delay); /// public bool ReScheduler(string schedulerId, DateTimeOffset at) => client.Reschedule(schedulerId, at); @@ -30,8 +30,17 @@ public string Schedule(Message message, TimeSpan delay) /// public void Cancel(string id) => client.Delete(queue, id); - private async Task ConsumeAsync(Message message) - => await processor.PostAsync(new FireSchedulerMessage { Id = message.Id, Message = message }); + private async Task ConsumeAsync(Message message, bool async) + { + if (async) + { + await processor.PostAsync(new FireSchedulerMessage { Id = message.Id, Message = message }); + } + else + { + processor.Post(new FireSchedulerMessage { Id = message.Id, Message = message }); + } + } /// public Task ScheduleAsync(Message message, DateTimeOffset at, CancellationToken cancellationToken = default) diff --git a/src/Paramore.Brighter/CommandProcessor.cs b/src/Paramore.Brighter/CommandProcessor.cs index aa1f4f4aa5..fff2be65c3 100644 --- a/src/Paramore.Brighter/CommandProcessor.cs +++ b/src/Paramore.Brighter/CommandProcessor.cs @@ -187,6 +187,7 @@ public CommandProcessor( _tracer = tracer; _instrumentationOptions = instrumentationOptions; _replySubscriptions = replySubscriptions; + _messageSchedulerFactory = messageSchedulerFactory; InitExtServiceBus(bus); } @@ -203,6 +204,7 @@ public CommandProcessor( /// The Subscriptions for creating the reply queues /// What is the tracer we will use for telemetry /// When creating a span for operations how noisy should the attributes be + /// The . public CommandProcessor( IAmARequestContextFactory requestContextFactory, IPolicyRegistry policyRegistry, @@ -211,7 +213,8 @@ public CommandProcessor( InboxConfiguration? inboxConfiguration = null, IEnumerable? replySubscriptions = null, IAmABrighterTracer? tracer = null, - InstrumentationOptions instrumentationOptions = InstrumentationOptions.All) + InstrumentationOptions instrumentationOptions = InstrumentationOptions.All, + IAmAMessageSchedulerFactory? messageSchedulerFactory = null) { _requestContextFactory = requestContextFactory; _policyRegistry = policyRegistry; @@ -220,6 +223,7 @@ public CommandProcessor( _replySubscriptions = replySubscriptions; _tracer = tracer; _instrumentationOptions = instrumentationOptions; + _messageSchedulerFactory = messageSchedulerFactory; InitExtServiceBus(mediator); } diff --git a/src/Paramore.Brighter/InMemoryMessageScheduler.cs b/src/Paramore.Brighter/InMemoryMessageScheduler.cs index ee322e6192..930025297f 100644 --- a/src/Paramore.Brighter/InMemoryMessageScheduler.cs +++ b/src/Paramore.Brighter/InMemoryMessageScheduler.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Paramore.Brighter.Scheduler.Events; using Paramore.Brighter.Tasks; +using InvalidOperationException = System.InvalidOperationException; namespace Paramore.Brighter; @@ -13,7 +14,7 @@ public class InMemoryMessageScheduler(IAmACommandProcessor processor, OnSchedulerConflict onConflict) : IAmAMessageSchedulerSync, IAmAMessageSchedulerAsync { - private readonly ConcurrentDictionary _timers = new(); + private static readonly ConcurrentDictionary s_timers = new(); /// public string Schedule(Message message, DateTimeOffset at) @@ -23,7 +24,7 @@ public string Schedule(Message message, DateTimeOffset at) public string Schedule(Message message, TimeSpan delay) { var id = getOrCreateSchedulerId(message); - if (_timers.TryGetValue(id, out var timer)) + if (s_timers.TryGetValue(id, out var timer)) { if (onConflict == OnSchedulerConflict.Throw) { @@ -33,7 +34,7 @@ public string Schedule(Message message, TimeSpan delay) timer.Dispose(); } - _timers[id] = timeProvider.CreateTimer(_ => Execute(id, message), null, delay, TimeSpan.Zero); + s_timers[id] = timeProvider.CreateTimer(Execute, (processor, id, message, false), delay, TimeSpan.Zero); return id; } @@ -44,7 +45,7 @@ public bool ReScheduler(string schedulerId, DateTimeOffset at) /// public bool ReScheduler(string schedulerId, TimeSpan delay) { - if(_timers.TryGetValue(schedulerId, out var timer)) + if(s_timers.TryGetValue(schedulerId, out var timer)) { timer.Change(delay, TimeSpan.Zero); return true; @@ -56,7 +57,7 @@ public bool ReScheduler(string schedulerId, TimeSpan delay) /// public void Cancel(string id) { - if (_timers.TryRemove(id, out var timer)) + if (s_timers.TryRemove(id, out var timer)) { timer.Dispose(); } @@ -70,7 +71,7 @@ public Task ScheduleAsync(Message message, DateTimeOffset at, Cancellati public async Task ScheduleAsync(Message message, TimeSpan delay, CancellationToken cancellationToken = default) { var id = getOrCreateSchedulerId(message); - if (_timers.TryGetValue(id, out var timer)) + if (s_timers.TryGetValue(id, out var timer)) { if (onConflict == OnSchedulerConflict.Throw) { @@ -80,7 +81,7 @@ public async Task ScheduleAsync(Message message, TimeSpan delay, Cancell await timer.DisposeAsync(); } - _timers[id] = timeProvider.CreateTimer(_ => Execute(id, message), null, delay, TimeSpan.Zero); + s_timers[id] = timeProvider.CreateTimer(Execute, (processor, id, message, true), delay, TimeSpan.Zero); return id; } @@ -97,7 +98,7 @@ public Task ReSchedulerAsync(string schedulerId, TimeSpan delay, /// public async Task CancelAsync(string id, CancellationToken cancellationToken = default) { - if (_timers.TryRemove(id, out var timer)) + if (s_timers.TryRemove(id, out var timer)) { await timer.DisposeAsync(); } @@ -106,34 +107,35 @@ public async Task CancelAsync(string id, CancellationToken cancellationToken = d /// public void Dispose() { - foreach (var timer in _timers.Values) - { - timer.Dispose(); - } - - _timers.Clear(); } /// - public async ValueTask DisposeAsync() + public ValueTask DisposeAsync() { - foreach (var timer in _timers.Values) - { - await timer.DisposeAsync(); - } - - _timers.Clear(); + return new ValueTask(); } - private void Execute(string id, Message message) + private static void Execute(object? state) { - BrighterAsyncContext.Run(async () => await processor.PostAsync(new FireSchedulerMessage + var (processor, id, message, async) = ((IAmACommandProcessor, string, Message, bool)) state!; + if(async) + { + BrighterAsyncContext.Run(async () => await processor.PostAsync(new FireSchedulerMessage + { + Id = id, + Message = message + })); + } + else { - Id = id, - Message = message - })); + processor.Post(new FireSchedulerMessage + { + Id = id, + Message = message + }); + } - if (_timers.TryRemove(id, out var timer)) + if (s_timers.TryRemove(id, out var timer)) { timer.Dispose(); } diff --git a/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs b/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs index e6fc3453b2..3003f3463c 100644 --- a/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs +++ b/src/Paramore.Brighter/InMemoryMessageSchedulerFactory.cs @@ -26,25 +26,7 @@ public InMemoryMessageSchedulerFactory() { } - public IAmAMessageScheduler Create(IAmACommandProcessor processor) - { - return GetOrCreate(processor); - } - - private static readonly object s_lock = new(); - private static InMemoryMessageScheduler? s_scheduler; - - private InMemoryMessageScheduler GetOrCreate(IAmACommandProcessor processor) - { - if (s_scheduler == null) - { - lock (s_lock) - { - s_scheduler ??= new InMemoryMessageScheduler(processor, timerProvider, GetOrCreateSchedulerId, OnConflict); - } - } - - return s_scheduler; - } + public IAmAMessageScheduler Create(IAmACommandProcessor processor) + => new InMemoryMessageScheduler(processor, timerProvider, GetOrCreateSchedulerId, OnConflict); } diff --git a/src/Paramore.Brighter/Observability/BrighterSpanExtensions.cs b/src/Paramore.Brighter/Observability/BrighterSpanExtensions.cs index 9bc5e89a22..df9afe05de 100644 --- a/src/Paramore.Brighter/Observability/BrighterSpanExtensions.cs +++ b/src/Paramore.Brighter/Observability/BrighterSpanExtensions.cs @@ -42,6 +42,7 @@ public static class BrighterSpanExtensions CommandProcessorSpanOperation.Send => "send", CommandProcessorSpanOperation.Clear => "clear", CommandProcessorSpanOperation.Archive => "archive", + CommandProcessorSpanOperation.Scheduler => "scheduler", _ => throw new ArgumentOutOfRangeException(nameof(operation), operation, null) }; diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Post/When_Posting_A_Message_To_The_Command_Processor.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Post/When_Posting_A_Message_To_The_Command_Processor.cs index 9735e800b3..719b22a924 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Post/When_Posting_A_Message_To_The_Command_Processor.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Post/When_Posting_A_Message_To_The_Command_Processor.cs @@ -99,7 +99,7 @@ public CommandProcessorPostCommandTests() policyRegistry, bus ); - } + } [Fact] public void When_Posting_A_Message_To_The_Command_Processor() diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_And_There_Is_No_Message_Mapper_Registry.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_And_There_Is_No_Message_Mapper_Registry.cs new file mode 100644 index 0000000000..55908b050a --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_And_There_Is_No_Message_Mapper_Registry.cs @@ -0,0 +1,116 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2015 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Transactions; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Core.Tests.TestHelpers; +using Paramore.Brighter.Observability; +using Polly; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.CommandProcessors.Scheduler; + +[Collection("CommandProcessor")] +public class CommandSchedulingNoMessageMapperTests : IDisposable +{ + private readonly CommandProcessor _commandProcessor; + private readonly MyCommand _myCommand = new MyCommand(); + private Exception _exception; + + public CommandSchedulingNoMessageMapperTests() + { + var routingKey = new RoutingKey("MyCommand"); + _myCommand.Value = "Hello World"; + + var timeProvider = new FakeTimeProvider(); + InMemoryProducer producer = new(new InternalBus(), timeProvider) + { + Publication = {Topic = routingKey, RequestType = typeof(MyCommand)} + }; + + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory((_) => new MyCommandMessageMapper()), + null); + + var retryPolicy = Policy + .Handle() + .Retry(); + + var circuitBreakerPolicy = Policy + .Handle() + .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); + + var producerRegistry = new ProducerRegistry(new Dictionary + { + { routingKey, producer }, + }); + + var policyRegistry = new PolicyRegistry + { + { CommandProcessor.RETRYPOLICY, retryPolicy }, + { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } + }; + + var tracer = new BrighterTracer(timeProvider); + var outbox = new InMemoryOutbox(timeProvider) {Tracer = tracer}; + + IAmAnOutboxProducerMediator bus = new OutboxProducerMediator( + producerRegistry, + policyRegistry, + messageMapperRegistry, + new EmptyMessageTransformerFactory(), + new EmptyMessageTransformerFactoryAsync(), + tracer, + outbox + ); + + CommandProcessor.ClearServiceBus(); + _commandProcessor = new CommandProcessor( + new InMemoryRequestContextFactory(), + policyRegistry, + bus, + messageSchedulerFactory: new InMemoryMessageSchedulerFactory() + ); + } + + [Fact] + public void When_Scheduling_A_Message_And_There_Is_No_Message_Mapper_Factory() + { + _exception = Catch.Exception(() => _commandProcessor.SchedulerPost(_myCommand, TimeSpan.FromSeconds(10))); + _exception.Should().BeOfType(); + + _exception = Catch.Exception(() => _commandProcessor.SchedulerPost(_myCommand, DateTimeOffset.UtcNow.AddSeconds(10))); + _exception.Should().BeOfType(); + } + + public void Dispose() + { + CommandProcessor.ClearServiceBus(); + } +} \ No newline at end of file diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_And_There_Is_No_Message_Mapper_Registry_Async.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_And_There_Is_No_Message_Mapper_Registry_Async.cs new file mode 100644 index 0000000000..0c962ed0c8 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_And_There_Is_No_Message_Mapper_Registry_Async.cs @@ -0,0 +1,108 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2015 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Transactions; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Core.Tests.TestHelpers; +using Paramore.Brighter.Observability; +using Polly; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.CommandProcessors.Scheduler; + +[Collection("CommandProcessor")] +public class CommandSchedulingNoMessageMapperAsyncTests : IDisposable +{ + private readonly CommandProcessor _commandProcessor; + private readonly MyCommand _myCommand = new(); + private Exception _exception; + + public CommandSchedulingNoMessageMapperAsyncTests() + { + var routingKey = new RoutingKey("MyCommand"); + _myCommand.Value = "Hello World"; + + var timeProvider = new FakeTimeProvider(); + InMemoryProducer producer = new(new InternalBus(), timeProvider) + { + Publication = {Topic = routingKey, RequestType = typeof(MyCommand)} + }; + + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory((_) => new MyCommandMessageMapper()), + null); + + var retryPolicy = Policy + .Handle() + .RetryAsync(); + + var circuitBreakerPolicy = Policy + .Handle() + .CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1)); + + var policyRegistry = new PolicyRegistry { { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } }; + var producerRegistry = new ProducerRegistry(new Dictionary {{routingKey, producer},}); + + var tracer = new BrighterTracer(timeProvider); + var outbox = new InMemoryOutbox(timeProvider) {Tracer = tracer}; + + IAmAnOutboxProducerMediator bus = new OutboxProducerMediator( + producerRegistry, + policyRegistry, + messageMapperRegistry, + new EmptyMessageTransformerFactory(), + new EmptyMessageTransformerFactoryAsync(), + tracer, + outbox + ); + + _commandProcessor = new CommandProcessor( + new InMemoryRequestContextFactory(), + policyRegistry, + bus, + messageSchedulerFactory: new InMemoryMessageSchedulerFactory() + ); + } + + [Fact] + public async Task When_Scheduling_A_Message_And_There_Is_No_Message_Mapper_Factory_Async() + { + _exception = await Catch.ExceptionAsync(async () => await _commandProcessor.SchedulerPostAsync(_myCommand, TimeSpan.FromMinutes(1))); + _exception.Should().BeOfType(); + + _exception = await Catch.ExceptionAsync(async () => await _commandProcessor.SchedulerPostAsync(_myCommand, DateTimeOffset.UtcNow.AddMinutes(1))); + _exception.Should().BeOfType(); + } + + public void Dispose() + { + CommandProcessor.ClearServiceBus(); + } +} \ No newline at end of file diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_And_There_Is_No_Message_Scheduler_Factory.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_And_There_Is_No_Message_Scheduler_Factory.cs new file mode 100644 index 0000000000..880879df25 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_And_There_Is_No_Message_Scheduler_Factory.cs @@ -0,0 +1,115 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2015 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Transactions; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Core.Tests.TestHelpers; +using Paramore.Brighter.Observability; +using Polly; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.CommandProcessors.Scheduler; + +[Collection("CommandProcessor")] +public class CommandSchedulerNoMessageSchedulerFactoryTests : IDisposable +{ + private readonly CommandProcessor _commandProcessor; + private readonly MyCommand _myCommand = new MyCommand(); + private Exception _exception; + + public CommandSchedulerNoMessageSchedulerFactoryTests() + { + var routingKey = new RoutingKey("MyCommand"); + _myCommand.Value = "Hello World"; + + var timeProvider = new FakeTimeProvider(); + InMemoryProducer producer = new(new InternalBus(), timeProvider) + { + Publication = {Topic = routingKey, RequestType = typeof(MyCommand)} + }; + + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory((_) => new MyCommandMessageMapper()), + null); + + var retryPolicy = Policy + .Handle() + .Retry(); + + var circuitBreakerPolicy = Policy + .Handle() + .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); + + var producerRegistry = new ProducerRegistry(new Dictionary + { + { routingKey, producer }, + }); + + var policyRegistry = new PolicyRegistry + { + { CommandProcessor.RETRYPOLICY, retryPolicy }, + { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } + }; + + var tracer = new BrighterTracer(timeProvider); + var outbox = new InMemoryOutbox(timeProvider) {Tracer = tracer}; + + IAmAnOutboxProducerMediator bus = new OutboxProducerMediator( + producerRegistry, + policyRegistry, + messageMapperRegistry, + new EmptyMessageTransformerFactory(), + new EmptyMessageTransformerFactoryAsync(), + tracer, + outbox + ); + + CommandProcessor.ClearServiceBus(); + _commandProcessor = new CommandProcessor( + new InMemoryRequestContextFactory(), + policyRegistry, + bus + ); + } + + [Fact] + public void When_Scheduling_A_Message_And_There_Is_No_Message_Scheduler_Factory() + { + _exception = Catch.Exception(() => _commandProcessor.SchedulerPost(_myCommand, TimeSpan.FromSeconds(1))); + _exception.Should().BeOfType(); + + _exception = Catch.Exception(() => _commandProcessor.SchedulerPost(_myCommand, DateTimeOffset.UtcNow.AddSeconds(10))); + _exception.Should().BeOfType(); + } + + public void Dispose() + { + CommandProcessor.ClearServiceBus(); + } +} \ No newline at end of file diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_And_There_Is_No_Message_Scheduler_Factory_Async.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_And_There_Is_No_Message_Scheduler_Factory_Async.cs new file mode 100644 index 0000000000..019a5210da --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_And_There_Is_No_Message_Scheduler_Factory_Async.cs @@ -0,0 +1,116 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2015 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Transactions; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Core.Tests.TestHelpers; +using Paramore.Brighter.Observability; +using Polly; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.CommandProcessors.Scheduler; + +[Collection("CommandProcessor")] +public class CommandSchedulerNoMessageSchedulerFactoryAsyncTests : IDisposable +{ + private readonly CommandProcessor _commandProcessor; + private readonly MyCommand _myCommand = new MyCommand(); + private Exception _exception; + + public CommandSchedulerNoMessageSchedulerFactoryAsyncTests() + { + var routingKey = new RoutingKey("MyCommand"); + _myCommand.Value = "Hello World"; + + var timeProvider = new FakeTimeProvider(); + InMemoryProducer producer = new(new InternalBus(), timeProvider) + { + Publication = {Topic = routingKey, RequestType = typeof(MyCommand)} + }; + + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory((_) => new MyCommandMessageMapper()), + null); + + var retryPolicy = Policy + .Handle() + .Retry(); + + var circuitBreakerPolicy = Policy + .Handle() + .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); + + var producerRegistry = new ProducerRegistry(new Dictionary + { + { routingKey, producer }, + }); + + var policyRegistry = new PolicyRegistry + { + { CommandProcessor.RETRYPOLICY, retryPolicy }, + { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } + }; + + var tracer = new BrighterTracer(timeProvider); + var outbox = new InMemoryOutbox(timeProvider) {Tracer = tracer}; + + IAmAnOutboxProducerMediator bus = new OutboxProducerMediator( + producerRegistry, + policyRegistry, + messageMapperRegistry, + new EmptyMessageTransformerFactory(), + new EmptyMessageTransformerFactoryAsync(), + tracer, + outbox + ); + + CommandProcessor.ClearServiceBus(); + _commandProcessor = new CommandProcessor( + new InMemoryRequestContextFactory(), + policyRegistry, + bus + ); + } + + [Fact] + public async Task When_Scheduling_A_Message_And_There_Is_No_Message_Scheduler_Factory_Async() + { + _exception = await Catch.ExceptionAsync(async () => await _commandProcessor.SchedulerPostAsync(_myCommand, TimeSpan.FromSeconds(1))); + _exception.Should().BeOfType(); + + _exception = await Catch.ExceptionAsync(async () => await _commandProcessor.SchedulerPostAsync(_myCommand, DateTimeOffset.UtcNow.AddSeconds(10))); + _exception.Should().BeOfType(); + } + + public void Dispose() + { + CommandProcessor.ClearServiceBus(); + } +} \ No newline at end of file diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_To_The_Command_Processor.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_To_The_Command_Processor.cs new file mode 100644 index 0000000000..e2472f9d16 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_To_The_Command_Processor.cs @@ -0,0 +1,139 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2015 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using System.Transactions; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Observability; +using Polly; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.CommandProcessors.Scheduler; + +[Collection("CommandProcessor")] +public class CommandProcessorSchedulerCommandTests : IDisposable +{ + private const string Topic = "MyCommand"; + private readonly CommandProcessor _commandProcessor; + private readonly MyCommand _myCommand; + private readonly Message _message; + private readonly InMemoryOutbox _outbox; + private readonly InternalBus _internalBus = new(); + + public CommandProcessorSchedulerCommandTests() + { + _myCommand = new MyCommand { Value = $"Hello World {Guid.NewGuid():N}"}; + + var timeProvider = new FakeTimeProvider(); + var routingKey = new RoutingKey(Topic); + + InMemoryProducer producer = new(_internalBus, timeProvider) {Publication = {Topic = routingKey, RequestType = typeof(MyCommand)}}; + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND), + new MessageBody(JsonSerializer.Serialize(_myCommand, JsonSerialisationOptions.Options)) + ); + + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory((_) => new MyCommandMessageMapper()), + null); + messageMapperRegistry.Register(); + + var retryPolicy = Policy + .Handle() + .Retry(); + + var circuitBreakerPolicy = Policy + .Handle() + .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); + + var policyRegistry = new PolicyRegistry + { + { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } + }; + var producerRegistry = new ProducerRegistry(new Dictionary {{routingKey, producer},}); + + var tracer = new BrighterTracer(timeProvider); + _outbox = new InMemoryOutbox(timeProvider) {Tracer = tracer}; + + IAmAnOutboxProducerMediator bus = new OutboxProducerMediator( + producerRegistry, + policyRegistry, + messageMapperRegistry, + new EmptyMessageTransformerFactory(), + new EmptyMessageTransformerFactoryAsync(), + tracer, + _outbox + ); + + CommandProcessor.ClearServiceBus(); + _commandProcessor = new CommandProcessor( + new InMemoryRequestContextFactory(), + policyRegistry, + bus, + messageSchedulerFactory: new InMemoryMessageSchedulerFactory() + ); + } + + [Fact] + public void When_Scheduling_With_Delay_A_Message_To_The_Command_Processor() + { + _commandProcessor.SchedulerPost(_myCommand, TimeSpan.FromSeconds(1)); + _internalBus.Stream(new RoutingKey(Topic)).Any().Should().BeFalse(); + + Task.Delay(TimeSpan.FromSeconds(2)).GetAwaiter().GetResult(); + _internalBus.Stream(new RoutingKey(Topic)).Any().Should().BeTrue(); + + var message = _outbox.Get(_myCommand.Id, new RequestContext()); + message.Should().NotBeNull(); + message.Should().Be(_message); + } + + [Fact] + public void When_Scheduling_With_At_A_Message_To_The_Command_Processor() + { + _commandProcessor.SchedulerPost(_myCommand, DateTimeOffset.UtcNow.AddSeconds(1)); + _internalBus.Stream(new RoutingKey(Topic)).Any().Should().BeFalse(); + + Task.Delay(TimeSpan.FromSeconds(2)).GetAwaiter().GetResult(); + _internalBus.Stream(new RoutingKey(Topic)).Any().Should().BeTrue(); + + var message = _outbox.Get(_myCommand.Id, new RequestContext()); + message.Should().NotBeNull(); + + message.Should().Be(_message); + } + + public void Dispose() + { + CommandProcessor.ClearServiceBus(); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_To_The_Command_Processor_Async.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_To_The_Command_Processor_Async.cs new file mode 100644 index 0000000000..ce0434f3e3 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_To_The_Command_Processor_Async.cs @@ -0,0 +1,135 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2015 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Transactions; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Observability; +using Polly; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.CommandProcessors.Scheduler; + +[Collection("CommandProcessor")] +public class CommandProcessorSchedulerCommandAsyncTests : IDisposable +{ + private readonly CommandProcessor _commandProcessor; + private readonly MyCommand _myCommand; + private readonly InMemoryOutbox _outbox; + private readonly InternalBus _internalBus = new(); + private readonly RoutingKey _routingKey; + + public CommandProcessorSchedulerCommandAsyncTests() + { + _myCommand = new() { Value = $"Hello World {Guid.NewGuid():N}" }; + _routingKey = new RoutingKey("MyCommand"); + + var timeProvider = new FakeTimeProvider(); + InMemoryProducer producer = new(_internalBus, timeProvider) + { + Publication = { Topic = _routingKey, RequestType = typeof(MyCommand) } + }; + + var messageMapperRegistry = new MessageMapperRegistry( + null, + new SimpleMessageMapperFactoryAsync((_) => new MyCommandMessageMapperAsync()) + ); + messageMapperRegistry.RegisterAsync(); + + var retryPolicy = Policy + .Handle() + .RetryAsync(); + + var circuitBreakerPolicy = Policy + .Handle() + .CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1)); + + var policyRegistry = new PolicyRegistry + { + { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, + { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } + }; + var producerRegistry = + new ProducerRegistry(new Dictionary { { _routingKey, producer }, }); + + var tracer = new BrighterTracer(timeProvider); + _outbox = new InMemoryOutbox(timeProvider) { Tracer = tracer }; + + IAmAnOutboxProducerMediator bus = new OutboxProducerMediator( + producerRegistry, + policyRegistry, + messageMapperRegistry, + new EmptyMessageTransformerFactory(), + new EmptyMessageTransformerFactoryAsync(), + tracer, + _outbox + ); + + CommandProcessor.ClearServiceBus(); + _commandProcessor = new CommandProcessor( + new InMemoryRequestContextFactory(), + policyRegistry, + bus, + messageSchedulerFactory: new InMemoryMessageSchedulerFactory() + ); + } + + [Fact] + public async Task When_Scheduling_With_Delay_A_Message_To_The_Command_Processor_Async() + { + await _commandProcessor.SchedulerPostAsync(_myCommand, TimeSpan.FromSeconds(1)); + _internalBus.Stream(_routingKey).Any().Should().BeFalse(); + await Task.Delay(TimeSpan.FromSeconds(2)); + _internalBus.Stream(_routingKey).Any().Should().BeTrue(); + + _outbox + .Get(_myCommand.Id, new RequestContext()) + .Should().NotBeNull(); + } + + [Fact] + public async Task When_Scheduling_With_At_A_Message_To_The_Command_Processor_Async() + { + await _commandProcessor.SchedulerPostAsync(_myCommand, DateTimeOffset.UtcNow.AddSeconds(1)); + _internalBus.Stream(_routingKey).Any().Should().BeFalse(); + await Task.Delay(TimeSpan.FromSeconds(2)); + _internalBus.Stream(_routingKey).Any().Should().BeTrue(); + + _outbox + .Get(_myCommand.Id, new RequestContext()) + .Should().NotBeNull(); + } + + public void Dispose() + { + CommandProcessor.ClearServiceBus(); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_With_A_Default_Policy.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_With_A_Default_Policy.cs new file mode 100644 index 0000000000..a6392a5280 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_With_A_Default_Policy.cs @@ -0,0 +1,138 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2015 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using System.Transactions; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Observability; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.CommandProcessors.Scheduler; + +[Collection("CommandProcessor")] +public class SchedulerCommandTests : IDisposable +{ + private readonly RoutingKey _routingKey = new("MyCommand"); + private readonly CommandProcessor _commandProcessor; + private readonly MyCommand _myCommand; + private readonly Message _message; + private readonly InMemoryOutbox _outbox; + private readonly InternalBus _internalBus = new(); + + public SchedulerCommandTests() + { + _myCommand = new(){ Value = $"Hello World {Guid.NewGuid():N}"}; + + var timeProvider = new FakeTimeProvider(); + var tracer = new BrighterTracer(timeProvider); + _outbox = new InMemoryOutbox(timeProvider) { Tracer = tracer }; + InMemoryProducer producer = new(_internalBus, timeProvider) + { + Publication = { Topic = _routingKey, RequestType = typeof(MyCommand) } + }; + + _message = new Message( + new MessageHeader(_myCommand.Id, _routingKey, MessageType.MT_COMMAND), + new MessageBody(JsonSerializer.Serialize(_myCommand, JsonSerialisationOptions.Options)) + ); + + var messageMapperRegistry = + new MessageMapperRegistry( + new SimpleMessageMapperFactory((_) => new MyCommandMessageMapper()), + null); + messageMapperRegistry.Register(); + + var producerRegistry = + new ProducerRegistry(new Dictionary { { _routingKey, producer }, }); + + var externalBus = new OutboxProducerMediator( + producerRegistry: producerRegistry, + policyRegistry: new DefaultPolicy(), + mapperRegistry: messageMapperRegistry, + messageTransformerFactory: new EmptyMessageTransformerFactory(), + messageTransformerFactoryAsync: new EmptyMessageTransformerFactoryAsync(), + tracer: tracer, + outbox: _outbox + ); + + _commandProcessor = CommandProcessorBuilder.StartNew() + .Handlers(new HandlerConfiguration(new SubscriberRegistry(), new EmptyHandlerFactorySync())) + .DefaultPolicy() + .ExternalBus(ExternalBusType.FireAndForget, externalBus) + .ConfigureInstrumentation(new BrighterTracer(TimeProvider.System), InstrumentationOptions.All) + .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(new InMemoryMessageSchedulerFactory()) + .Build(); + } + + [Fact] + public void When_Scheduling_With_A_Default_Policy_And_Passing_A_Delay() + { + _commandProcessor.SchedulerPost(_myCommand, TimeSpan.FromSeconds(1)); + _internalBus.Stream(new RoutingKey(_routingKey)).Any().Should().BeFalse(); + + Task.Delay(TimeSpan.FromSeconds(2)).Wait(); + _internalBus.Stream(new RoutingKey(_routingKey)).Any().Should().BeTrue(); + + var message = _outbox.Get(_myCommand.Id, new RequestContext()); + message.Should().NotBeNull(); + message.Should().Be(_message); + } + + [Fact] + public void When_Scheduling_With_A_Default_Policy_And_Passing_An_At() + { + _commandProcessor.SchedulerPost(_myCommand, DateTimeOffset.UtcNow.AddSeconds(1)); + _internalBus.Stream(new RoutingKey(_routingKey)).Any().Should().BeFalse(); + + Task.Delay(TimeSpan.FromSeconds(2)).Wait(); + _internalBus.Stream(new RoutingKey(_routingKey)).Any().Should().BeTrue(); + + var message = _outbox.Get(_myCommand.Id, new RequestContext()); + message.Should().NotBeNull(); + message.Should().Be(_message); + } + + public void Dispose() + { + CommandProcessor.ClearServiceBus(); + } + + internal class EmptyHandlerFactorySync : IAmAHandlerFactorySync + { + public IHandleRequests Create(Type handlerType, IAmALifetime lifetime) + { + return null; + } + + public void Release(IHandleRequests handler, IAmALifetime lifetime) { } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Observability/CommandProcessor/Scheduler/When_Scheduling_A_Request_A_Span_Is_Exported.cs b/tests/Paramore.Brighter.Core.Tests/Observability/CommandProcessor/Scheduler/When_Scheduling_A_Request_A_Span_Is_Exported.cs new file mode 100644 index 0000000000..764196f735 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Observability/CommandProcessor/Scheduler/When_Scheduling_A_Request_A_Span_Is_Exported.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using System.Transactions; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using OpenTelemetry; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using Paramore.Brighter.Core.Tests.CommandProcessors.Post; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Observability; +using Polly; +using Polly.Registry; +using Xunit; +using MyEvent = Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles.MyEvent; + +namespace Paramore.Brighter.Core.Tests.Observability.CommandProcessor.Scheduler; + +[Collection("Observability")] +public class CommandProcessorSchedulerObservabilityTests +{ + private readonly List _exportedActivities; + private readonly TracerProvider _traceProvider; + private readonly Brighter.CommandProcessor _commandProcessor; + + public CommandProcessorSchedulerObservabilityTests() + { + var routingKey = new RoutingKey("MyEvent"); + + var builder = Sdk.CreateTracerProviderBuilder(); + _exportedActivities = new List(); + + _traceProvider = builder + .AddSource("Paramore.Brighter.Tests", "Paramore.Brighter") + .ConfigureResource(r => r.AddService("in-memory-tracer")) + .AddInMemoryExporter(_exportedActivities) + .Build(); + + Brighter.CommandProcessor.ClearServiceBus(); + + var registry = new SubscriberRegistry(); + + var handlerFactory = new PostCommandTests.EmptyHandlerFactorySync(); + + var retryPolicy = Policy + .Handle() + .Retry(); + + var policyRegistry = new PolicyRegistry { { Brighter.CommandProcessor.RETRYPOLICY, retryPolicy } }; + + var timeProvider = new FakeTimeProvider(); + var tracer = new BrighterTracer(timeProvider); + InMemoryOutbox outbox = new(timeProvider) { Tracer = tracer }; + + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory((_) => new MyEventMessageMapper()), + null); + messageMapperRegistry.Register(); + + var producerRegistry = new ProducerRegistry(new Dictionary + { + { + routingKey, + new InMemoryProducer(new InternalBus(), new FakeTimeProvider()) + { + Publication = { Topic = routingKey, RequestType = typeof(MyEvent) } + } + } + }); + + IAmAnOutboxProducerMediator bus = new OutboxProducerMediator( + producerRegistry, + policyRegistry, + messageMapperRegistry, + new EmptyMessageTransformerFactory(), + new EmptyMessageTransformerFactoryAsync(), + tracer, + outbox, + maxOutStandingMessages: -1 + ); + + _commandProcessor = new Brighter.CommandProcessor( + registry, + handlerFactory, + new InMemoryRequestContextFactory(), + policyRegistry, + bus, + tracer: tracer, + instrumentationOptions: InstrumentationOptions.All, + messageSchedulerFactory: new InMemoryMessageSchedulerFactory() + ); + } + + [Fact] + public void When_Scheduling_A_Request_With_A_Delay_A_Span_Is_Exported() + { + //arrange + var parentActivity = new ActivitySource("Paramore.Brighter.Tests").StartActivity("BrighterTracerSpanTests"); + + var @event = new MyEvent(); + var context = new RequestContext { Span = parentActivity }; + + //act + _commandProcessor.SchedulerPost(@event, TimeSpan.FromSeconds(5), context); + parentActivity?.Stop(); + + _traceProvider.ForceFlush(); + + //assert + _exportedActivities.Count.Should().Be(2); + _exportedActivities.Any(a => a.Source.Name == "Paramore.Brighter").Should().BeTrue(); + var depositActivity = _exportedActivities.Single(a => + a.DisplayName == $"{nameof(MyEvent)} {CommandProcessorSpanOperation.Scheduler.ToSpanName()}"); + depositActivity.Should().NotBeNull(); + depositActivity.ParentId.Should().Be(parentActivity?.Id); + + depositActivity.Tags.Any(t => t.Key == BrighterSemanticConventions.RequestId && t.Value == @event.Id).Should() + .BeTrue(); + depositActivity.Tags.Any(t => t is { Key: BrighterSemanticConventions.RequestType, Value: nameof(MyEvent) }) + .Should().BeTrue(); + depositActivity.Tags + .Any(t => t.Key == BrighterSemanticConventions.RequestBody && + t.Value == JsonSerializer.Serialize(@event, JsonSerialisationOptions.Options)).Should().BeTrue(); + depositActivity.Tags.Any(t => t is { Key: BrighterSemanticConventions.Operation, Value: "scheduler" }).Should() + .BeTrue(); + + var events = depositActivity.Events.ToList(); + events.Count.Should().Be(1); + + //mapping a message should be an event + var mapperEvent = events.Single(e => e.Name == $"{nameof(MyEventMessageMapper)}"); + mapperEvent.Tags + .Any(a => a.Key == BrighterSemanticConventions.MapperName && + (string)a.Value == nameof(MyEventMessageMapper)).Should().BeTrue(); + mapperEvent.Tags.Any(a => a.Key == BrighterSemanticConventions.MapperType && (string)a.Value == "sync").Should() + .BeTrue(); + } + + [Fact] + public void When_Scheduling_A_Request_With_A_DateTime_A_Span_Is_Exported() + { + //arrange + var parentActivity = new ActivitySource("Paramore.Brighter.Tests").StartActivity("BrighterTracerSpanTests"); + + var @event = new MyEvent(); + var context = new RequestContext { Span = parentActivity }; + + //act + _commandProcessor.SchedulerPost(@event, DateTimeOffset.UtcNow.AddSeconds(5), context); + parentActivity?.Stop(); + + _traceProvider.ForceFlush(); + + //assert + _exportedActivities.Count.Should().Be(2); + _exportedActivities.Any(a => a.Source.Name == "Paramore.Brighter").Should().BeTrue(); + var depositActivity = _exportedActivities.Single(a => + a.DisplayName == $"{nameof(MyEvent)} {CommandProcessorSpanOperation.Scheduler.ToSpanName()}"); + depositActivity.Should().NotBeNull(); + depositActivity.ParentId.Should().Be(parentActivity?.Id); + + depositActivity.Tags.Any(t => t.Key == BrighterSemanticConventions.RequestId && t.Value == @event.Id).Should() + .BeTrue(); + depositActivity.Tags.Any(t => t is { Key: BrighterSemanticConventions.RequestType, Value: nameof(MyEvent) }) + .Should().BeTrue(); + depositActivity.Tags + .Any(t => t.Key == BrighterSemanticConventions.RequestBody && + t.Value == JsonSerializer.Serialize(@event, JsonSerialisationOptions.Options)).Should().BeTrue(); + depositActivity.Tags.Any(t => t is { Key: BrighterSemanticConventions.Operation, Value: "scheduler" }).Should() + .BeTrue(); + + var events = depositActivity.Events.ToList(); + events.Count.Should().Be(1); + + //mapping a message should be an event + var mapperEvent = events.Single(e => e.Name == $"{nameof(MyEventMessageMapper)}"); + mapperEvent.Tags + .Any(a => a.Key == BrighterSemanticConventions.MapperName && + (string)a.Value == nameof(MyEventMessageMapper)).Should().BeTrue(); + mapperEvent.Tags.Any(a => a.Key == BrighterSemanticConventions.MapperType && (string)a.Value == "sync").Should() + .BeTrue(); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Observability/CommandProcessor/Scheduler/When_Scheduling_A_Request_A_Span_Is_Exported_Async.cs b/tests/Paramore.Brighter.Core.Tests/Observability/CommandProcessor/Scheduler/When_Scheduling_A_Request_A_Span_Is_Exported_Async.cs new file mode 100644 index 0000000000..4a76f3127b --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Observability/CommandProcessor/Scheduler/When_Scheduling_A_Request_A_Span_Is_Exported_Async.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using System.Transactions; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using OpenTelemetry; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using Paramore.Brighter.Core.Tests.CommandProcessors.Post; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Observability; +using Polly; +using Polly.Registry; +using Xunit; +using MyEvent = Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles.MyEvent; + +namespace Paramore.Brighter.Core.Tests.Observability.CommandProcessor.Scheduler; + +[Collection("Observability")] +public class CommandProcessorSchedulerAsyncObservabilityTests +{ + private readonly List _exportedActivities; + private readonly TracerProvider _traceProvider; + private readonly Brighter.CommandProcessor _commandProcessor; + + public CommandProcessorSchedulerAsyncObservabilityTests() + { + var routingKey = new RoutingKey("MyEvent"); + + var builder = Sdk.CreateTracerProviderBuilder(); + _exportedActivities = new List(); + + _traceProvider = builder + .AddSource("Paramore.Brighter.Tests", "Paramore.Brighter") + .ConfigureResource(r => r.AddService("in-memory-tracer")) + .AddInMemoryExporter(_exportedActivities) + .Build(); + + Brighter.CommandProcessor.ClearServiceBus(); + + var registry = new SubscriberRegistry(); + + var handlerFactory = new PostCommandTests.EmptyHandlerFactorySync(); + + var retryPolicy = Policy + .Handle() + .Retry(); + + var policyRegistry = new PolicyRegistry { { Brighter.CommandProcessor.RETRYPOLICY, retryPolicy } }; + + var timeProvider = new FakeTimeProvider(); + var tracer = new BrighterTracer(timeProvider); + InMemoryOutbox outbox = new(timeProvider) { Tracer = tracer }; + + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory((_) => new MyEventMessageMapper()), + null); + messageMapperRegistry.Register(); + + var producerRegistry = new ProducerRegistry(new Dictionary + { + { + routingKey, + new InMemoryProducer(new InternalBus(), new FakeTimeProvider()) + { + Publication = { Topic = routingKey, RequestType = typeof(MyEvent) } + } + } + }); + + IAmAnOutboxProducerMediator bus = new OutboxProducerMediator( + producerRegistry, + policyRegistry, + messageMapperRegistry, + new EmptyMessageTransformerFactory(), + new EmptyMessageTransformerFactoryAsync(), + tracer, + outbox, + maxOutStandingMessages: -1 + ); + + _commandProcessor = new Brighter.CommandProcessor( + registry, + handlerFactory, + new InMemoryRequestContextFactory(), + policyRegistry, + bus, + tracer: tracer, + instrumentationOptions: InstrumentationOptions.All, + messageSchedulerFactory: new InMemoryMessageSchedulerFactory() + ); + } + + [Fact] + public async Task When_Scheduling_A_Request_With_A_Delay_A_Span_Is_Exported_Async() + { + //arrange + var parentActivity = new ActivitySource("Paramore.Brighter.Tests").StartActivity("BrighterTracerSpanTests"); + + var @event = new MyEvent(); + var context = new RequestContext { Span = parentActivity }; + + //act + await _commandProcessor.SchedulerPostAsync(@event, TimeSpan.FromSeconds(5), context); + parentActivity?.Stop(); + + _traceProvider.ForceFlush(); + + //assert + _exportedActivities.Count.Should().Be(2); + _exportedActivities.Any(a => a.Source.Name == "Paramore.Brighter").Should().BeTrue(); + var depositActivity = _exportedActivities.Single(a => + a.DisplayName == $"{nameof(MyEvent)} {CommandProcessorSpanOperation.Scheduler.ToSpanName()}"); + depositActivity.Should().NotBeNull(); + depositActivity.ParentId.Should().Be(parentActivity?.Id); + + depositActivity.Tags.Any(t => t.Key == BrighterSemanticConventions.RequestId && t.Value == @event.Id).Should() + .BeTrue(); + depositActivity.Tags.Any(t => t is { Key: BrighterSemanticConventions.RequestType, Value: nameof(MyEvent) }) + .Should().BeTrue(); + depositActivity.Tags + .Any(t => t.Key == BrighterSemanticConventions.RequestBody && + t.Value == JsonSerializer.Serialize(@event, JsonSerialisationOptions.Options)).Should().BeTrue(); + depositActivity.Tags.Any(t => t is { Key: BrighterSemanticConventions.Operation, Value: "scheduler" }).Should() + .BeTrue(); + + var events = depositActivity.Events.ToList(); + events.Count.Should().Be(1); + + //mapping a message should be an event + var mapperEvent = events.Single(e => e.Name == $"{nameof(MyEventMessageMapper)}"); + mapperEvent.Tags + .Any(a => a.Key == BrighterSemanticConventions.MapperName && + (string)a.Value == nameof(MyEventMessageMapper)).Should().BeTrue(); + mapperEvent.Tags.Any(a => a.Key == BrighterSemanticConventions.MapperType && (string)a.Value == "sync").Should() + .BeTrue(); + } + + [Fact] + public async Task When_Scheduling_A_Request_With_A_DateTime_A_Span_Is_Exported_Async() + { + //arrange + var parentActivity = new ActivitySource("Paramore.Brighter.Tests").StartActivity("BrighterTracerSpanTests"); + + var @event = new MyEvent(); + var context = new RequestContext { Span = parentActivity }; + + //act + await _commandProcessor.SchedulerPostAsync(@event, DateTimeOffset.UtcNow.AddSeconds(5), context); + parentActivity?.Stop(); + + _traceProvider.ForceFlush(); + + //assert + _exportedActivities.Count.Should().Be(2); + _exportedActivities.Any(a => a.Source.Name == "Paramore.Brighter").Should().BeTrue(); + var depositActivity = _exportedActivities.Single(a => + a.DisplayName == $"{nameof(MyEvent)} {CommandProcessorSpanOperation.Scheduler.ToSpanName()}"); + depositActivity.Should().NotBeNull(); + depositActivity.ParentId.Should().Be(parentActivity?.Id); + + depositActivity.Tags.Any(t => t.Key == BrighterSemanticConventions.RequestId && t.Value == @event.Id).Should() + .BeTrue(); + depositActivity.Tags.Any(t => t is { Key: BrighterSemanticConventions.RequestType, Value: nameof(MyEvent) }) + .Should().BeTrue(); + depositActivity.Tags + .Any(t => t.Key == BrighterSemanticConventions.RequestBody && + t.Value == JsonSerializer.Serialize(@event, JsonSerialisationOptions.Options)).Should().BeTrue(); + depositActivity.Tags.Any(t => t is { Key: BrighterSemanticConventions.Operation, Value: "scheduler" }).Should() + .BeTrue(); + + var events = depositActivity.Events.ToList(); + events.Count.Should().Be(1); + + //mapping a message should be an event + var mapperEvent = events.Single(e => e.Name == $"{nameof(MyEventMessageMapper)}"); + mapperEvent.Tags + .Any(a => a.Key == BrighterSemanticConventions.MapperName && + (string)a.Value == nameof(MyEventMessageMapper)).Should().BeTrue(); + mapperEvent.Tags.Any(a => a.Key == BrighterSemanticConventions.MapperType && (string)a.Value == "sync").Should() + .BeTrue(); + } +} From 0bed01eb4deb3ec1c6a85efc565586a329343c4e Mon Sep 17 00:00:00 2001 From: Rafael Andrade Date: Tue, 4 Feb 2025 16:42:08 +0000 Subject: [PATCH 11/17] Remove Hangfire and add Quartz --- Brighter.sln | 41 +++-- Directory.Packages.props | 3 +- .../AWSTaskQueue/GreetingsSender/Program.cs | 15 -- .../RMQTaskQueue/GreetingsSender/Program.cs | 15 -- .../HangfireMessageScheduler.cs | 77 --------- .../HangfireMessageSchedulerFactory.cs | 13 -- ...e.Brighter.MessageScheduler.Quartz.csproj} | 2 +- .../QuartzBrighterJob.cs | 36 ++++ .../QuartzMessageScheduler.cs | 105 ++++++++++++ .../QuartzMessageSchedulerFactory.cs | 23 +++ .../ParamoreBrighter.Quartz.Tests.csproj | 37 +++++ .../QuartzMessageSchedulerTest.cs | 154 ++++++++++++++++++ 12 files changed, 386 insertions(+), 135 deletions(-) delete mode 100644 src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageScheduler.cs delete mode 100644 src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageSchedulerFactory.cs rename src/{Paramore.Brighter.MessageScheduler.Hangfire/Paramore.Brighter.MessageScheduler.Hangfire.csproj => Paramore.Brighter.MessageScheduler.Quartz/Paramore.Brighter.MessageScheduler.Quartz.csproj} (88%) create mode 100644 src/Paramore.Brighter.MessageScheduler.Quartz/QuartzBrighterJob.cs create mode 100644 src/Paramore.Brighter.MessageScheduler.Quartz/QuartzMessageScheduler.cs create mode 100644 src/Paramore.Brighter.MessageScheduler.Quartz/QuartzMessageSchedulerFactory.cs create mode 100644 tests/ParamoreBrighter.Quartz.Tests/ParamoreBrighter.Quartz.Tests.csproj create mode 100644 tests/ParamoreBrighter.Quartz.Tests/QuartzMessageSchedulerTest.cs diff --git a/Brighter.sln b/Brighter.sln index 08cda26753..a14400d442 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -315,7 +315,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Salutation_Sweeper", "sampl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Locking.MsSql", "src\Paramore.Brighter.Locking.MsSql\Paramore.Brighter.Locking.MsSql.csproj", "{758EE237-C722-4A0A-908C-2D08C1E59025}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.MessageScheduler.Hangfire", "src\Paramore.Brighter.MessageScheduler.Hangfire\Paramore.Brighter.MessageScheduler.Hangfire.csproj", "{BF515169-0C70-4027-946F-89686BC552FE}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.MessageScheduler.Quartz", "src\Paramore.Brighter.MessageScheduler.Quartz\Paramore.Brighter.MessageScheduler.Quartz.csproj", "{097CB927-EC20-413A-85C9-61E6380814CA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParamoreBrighter.Quartz.Tests", "tests\ParamoreBrighter.Quartz.Tests\ParamoreBrighter.Quartz.Tests.csproj", "{4469AEE3-B460-4948-A0A5-B9480EE70EA4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -1767,18 +1769,30 @@ Global {758EE237-C722-4A0A-908C-2D08C1E59025}.Release|Mixed Platforms.Build.0 = Release|Any CPU {758EE237-C722-4A0A-908C-2D08C1E59025}.Release|x86.ActiveCfg = Release|Any CPU {758EE237-C722-4A0A-908C-2D08C1E59025}.Release|x86.Build.0 = Release|Any CPU - {BF515169-0C70-4027-946F-89686BC552FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BF515169-0C70-4027-946F-89686BC552FE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BF515169-0C70-4027-946F-89686BC552FE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {BF515169-0C70-4027-946F-89686BC552FE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {BF515169-0C70-4027-946F-89686BC552FE}.Debug|x86.ActiveCfg = Debug|Any CPU - {BF515169-0C70-4027-946F-89686BC552FE}.Debug|x86.Build.0 = Debug|Any CPU - {BF515169-0C70-4027-946F-89686BC552FE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BF515169-0C70-4027-946F-89686BC552FE}.Release|Any CPU.Build.0 = Release|Any CPU - {BF515169-0C70-4027-946F-89686BC552FE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {BF515169-0C70-4027-946F-89686BC552FE}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {BF515169-0C70-4027-946F-89686BC552FE}.Release|x86.ActiveCfg = Release|Any CPU - {BF515169-0C70-4027-946F-89686BC552FE}.Release|x86.Build.0 = Release|Any CPU + {097CB927-EC20-413A-85C9-61E6380814CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {097CB927-EC20-413A-85C9-61E6380814CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {097CB927-EC20-413A-85C9-61E6380814CA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {097CB927-EC20-413A-85C9-61E6380814CA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {097CB927-EC20-413A-85C9-61E6380814CA}.Debug|x86.ActiveCfg = Debug|Any CPU + {097CB927-EC20-413A-85C9-61E6380814CA}.Debug|x86.Build.0 = Debug|Any CPU + {097CB927-EC20-413A-85C9-61E6380814CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {097CB927-EC20-413A-85C9-61E6380814CA}.Release|Any CPU.Build.0 = Release|Any CPU + {097CB927-EC20-413A-85C9-61E6380814CA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {097CB927-EC20-413A-85C9-61E6380814CA}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {097CB927-EC20-413A-85C9-61E6380814CA}.Release|x86.ActiveCfg = Release|Any CPU + {097CB927-EC20-413A-85C9-61E6380814CA}.Release|x86.Build.0 = Release|Any CPU + {4469AEE3-B460-4948-A0A5-B9480EE70EA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4469AEE3-B460-4948-A0A5-B9480EE70EA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4469AEE3-B460-4948-A0A5-B9480EE70EA4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4469AEE3-B460-4948-A0A5-B9480EE70EA4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {4469AEE3-B460-4948-A0A5-B9480EE70EA4}.Debug|x86.ActiveCfg = Debug|Any CPU + {4469AEE3-B460-4948-A0A5-B9480EE70EA4}.Debug|x86.Build.0 = Debug|Any CPU + {4469AEE3-B460-4948-A0A5-B9480EE70EA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4469AEE3-B460-4948-A0A5-B9480EE70EA4}.Release|Any CPU.Build.0 = Release|Any CPU + {4469AEE3-B460-4948-A0A5-B9480EE70EA4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {4469AEE3-B460-4948-A0A5-B9480EE70EA4}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {4469AEE3-B460-4948-A0A5-B9480EE70EA4}.Release|x86.ActiveCfg = Release|Any CPU + {4469AEE3-B460-4948-A0A5-B9480EE70EA4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1876,6 +1890,7 @@ Global {915BD9FD-2E29-4E2B-8DA3-A74055F32A20} = {C6B17EFD-4F05-4D45-AF3E-C4F3F790B994} {7D8CE752-CCBB-4868-ADF0-30FF94CA611C} = {11935469-A062-4CFF-9F72-F4F41E14C2B4} {FBAF452E-C0AB-4C4B-9A81-F1ED9616DE2A} = {202BA107-89D5-4868-AC5A-3527114C0109} + {4469AEE3-B460-4948-A0A5-B9480EE70EA4} = {329736D2-BF92-4D06-A7BF-19F4B6B64EDD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8B7C7E31-2E32-4E0D-9426-BC9AF22E9F4C} diff --git a/Directory.Packages.props b/Directory.Packages.props index 746b73b62c..7473c736af 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -32,7 +32,6 @@ - @@ -79,6 +78,8 @@ + + diff --git a/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs b/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs index a12c4f8446..961ac3e14d 100644 --- a/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs +++ b/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs @@ -87,7 +87,6 @@ static async Task Main(string[] args) serviceCollection .AddBrighter() - .UseMessageScheduler(new InMemoryMessageSchedulerFactory()) .UseExternalBus(configure => { configure.ProducerRegistry = producerRegistry; @@ -99,20 +98,6 @@ static async Task Main(string[] args) var commandProcessor = serviceProvider.GetRequiredService(); commandProcessor.Post(new GreetingEvent("Ian says: Hi there!")); - - // TODO Remove this code: - while (true) - { - Console.WriteLine("Enter a name to greet (Q to quit):"); - var name = Console.ReadLine(); - if (name is "Q" or "q") - { - break; - } - - commandProcessor.SchedulerPost(new GreetingEvent($"Ian says: Hi {name}"), TimeSpan.FromSeconds(10)); - } - commandProcessor.Post(new FarewellEvent("Ian says: See you later!")); } } diff --git a/samples/TaskQueue/RMQTaskQueue/GreetingsSender/Program.cs b/samples/TaskQueue/RMQTaskQueue/GreetingsSender/Program.cs index 61ea97e52e..a8ed45bd41 100644 --- a/samples/TaskQueue/RMQTaskQueue/GreetingsSender/Program.cs +++ b/samples/TaskQueue/RMQTaskQueue/GreetingsSender/Program.cs @@ -82,7 +82,6 @@ static void Main(string[] args) configure.MaxOutStandingMessages = 5; configure.MaxOutStandingCheckInterval = TimeSpan.FromMilliseconds(500); }) - .UseMessageScheduler(new InMemoryMessageSchedulerFactory()) .AutoFromAssemblies(); var serviceProvider = serviceCollection.BuildServiceProvider(); @@ -90,20 +89,6 @@ static void Main(string[] args) var commandProcessor = serviceProvider.GetService(); commandProcessor.Post(new GreetingEvent("Ian says: Hi there!")); - - // TODO Remove this code: - while (true) - { - Console.WriteLine("Enter a name to greet (Q to quit):"); - var name = Console.ReadLine(); - if (name is "Q" or "q") - { - break; - } - - commandProcessor.SchedulerPost(new GreetingEvent($"Ian says: Hi {name}"), TimeSpan.FromSeconds(60)); - } - commandProcessor.Post(new FarewellEvent("Ian says: See you later!")); } } diff --git a/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageScheduler.cs b/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageScheduler.cs deleted file mode 100644 index d280b6da59..0000000000 --- a/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageScheduler.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Hangfire; -using Paramore.Brighter.Scheduler.Events; - -namespace Paramore.Brighter.MessageScheduler.Hangfire; - -/// -/// The Hangfire adaptor for . -/// -/// -/// -public class HangfireMessageScheduler( - IAmACommandProcessor processor, - IBackgroundJobClientV2 client, - string? queue) : IAmAMessageSchedulerSync, IAmAMessageSchedulerAsync -{ - /// - public string Schedule(Message message, DateTimeOffset at) - => client.Schedule(queue, () => ConsumeAsync(message, false), at); - - /// - public string Schedule(Message message, TimeSpan delay) - => client.Schedule(queue, () => ConsumeAsync(message, true), delay); - - /// - public bool ReScheduler(string schedulerId, DateTimeOffset at) => client.Reschedule(schedulerId, at); - - /// - public bool ReScheduler(string schedulerId, TimeSpan delay) => client.Reschedule(schedulerId, delay); - - /// - public void Cancel(string id) => client.Delete(queue, id); - - private async Task ConsumeAsync(Message message, bool async) - { - if (async) - { - await processor.PostAsync(new FireSchedulerMessage { Id = message.Id, Message = message }); - } - else - { - processor.Post(new FireSchedulerMessage { Id = message.Id, Message = message }); - } - } - - /// - public Task ScheduleAsync(Message message, DateTimeOffset at, CancellationToken cancellationToken = default) - => Task.FromResult(Schedule(message, at)); - - /// - public Task ScheduleAsync(Message message, TimeSpan delay, CancellationToken cancellationToken = default) - => Task.FromResult(Schedule(message, delay)); - - /// - public Task ReSchedulerAsync(string schedulerId, DateTimeOffset at, - CancellationToken cancellationToken = default) - => Task.FromResult(ReScheduler(schedulerId, at)); - - /// - public Task ReSchedulerAsync(string schedulerId, TimeSpan delay, - CancellationToken cancellationToken = default) - => Task.FromResult(ReScheduler(schedulerId, delay)); - - /// - public Task CancelAsync(string id, CancellationToken cancellationToken = default) - { - Cancel(id); - return Task.CompletedTask; - } - - /// - public ValueTask DisposeAsync() => new(); - - /// - public void Dispose() - { - } -} diff --git a/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageSchedulerFactory.cs b/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageSchedulerFactory.cs deleted file mode 100644 index 7080112ad9..0000000000 --- a/src/Paramore.Brighter.MessageScheduler.Hangfire/HangfireMessageSchedulerFactory.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Hangfire; - -namespace Paramore.Brighter.MessageScheduler.Hangfire; - -/// -/// The factory -/// -public class HangfireMessageSchedulerFactory(IBackgroundJobClientV2 client, string? queue) : IAmAMessageSchedulerFactory -{ - /// - public IAmAMessageScheduler Create(IAmACommandProcessor processor) - => new HangfireMessageScheduler(processor, client, queue); -} diff --git a/src/Paramore.Brighter.MessageScheduler.Hangfire/Paramore.Brighter.MessageScheduler.Hangfire.csproj b/src/Paramore.Brighter.MessageScheduler.Quartz/Paramore.Brighter.MessageScheduler.Quartz.csproj similarity index 88% rename from src/Paramore.Brighter.MessageScheduler.Hangfire/Paramore.Brighter.MessageScheduler.Hangfire.csproj rename to src/Paramore.Brighter.MessageScheduler.Quartz/Paramore.Brighter.MessageScheduler.Quartz.csproj index a16bd42cc6..7a8039ae0a 100644 --- a/src/Paramore.Brighter.MessageScheduler.Hangfire/Paramore.Brighter.MessageScheduler.Hangfire.csproj +++ b/src/Paramore.Brighter.MessageScheduler.Quartz/Paramore.Brighter.MessageScheduler.Quartz.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Paramore.Brighter.MessageScheduler.Quartz/QuartzBrighterJob.cs b/src/Paramore.Brighter.MessageScheduler.Quartz/QuartzBrighterJob.cs new file mode 100644 index 0000000000..00e79ba556 --- /dev/null +++ b/src/Paramore.Brighter.MessageScheduler.Quartz/QuartzBrighterJob.cs @@ -0,0 +1,36 @@ +using System.Text.Json; +using Paramore.Brighter.Scheduler.Events; +using Quartz; + +namespace Paramore.Brighter.MessageScheduler.Quartz; + +/// +/// The Quartz Message scheduler Job +/// +/// +public class QuartzBrighterJob(IAmACommandProcessor processor) : IJob +{ + public async Task Execute(IJobExecutionContext context) + { + if (!context.JobDetail.JobDataMap.TryGetString("message", out var obj)) + { + return; + } + + if (!context.JobDetail.JobDataMap.TryGetBooleanValue("async", out var async)) + { + return; + } + + var id = context.JobDetail.Key.Name; + var message = JsonSerializer.Deserialize(obj!, JsonSerialisationOptions.Options)!; + if (async) + { + await processor.PostAsync(new FireSchedulerMessage { Id = id, Message = message }); + } + else + { + processor.Post(new FireSchedulerMessage { Id = id, Message = message }); + } + } +} diff --git a/src/Paramore.Brighter.MessageScheduler.Quartz/QuartzMessageScheduler.cs b/src/Paramore.Brighter.MessageScheduler.Quartz/QuartzMessageScheduler.cs new file mode 100644 index 0000000000..8c70a7e323 --- /dev/null +++ b/src/Paramore.Brighter.MessageScheduler.Quartz/QuartzMessageScheduler.cs @@ -0,0 +1,105 @@ +using Paramore.Brighter.Tasks; +using Quartz; +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace Paramore.Brighter.MessageScheduler.Quartz; + +/// +/// The Quartz Message scheduler +/// +/// +/// +/// +public class QuartzMessageScheduler(IScheduler scheduler, string? group, Func getOrCreateSchedulerId) + : IAmAMessageSchedulerSync, IAmAMessageSchedulerAsync +{ + /// + public string Schedule(Message message, DateTimeOffset at) + { + var id = getOrCreateSchedulerId(message); + var job = JobBuilder.Create() + .WithIdentity(getOrCreateSchedulerId(message), group!) + .UsingJobData("message", JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)) + .UsingJobData("async", false) + .Build(); + + var trigger = TriggerBuilder.Create() + .WithIdentity(getOrCreateSchedulerId(message) + "-trigger", group!) + .StartAt(at) + .Build(); + + BrighterAsyncContext.Run(async () => await scheduler.ScheduleJob(job, trigger)); + return id; + } + + /// + public string Schedule(Message message, TimeSpan delay) => Schedule(message, DateTimeOffset.Now.Add(delay)); + + /// + public bool ReScheduler(string schedulerId, DateTimeOffset at) + => BrighterAsyncContext.Run(async () => await ReSchedulerAsync(schedulerId, at)); + + /// + public bool ReScheduler(string schedulerId, TimeSpan delay) => + ReScheduler(schedulerId, DateTimeOffset.Now.Add(delay)); + + /// + public void Cancel(string id) + => BrighterAsyncContext.Run(async () => await CancelAsync(id)); + + /// + public async Task ScheduleAsync(Message message, DateTimeOffset at, + CancellationToken cancellationToken = default) + { + var id = getOrCreateSchedulerId(message); + var job = JobBuilder.Create() + .WithIdentity(getOrCreateSchedulerId(message), group!) + .UsingJobData("message", JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)) + .UsingJobData("async", true) + .Build(); + + var trigger = TriggerBuilder.Create() + .WithIdentity(getOrCreateSchedulerId(message) + "-trigger", group!) + .StartAt(at) + .Build(); + + await scheduler.ScheduleJob(job, trigger, cancellationToken); + return id; + } + + /// + public async Task ScheduleAsync(Message message, TimeSpan delay, + CancellationToken cancellationToken = default) + => await ScheduleAsync(message, DateTimeOffset.Now.Add(delay), cancellationToken); + + /// + public async Task ReSchedulerAsync(string schedulerId, DateTimeOffset at, + CancellationToken cancellationToken = default) + { + var date = await scheduler.RescheduleJob(new TriggerKey(schedulerId + "-trigger", group!), TriggerBuilder + .Create() + .WithIdentity(schedulerId + "-trigger", group!) + .StartAt(at) + .Build(), cancellationToken); + + return date != null; + } + + /// + public async Task ReSchedulerAsync(string schedulerId, TimeSpan delay, + CancellationToken cancellationToken = default) => + await ReSchedulerAsync(schedulerId, DateTimeOffset.Now.Add(delay), cancellationToken); + + /// + public async Task CancelAsync(string id, CancellationToken cancellationToken = default) + => await scheduler.DeleteJob(new JobKey(id, group!), cancellationToken); + + + /// + public ValueTask DisposeAsync() => new(); + + /// + public void Dispose() + { + } +} diff --git a/src/Paramore.Brighter.MessageScheduler.Quartz/QuartzMessageSchedulerFactory.cs b/src/Paramore.Brighter.MessageScheduler.Quartz/QuartzMessageSchedulerFactory.cs new file mode 100644 index 0000000000..db292ef22e --- /dev/null +++ b/src/Paramore.Brighter.MessageScheduler.Quartz/QuartzMessageSchedulerFactory.cs @@ -0,0 +1,23 @@ +using Quartz; + +namespace Paramore.Brighter.MessageScheduler.Quartz; + +/// +/// The factory +/// +public class QuartzMessageSchedulerFactory(IScheduler scheduler) : IAmAMessageSchedulerFactory +{ + /// + /// The Quartz Group. + /// + public string? Group { get; set; } + + /// + /// Get or create scheduler + /// + public Func GetOrCreateSchedulerId { get; set; } = message => message.Id; + + /// + public IAmAMessageScheduler Create(IAmACommandProcessor processor) + => new QuartzMessageScheduler(scheduler, Group, GetOrCreateSchedulerId); +} diff --git a/tests/ParamoreBrighter.Quartz.Tests/ParamoreBrighter.Quartz.Tests.csproj b/tests/ParamoreBrighter.Quartz.Tests/ParamoreBrighter.Quartz.Tests.csproj new file mode 100644 index 0000000000..3b42df7916 --- /dev/null +++ b/tests/ParamoreBrighter.Quartz.Tests/ParamoreBrighter.Quartz.Tests.csproj @@ -0,0 +1,37 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/tests/ParamoreBrighter.Quartz.Tests/QuartzMessageSchedulerTest.cs b/tests/ParamoreBrighter.Quartz.Tests/QuartzMessageSchedulerTest.cs new file mode 100644 index 0000000000..61149a75c5 --- /dev/null +++ b/tests/ParamoreBrighter.Quartz.Tests/QuartzMessageSchedulerTest.cs @@ -0,0 +1,154 @@ +using System.Collections.Specialized; +using System.Text.Json; +using System.Transactions; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter; +using Paramore.Brighter.Extensions; +using Paramore.Brighter.MessageScheduler.Quartz; +using Paramore.Brighter.Observability; +using Quartz; +using Quartz.Impl; +using Quartz.Simpl; +using Quartz.Spi; + +namespace ParamoreBrighter.Quartz.Tests; + +public class QuartzMessageSchedulerTest +{ + private readonly RoutingKey _routingKey = new("MyCommand"); + private readonly CommandProcessor _commandProcessor; + private readonly MyCommand _myCommand; + private readonly Message _message; + private readonly InMemoryOutbox _outbox; + private readonly InternalBus _internalBus = new(); + private readonly StdSchedulerFactory _schedulerFactory; + + public QuartzMessageSchedulerTest() + { + _myCommand = new() { Value = $"Hello World {Guid.NewGuid():N}" }; + + var timeProvider = new FakeTimeProvider(); + var tracer = new BrighterTracer(timeProvider); + _outbox = new InMemoryOutbox(timeProvider) { Tracer = tracer }; + InMemoryProducer producer = new(_internalBus, timeProvider) + { + Publication = { Topic = _routingKey, RequestType = typeof(MyCommand) } + }; + + _message = new Message( + new MessageHeader(_myCommand.Id, _routingKey, MessageType.MT_COMMAND), + new MessageBody(JsonSerializer.Serialize(_myCommand, JsonSerialisationOptions.Options)) + ); + + var messageMapperRegistry = + new MessageMapperRegistry( + new SimpleMessageMapperFactory((_) => new MyCommandMessageMapper()), + null); + messageMapperRegistry.Register(); + + var producerRegistry = + new ProducerRegistry(new Dictionary { { _routingKey, producer }, }); + + var externalBus = new OutboxProducerMediator( + producerRegistry: producerRegistry, + policyRegistry: new DefaultPolicy(), + mapperRegistry: messageMapperRegistry, + messageTransformerFactory: new EmptyMessageTransformerFactory(), + messageTransformerFactoryAsync: new EmptyMessageTransformerFactoryAsync(), + tracer: tracer, + outbox: _outbox + ); + + _schedulerFactory = SchedulerBuilder.Create(new NameValueCollection()) + .UseDefaultThreadPool(x => x.MaxConcurrency = 5) + .UseJobFactory() + .Build(); + + var scheduler = _schedulerFactory.GetScheduler().GetAwaiter().GetResult(); + scheduler.Start().GetAwaiter().GetResult(); + + _commandProcessor = CommandProcessorBuilder.StartNew() + .Handlers(new HandlerConfiguration(new SubscriberRegistry(), new EmptyHandlerFactorySync())) + .DefaultPolicy() + .ExternalBus(ExternalBusType.FireAndForget, externalBus) + .ConfigureInstrumentation(new BrighterTracer(TimeProvider.System), InstrumentationOptions.All) + .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(new QuartzMessageSchedulerFactory(scheduler)) + .Build(); + + BrighterResolver.Processor = _commandProcessor; + } + + [Fact] + public void Quartz_Scheduling_A_Message() + { + _commandProcessor.SchedulerPost(_myCommand, TimeSpan.FromSeconds(1)); + + Task.Delay(TimeSpan.FromSeconds(2)).Wait(); + + _outbox.Get(_myCommand.Id, new RequestContext()).Should().NotBeNull(); + } + + [Fact] + public async Task Scheduling_A_Message_Async() + { + _commandProcessor.SchedulerPost(_myCommand, DateTimeOffset.UtcNow.AddSeconds(1)); + + await Task.Delay(TimeSpan.FromSeconds(10)); + + _outbox.Get(_myCommand.Id, new RequestContext()).Should().NotBeNull(); + } +} + +public class MyCommand : Command +{ + public MyCommand() + : base(Guid.NewGuid()) + + { + } + + public string Value { get; set; } + public bool WasCancelled { get; set; } + public bool TaskCompleted { get; set; } +} + +internal class EmptyHandlerFactorySync : IAmAHandlerFactorySync +{ + public IHandleRequests Create(Type handlerType, IAmALifetime lifetime) + { + return null; + } + + public void Release(IHandleRequests handler, IAmALifetime lifetime) { } +} + +internal class MyCommandMessageMapper : IAmAMessageMapper +{ + public IRequestContext Context { get; set; } + + public Message MapToMessage(MyCommand request, Publication publication) + { + var header = new MessageHeader(request.Id, publication.Topic, request.RequestToMessageType()); + var body = new MessageBody(JsonSerializer.Serialize(request, JsonSerialisationOptions.Options)); + var message = new Message(header, body); + return message; + } + + public MyCommand MapToRequest(Message message) + { + var command = JsonSerializer.Deserialize(message.Body.Value, JsonSerialisationOptions.Options); + return command; + } +} + +public class BrighterResolver : PropertySettingJobFactory +{ + public static IAmACommandProcessor Processor { get; set; } + + public override IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) + { + return new QuartzBrighterJob(Processor); + } +} From a6abc6832aeb9677da8865afa038b3496d65bf74 Mon Sep 17 00:00:00 2001 From: Rafael Andrade Date: Wed, 5 Feb 2025 08:56:44 +0000 Subject: [PATCH 12/17] Fixes unit test --- .../InMemoryMessageScheduler.cs | 64 ++++---- ...ling_A_Message_To_The_Command_Processor.cs | 52 ++++--- ..._Message_To_The_Command_Processor_Async.cs | 22 +-- .../When_Scheduling_With_A_Default_Policy.cs | 26 ++-- ..._Scheduling_With_A_Default_Policy_Async.cs | 143 ++++++++++++++++++ ...Scheduling_A_Request_A_Span_Is_Exported.cs | 21 ++- ...ling_A_Request_A_Span_Is_Exported_Async.cs | 43 +++--- 7 files changed, 276 insertions(+), 95 deletions(-) create mode 100644 tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_With_A_Default_Policy_Async.cs diff --git a/src/Paramore.Brighter/InMemoryMessageScheduler.cs b/src/Paramore.Brighter/InMemoryMessageScheduler.cs index 930025297f..9e460bc03b 100644 --- a/src/Paramore.Brighter/InMemoryMessageScheduler.cs +++ b/src/Paramore.Brighter/InMemoryMessageScheduler.cs @@ -2,22 +2,26 @@ using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Logging; using Paramore.Brighter.Scheduler.Events; using Paramore.Brighter.Tasks; using InvalidOperationException = System.InvalidOperationException; namespace Paramore.Brighter; -public class InMemoryMessageScheduler(IAmACommandProcessor processor, +public class InMemoryMessageScheduler( + IAmACommandProcessor processor, TimeProvider timeProvider, Func getOrCreateSchedulerId, OnSchedulerConflict onConflict) : IAmAMessageSchedulerSync, IAmAMessageSchedulerAsync { private static readonly ConcurrentDictionary s_timers = new(); + private static readonly ILogger Logger = ApplicationLogging.CreateLogger(); /// - public string Schedule(Message message, DateTimeOffset at) + public string Schedule(Message message, DateTimeOffset at) => Schedule(message, at - DateTimeOffset.UtcNow); /// @@ -30,27 +34,27 @@ public string Schedule(Message message, TimeSpan delay) { throw new InvalidOperationException($"scheduler with '{id}' id already exists"); } - + timer.Dispose(); } - + s_timers[id] = timeProvider.CreateTimer(Execute, (processor, id, message, false), delay, TimeSpan.Zero); return id; } /// public bool ReScheduler(string schedulerId, DateTimeOffset at) - => ReScheduler(schedulerId, at - DateTimeOffset.UtcNow); + => ReScheduler(schedulerId, at - timeProvider.GetUtcNow()); /// public bool ReScheduler(string schedulerId, TimeSpan delay) { - if(s_timers.TryGetValue(schedulerId, out var timer)) + if (s_timers.TryGetValue(schedulerId, out var timer)) { timer.Change(delay, TimeSpan.Zero); return true; } - + return false; } @@ -64,11 +68,13 @@ public void Cancel(string id) } /// - public Task ScheduleAsync(Message message, DateTimeOffset at, CancellationToken cancellationToken = default) - => Task.FromResult(Schedule(message, at)); + public async Task ScheduleAsync(Message message, DateTimeOffset at, + CancellationToken cancellationToken = default) + => await ScheduleAsync(message, at - timeProvider.GetUtcNow(), cancellationToken); /// - public async Task ScheduleAsync(Message message, TimeSpan delay, CancellationToken cancellationToken = default) + public async Task ScheduleAsync(Message message, TimeSpan delay, + CancellationToken cancellationToken = default) { var id = getOrCreateSchedulerId(message); if (s_timers.TryGetValue(id, out var timer)) @@ -77,10 +83,10 @@ public async Task ScheduleAsync(Message message, TimeSpan delay, Cancell { throw new InvalidOperationException($"scheduler with '{id}' id already exists"); } - + await timer.DisposeAsync(); } - + s_timers[id] = timeProvider.CreateTimer(Execute, (processor, id, message, true), delay, TimeSpan.Zero); return id; } @@ -117,27 +123,27 @@ public ValueTask DisposeAsync() private static void Execute(object? state) { - var (processor, id, message, async) = ((IAmACommandProcessor, string, Message, bool)) state!; - if(async) - { - BrighterAsyncContext.Run(async () => await processor.PostAsync(new FireSchedulerMessage - { - Id = id, - Message = message - })); - } - else + var (processor, id, message, async) = ((IAmACommandProcessor, string, Message, bool))state!; + try { - processor.Post(new FireSchedulerMessage + if (async) { - Id = id, - Message = message - }); - } + BrighterAsyncContext.Run(async () => + await processor.PostAsync(new FireSchedulerMessage { Id = id, Message = message })); + } + else + { + processor.Post(new FireSchedulerMessage { Id = id, Message = message }); + } - if (s_timers.TryRemove(id, out var timer)) + if (s_timers.TryRemove(id, out var timer)) + { + timer.Dispose(); + } + } + catch (Exception e) { - timer.Dispose(); + Logger.LogError(e, "Error during processing scheduler {Id}", id); } } } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_To_The_Command_Processor.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_To_The_Command_Processor.cs index e2472f9d16..96dae8ebad 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_To_The_Command_Processor.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_To_The_Command_Processor.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2015 Ian Cooper @@ -47,15 +48,19 @@ public class CommandProcessorSchedulerCommandTests : IDisposable private readonly Message _message; private readonly InMemoryOutbox _outbox; private readonly InternalBus _internalBus = new(); + private readonly FakeTimeProvider _timeProvider; public CommandProcessorSchedulerCommandTests() { - _myCommand = new MyCommand { Value = $"Hello World {Guid.NewGuid():N}"}; + _myCommand = new MyCommand { Value = $"Hello World {Guid.NewGuid():N}" }; - var timeProvider = new FakeTimeProvider(); var routingKey = new RoutingKey(Topic); - - InMemoryProducer producer = new(_internalBus, timeProvider) {Publication = {Topic = routingKey, RequestType = typeof(MyCommand)}}; + + _timeProvider = new FakeTimeProvider(); + _timeProvider.SetUtcNow(DateTimeOffset.UtcNow); + + InMemoryProducer producer = + new(_internalBus, _timeProvider) { Publication = { Topic = routingKey, RequestType = typeof(MyCommand) } }; _message = new Message( new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND), @@ -74,19 +79,20 @@ public CommandProcessorSchedulerCommandTests() var circuitBreakerPolicy = Policy .Handle() .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); - + var policyRegistry = new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }; - var producerRegistry = new ProducerRegistry(new Dictionary {{routingKey, producer},}); + var producerRegistry = + new ProducerRegistry(new Dictionary { { routingKey, producer }, }); + + var tracer = new BrighterTracer(_timeProvider); + _outbox = new InMemoryOutbox(_timeProvider) { Tracer = tracer }; - var tracer = new BrighterTracer(timeProvider); - _outbox = new InMemoryOutbox(timeProvider) {Tracer = tracer}; - IAmAnOutboxProducerMediator bus = new OutboxProducerMediator( - producerRegistry, - policyRegistry, + producerRegistry, + policyRegistry, messageMapperRegistry, new EmptyMessageTransformerFactory(), new EmptyMessageTransformerFactoryAsync(), @@ -99,36 +105,38 @@ public CommandProcessorSchedulerCommandTests() new InMemoryRequestContextFactory(), policyRegistry, bus, - messageSchedulerFactory: new InMemoryMessageSchedulerFactory() + messageSchedulerFactory: new InMemoryMessageSchedulerFactory(_timeProvider) ); } [Fact] public void When_Scheduling_With_Delay_A_Message_To_The_Command_Processor() { - _commandProcessor.SchedulerPost(_myCommand, TimeSpan.FromSeconds(1)); + _commandProcessor.SchedulerPost(_myCommand, TimeSpan.FromSeconds(10)); _internalBus.Stream(new RoutingKey(Topic)).Any().Should().BeFalse(); - - Task.Delay(TimeSpan.FromSeconds(2)).GetAwaiter().GetResult(); + + _timeProvider.Advance(TimeSpan.FromSeconds(10)); + _internalBus.Stream(new RoutingKey(Topic)).Any().Should().BeTrue(); - + var message = _outbox.Get(_myCommand.Id, new RequestContext()); message.Should().NotBeNull(); message.Should().Be(_message); } - + [Fact] public void When_Scheduling_With_At_A_Message_To_The_Command_Processor() { - _commandProcessor.SchedulerPost(_myCommand, DateTimeOffset.UtcNow.AddSeconds(1)); + _commandProcessor.SchedulerPost(_myCommand, _timeProvider.GetUtcNow().AddSeconds(10)); _internalBus.Stream(new RoutingKey(Topic)).Any().Should().BeFalse(); - - Task.Delay(TimeSpan.FromSeconds(2)).GetAwaiter().GetResult(); + + _timeProvider.Advance(TimeSpan.FromSeconds(10)); + _internalBus.Stream(new RoutingKey(Topic)).Any().Should().BeTrue(); - + var message = _outbox.Get(_myCommand.Id, new RequestContext()); message.Should().NotBeNull(); - + message.Should().Be(_message); } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_To_The_Command_Processor_Async.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_To_The_Command_Processor_Async.cs index ce0434f3e3..81162eb68b 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_To_The_Command_Processor_Async.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_A_Message_To_The_Command_Processor_Async.cs @@ -46,14 +46,16 @@ public class CommandProcessorSchedulerCommandAsyncTests : IDisposable private readonly InMemoryOutbox _outbox; private readonly InternalBus _internalBus = new(); private readonly RoutingKey _routingKey; + private readonly FakeTimeProvider _timeProvider; public CommandProcessorSchedulerCommandAsyncTests() { _myCommand = new() { Value = $"Hello World {Guid.NewGuid():N}" }; _routingKey = new RoutingKey("MyCommand"); + _timeProvider = new FakeTimeProvider(); + _timeProvider.SetUtcNow(DateTimeOffset.UtcNow); - var timeProvider = new FakeTimeProvider(); - InMemoryProducer producer = new(_internalBus, timeProvider) + InMemoryProducer producer = new(_internalBus, _timeProvider) { Publication = { Topic = _routingKey, RequestType = typeof(MyCommand) } }; @@ -80,8 +82,8 @@ public CommandProcessorSchedulerCommandAsyncTests() var producerRegistry = new ProducerRegistry(new Dictionary { { _routingKey, producer }, }); - var tracer = new BrighterTracer(timeProvider); - _outbox = new InMemoryOutbox(timeProvider) { Tracer = tracer }; + var tracer = new BrighterTracer(_timeProvider); + _outbox = new InMemoryOutbox(_timeProvider) { Tracer = tracer }; IAmAnOutboxProducerMediator bus = new OutboxProducerMediator( producerRegistry, @@ -98,16 +100,18 @@ public CommandProcessorSchedulerCommandAsyncTests() new InMemoryRequestContextFactory(), policyRegistry, bus, - messageSchedulerFactory: new InMemoryMessageSchedulerFactory() + messageSchedulerFactory: new InMemoryMessageSchedulerFactory(_timeProvider) ); } [Fact] public async Task When_Scheduling_With_Delay_A_Message_To_The_Command_Processor_Async() { - await _commandProcessor.SchedulerPostAsync(_myCommand, TimeSpan.FromSeconds(1)); + await _commandProcessor.SchedulerPostAsync(_myCommand, TimeSpan.FromSeconds(10)); _internalBus.Stream(_routingKey).Any().Should().BeFalse(); - await Task.Delay(TimeSpan.FromSeconds(2)); + + _timeProvider.Advance(TimeSpan.FromSeconds(10)); + _internalBus.Stream(_routingKey).Any().Should().BeTrue(); _outbox @@ -118,9 +122,9 @@ public async Task When_Scheduling_With_Delay_A_Message_To_The_Command_Processor_ [Fact] public async Task When_Scheduling_With_At_A_Message_To_The_Command_Processor_Async() { - await _commandProcessor.SchedulerPostAsync(_myCommand, DateTimeOffset.UtcNow.AddSeconds(1)); + await _commandProcessor.SchedulerPostAsync(_myCommand, _timeProvider.GetUtcNow().AddSeconds(10)); _internalBus.Stream(_routingKey).Any().Should().BeFalse(); - await Task.Delay(TimeSpan.FromSeconds(2)); + _timeProvider.Advance(TimeSpan.FromSeconds(10)); _internalBus.Stream(_routingKey).Any().Should().BeTrue(); _outbox diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_With_A_Default_Policy.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_With_A_Default_Policy.cs index a6392a5280..d1e2c75c0e 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_With_A_Default_Policy.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_With_A_Default_Policy.cs @@ -27,7 +27,6 @@ THE SOFTWARE. */ using System.Collections.Generic; using System.Linq; using System.Text.Json; -using System.Threading.Tasks; using System.Transactions; using FluentAssertions; using Microsoft.Extensions.Time.Testing; @@ -46,15 +45,18 @@ public class SchedulerCommandTests : IDisposable private readonly Message _message; private readonly InMemoryOutbox _outbox; private readonly InternalBus _internalBus = new(); + private readonly FakeTimeProvider _timeProvider; public SchedulerCommandTests() { - _myCommand = new(){ Value = $"Hello World {Guid.NewGuid():N}"}; + _myCommand = new() { Value = $"Hello World {Guid.NewGuid():N}" }; - var timeProvider = new FakeTimeProvider(); - var tracer = new BrighterTracer(timeProvider); - _outbox = new InMemoryOutbox(timeProvider) { Tracer = tracer }; - InMemoryProducer producer = new(_internalBus, timeProvider) + _timeProvider = new FakeTimeProvider(); + _timeProvider.SetUtcNow(DateTimeOffset.UtcNow); + + var tracer = new BrighterTracer(_timeProvider); + _outbox = new InMemoryOutbox(_timeProvider) { Tracer = tracer }; + InMemoryProducer producer = new(_internalBus, _timeProvider) { Publication = { Topic = _routingKey, RequestType = typeof(MyCommand) } }; @@ -89,17 +91,18 @@ public SchedulerCommandTests() .ExternalBus(ExternalBusType.FireAndForget, externalBus) .ConfigureInstrumentation(new BrighterTracer(TimeProvider.System), InstrumentationOptions.All) .RequestContextFactory(new InMemoryRequestContextFactory()) - .MessageSchedulerFactory(new InMemoryMessageSchedulerFactory()) + .MessageSchedulerFactory(new InMemoryMessageSchedulerFactory(_timeProvider)) .Build(); } [Fact] public void When_Scheduling_With_A_Default_Policy_And_Passing_A_Delay() { - _commandProcessor.SchedulerPost(_myCommand, TimeSpan.FromSeconds(1)); + _commandProcessor.SchedulerPost(_myCommand, TimeSpan.FromSeconds(10)); _internalBus.Stream(new RoutingKey(_routingKey)).Any().Should().BeFalse(); - Task.Delay(TimeSpan.FromSeconds(2)).Wait(); + _timeProvider.Advance(TimeSpan.FromSeconds(10)); + _internalBus.Stream(new RoutingKey(_routingKey)).Any().Should().BeTrue(); var message = _outbox.Get(_myCommand.Id, new RequestContext()); @@ -110,10 +113,11 @@ public void When_Scheduling_With_A_Default_Policy_And_Passing_A_Delay() [Fact] public void When_Scheduling_With_A_Default_Policy_And_Passing_An_At() { - _commandProcessor.SchedulerPost(_myCommand, DateTimeOffset.UtcNow.AddSeconds(1)); + _commandProcessor.SchedulerPost(_myCommand, _timeProvider.GetUtcNow().AddSeconds(10)); _internalBus.Stream(new RoutingKey(_routingKey)).Any().Should().BeFalse(); - Task.Delay(TimeSpan.FromSeconds(2)).Wait(); + _timeProvider.Advance(TimeSpan.FromSeconds(10)); + _internalBus.Stream(new RoutingKey(_routingKey)).Any().Should().BeTrue(); var message = _outbox.Get(_myCommand.Id, new RequestContext()); diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_With_A_Default_Policy_Async.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_With_A_Default_Policy_Async.cs new file mode 100644 index 0000000000..78a4b4f7c9 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Scheduler/When_Scheduling_With_A_Default_Policy_Async.cs @@ -0,0 +1,143 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2015 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using System.Transactions; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Observability; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.CommandProcessors.Scheduler; + +[Collection("CommandProcessor")] +public class SchedulerCommandAsyncTests : IDisposable +{ + private readonly RoutingKey _routingKey = new("MyCommand"); + private readonly CommandProcessor _commandProcessor; + private readonly MyCommand _myCommand; + private readonly Message _message; + private readonly InMemoryOutbox _outbox; + private readonly InternalBus _internalBus = new(); + private readonly FakeTimeProvider _timeProvider; + + public SchedulerCommandAsyncTests() + { + _myCommand = new() { Value = $"Hello World {Guid.NewGuid():N}" }; + + _timeProvider = new FakeTimeProvider(); + _timeProvider.SetUtcNow(DateTimeOffset.UtcNow); + + var tracer = new BrighterTracer(_timeProvider); + _outbox = new InMemoryOutbox(_timeProvider) { Tracer = tracer }; + InMemoryProducer producer = new(_internalBus, _timeProvider) + { + Publication = { Topic = _routingKey, RequestType = typeof(MyCommand) } + }; + + _message = new Message( + new MessageHeader(_myCommand.Id, _routingKey, MessageType.MT_COMMAND), + new MessageBody(JsonSerializer.Serialize(_myCommand, JsonSerialisationOptions.Options)) + ); + + var messageMapperRegistry = new MessageMapperRegistry( + null, + new SimpleMessageMapperFactoryAsync((_) => new MyCommandMessageMapperAsync()) + ); + messageMapperRegistry.RegisterAsync(); + + var producerRegistry = + new ProducerRegistry(new Dictionary { { _routingKey, producer }, }); + + var externalBus = new OutboxProducerMediator( + producerRegistry: producerRegistry, + policyRegistry: new DefaultPolicy(), + mapperRegistry: messageMapperRegistry, + messageTransformerFactory: new EmptyMessageTransformerFactory(), + messageTransformerFactoryAsync: new EmptyMessageTransformerFactoryAsync(), + tracer: tracer, + outbox: _outbox + ); + + _commandProcessor = CommandProcessorBuilder.StartNew() + .Handlers(new HandlerConfiguration(new SubscriberRegistry(), new EmptyHandlerFactorySync())) + .DefaultPolicy() + .ExternalBus(ExternalBusType.FireAndForget, externalBus) + .ConfigureInstrumentation(new BrighterTracer(TimeProvider.System), InstrumentationOptions.All) + .RequestContextFactory(new InMemoryRequestContextFactory()) + .MessageSchedulerFactory(new InMemoryMessageSchedulerFactory(_timeProvider)) + .Build(); + } + + [Fact] + public async Task When_Scheduling_With_A_Default_Policy_And_Passing_A_Delay_Async() + { + await _commandProcessor.SchedulerPostAsync(_myCommand, TimeSpan.FromSeconds(10)); + _internalBus.Stream(new RoutingKey(_routingKey)).Any().Should().BeFalse(); + + _timeProvider.Advance(TimeSpan.FromSeconds(10)); + + _internalBus.Stream(new RoutingKey(_routingKey)).Any().Should().BeTrue(); + + var message = _outbox.Get(_myCommand.Id, new RequestContext()); + message.Should().NotBeNull(); + message.Should().Be(_message); + } + + [Fact] + public async Task When_Scheduling_With_A_Default_Policy_And_Passing_An_At_Async() + { + await _commandProcessor.SchedulerPostAsync(_myCommand, _timeProvider.GetUtcNow().AddSeconds(10)); + _internalBus.Stream(new RoutingKey(_routingKey)).Any().Should().BeFalse(); + + _timeProvider.Advance(TimeSpan.FromSeconds(10)); + + _internalBus.Stream(new RoutingKey(_routingKey)).Any().Should().BeTrue(); + + var message = _outbox.Get(_myCommand.Id, new RequestContext()); + message.Should().NotBeNull(); + message.Should().Be(_message); + } + + public void Dispose() + { + CommandProcessor.ClearServiceBus(); + } + + internal class EmptyHandlerFactorySync : IAmAHandlerFactorySync + { + public IHandleRequests Create(Type handlerType, IAmALifetime lifetime) + { + return null; + } + + public void Release(IHandleRequests handler, IAmALifetime lifetime) { } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Observability/CommandProcessor/Scheduler/When_Scheduling_A_Request_A_Span_Is_Exported.cs b/tests/Paramore.Brighter.Core.Tests/Observability/CommandProcessor/Scheduler/When_Scheduling_A_Request_A_Span_Is_Exported.cs index 764196f735..e9585a488b 100644 --- a/tests/Paramore.Brighter.Core.Tests/Observability/CommandProcessor/Scheduler/When_Scheduling_A_Request_A_Span_Is_Exported.cs +++ b/tests/Paramore.Brighter.Core.Tests/Observability/CommandProcessor/Scheduler/When_Scheduling_A_Request_A_Span_Is_Exported.cs @@ -26,11 +26,15 @@ public class CommandProcessorSchedulerObservabilityTests private readonly List _exportedActivities; private readonly TracerProvider _traceProvider; private readonly Brighter.CommandProcessor _commandProcessor; + private readonly FakeTimeProvider _timeProvider; public CommandProcessorSchedulerObservabilityTests() { var routingKey = new RoutingKey("MyEvent"); + _timeProvider = new FakeTimeProvider(); + _timeProvider.SetUtcNow(DateTimeOffset.UtcNow); + var builder = Sdk.CreateTracerProviderBuilder(); _exportedActivities = new List(); @@ -51,10 +55,9 @@ public CommandProcessorSchedulerObservabilityTests() .Retry(); var policyRegistry = new PolicyRegistry { { Brighter.CommandProcessor.RETRYPOLICY, retryPolicy } }; - - var timeProvider = new FakeTimeProvider(); - var tracer = new BrighterTracer(timeProvider); - InMemoryOutbox outbox = new(timeProvider) { Tracer = tracer }; + + var tracer = new BrighterTracer(_timeProvider); + InMemoryOutbox outbox = new(_timeProvider) { Tracer = tracer }; var messageMapperRegistry = new MessageMapperRegistry( new SimpleMessageMapperFactory((_) => new MyEventMessageMapper()), @@ -91,7 +94,7 @@ public CommandProcessorSchedulerObservabilityTests() bus, tracer: tracer, instrumentationOptions: InstrumentationOptions.All, - messageSchedulerFactory: new InMemoryMessageSchedulerFactory() + messageSchedulerFactory: new InMemoryMessageSchedulerFactory(_timeProvider) ); } @@ -105,7 +108,7 @@ public void When_Scheduling_A_Request_With_A_Delay_A_Span_Is_Exported() var context = new RequestContext { Span = parentActivity }; //act - _commandProcessor.SchedulerPost(@event, TimeSpan.FromSeconds(5), context); + _commandProcessor.SchedulerPost(@event, TimeSpan.FromSeconds(10), context); parentActivity?.Stop(); _traceProvider.ForceFlush(); @@ -138,6 +141,8 @@ public void When_Scheduling_A_Request_With_A_Delay_A_Span_Is_Exported() (string)a.Value == nameof(MyEventMessageMapper)).Should().BeTrue(); mapperEvent.Tags.Any(a => a.Key == BrighterSemanticConventions.MapperType && (string)a.Value == "sync").Should() .BeTrue(); + + _timeProvider.Advance(TimeSpan.FromSeconds(10)); } [Fact] @@ -150,7 +155,7 @@ public void When_Scheduling_A_Request_With_A_DateTime_A_Span_Is_Exported() var context = new RequestContext { Span = parentActivity }; //act - _commandProcessor.SchedulerPost(@event, DateTimeOffset.UtcNow.AddSeconds(5), context); + _commandProcessor.SchedulerPost(@event, _timeProvider.GetUtcNow().AddSeconds(10), context); parentActivity?.Stop(); _traceProvider.ForceFlush(); @@ -183,5 +188,7 @@ public void When_Scheduling_A_Request_With_A_DateTime_A_Span_Is_Exported() (string)a.Value == nameof(MyEventMessageMapper)).Should().BeTrue(); mapperEvent.Tags.Any(a => a.Key == BrighterSemanticConventions.MapperType && (string)a.Value == "sync").Should() .BeTrue(); + + _timeProvider.Advance(TimeSpan.FromSeconds(10)); } } diff --git a/tests/Paramore.Brighter.Core.Tests/Observability/CommandProcessor/Scheduler/When_Scheduling_A_Request_A_Span_Is_Exported_Async.cs b/tests/Paramore.Brighter.Core.Tests/Observability/CommandProcessor/Scheduler/When_Scheduling_A_Request_A_Span_Is_Exported_Async.cs index 4a76f3127b..5b910f844a 100644 --- a/tests/Paramore.Brighter.Core.Tests/Observability/CommandProcessor/Scheduler/When_Scheduling_A_Request_A_Span_Is_Exported_Async.cs +++ b/tests/Paramore.Brighter.Core.Tests/Observability/CommandProcessor/Scheduler/When_Scheduling_A_Request_A_Span_Is_Exported_Async.cs @@ -26,11 +26,15 @@ public class CommandProcessorSchedulerAsyncObservabilityTests private readonly List _exportedActivities; private readonly TracerProvider _traceProvider; private readonly Brighter.CommandProcessor _commandProcessor; + private readonly FakeTimeProvider _timeProvider; public CommandProcessorSchedulerAsyncObservabilityTests() { var routingKey = new RoutingKey("MyEvent"); + _timeProvider = new FakeTimeProvider(); + _timeProvider.SetUtcNow(DateTimeOffset.UtcNow); + var builder = Sdk.CreateTracerProviderBuilder(); _exportedActivities = new List(); @@ -48,18 +52,19 @@ public CommandProcessorSchedulerAsyncObservabilityTests() var retryPolicy = Policy .Handle() - .Retry(); + .RetryAsync(); - var policyRegistry = new PolicyRegistry { { Brighter.CommandProcessor.RETRYPOLICY, retryPolicy } }; + var policyRegistry = new PolicyRegistry { { Brighter.CommandProcessor.RETRYPOLICYASYNC, retryPolicy } }; - var timeProvider = new FakeTimeProvider(); - var tracer = new BrighterTracer(timeProvider); - InMemoryOutbox outbox = new(timeProvider) { Tracer = tracer }; + var tracer = new BrighterTracer(_timeProvider); + InMemoryOutbox outbox = new(_timeProvider) { Tracer = tracer }; + var messageMapperRegistry = new MessageMapperRegistry( - new SimpleMessageMapperFactory((_) => new MyEventMessageMapper()), - null); - messageMapperRegistry.Register(); + null, + new SimpleMessageMapperFactoryAsync((_) => new MyEventMessageMapperAsync()) + ); + messageMapperRegistry.RegisterAsync(); var producerRegistry = new ProducerRegistry(new Dictionary { @@ -91,7 +96,7 @@ public CommandProcessorSchedulerAsyncObservabilityTests() bus, tracer: tracer, instrumentationOptions: InstrumentationOptions.All, - messageSchedulerFactory: new InMemoryMessageSchedulerFactory() + messageSchedulerFactory: new InMemoryMessageSchedulerFactory(_timeProvider) ); } @@ -105,7 +110,7 @@ public async Task When_Scheduling_A_Request_With_A_Delay_A_Span_Is_Exported_Asyn var context = new RequestContext { Span = parentActivity }; //act - await _commandProcessor.SchedulerPostAsync(@event, TimeSpan.FromSeconds(5), context); + await _commandProcessor.SchedulerPostAsync(@event, TimeSpan.FromSeconds(10), context); parentActivity?.Stop(); _traceProvider.ForceFlush(); @@ -132,12 +137,14 @@ public async Task When_Scheduling_A_Request_With_A_Delay_A_Span_Is_Exported_Asyn events.Count.Should().Be(1); //mapping a message should be an event - var mapperEvent = events.Single(e => e.Name == $"{nameof(MyEventMessageMapper)}"); + var mapperEvent = events.Single(e => e.Name == $"{nameof(MyEventMessageMapperAsync)}"); mapperEvent.Tags .Any(a => a.Key == BrighterSemanticConventions.MapperName && - (string)a.Value == nameof(MyEventMessageMapper)).Should().BeTrue(); - mapperEvent.Tags.Any(a => a.Key == BrighterSemanticConventions.MapperType && (string)a.Value == "sync").Should() + (string)a.Value == nameof(MyEventMessageMapperAsync)).Should().BeTrue(); + mapperEvent.Tags.Any(a => a.Key == BrighterSemanticConventions.MapperType && (string)a.Value == "async").Should() .BeTrue(); + + _timeProvider.Advance(TimeSpan.FromSeconds(10)); } [Fact] @@ -150,7 +157,7 @@ public async Task When_Scheduling_A_Request_With_A_DateTime_A_Span_Is_Exported_A var context = new RequestContext { Span = parentActivity }; //act - await _commandProcessor.SchedulerPostAsync(@event, DateTimeOffset.UtcNow.AddSeconds(5), context); + await _commandProcessor.SchedulerPostAsync(@event, _timeProvider.GetUtcNow().AddSeconds(10), context); parentActivity?.Stop(); _traceProvider.ForceFlush(); @@ -177,11 +184,13 @@ public async Task When_Scheduling_A_Request_With_A_DateTime_A_Span_Is_Exported_A events.Count.Should().Be(1); //mapping a message should be an event - var mapperEvent = events.Single(e => e.Name == $"{nameof(MyEventMessageMapper)}"); + var mapperEvent = events.Single(e => e.Name == $"{nameof(MyEventMessageMapperAsync)}"); mapperEvent.Tags .Any(a => a.Key == BrighterSemanticConventions.MapperName && - (string)a.Value == nameof(MyEventMessageMapper)).Should().BeTrue(); - mapperEvent.Tags.Any(a => a.Key == BrighterSemanticConventions.MapperType && (string)a.Value == "sync").Should() + (string)a.Value == nameof(MyEventMessageMapperAsync)).Should().BeTrue(); + mapperEvent.Tags.Any(a => a.Key == BrighterSemanticConventions.MapperType && (string)a.Value == "async").Should() .BeTrue(); + + _timeProvider.Advance(TimeSpan.FromSeconds(10)); } } From c266d0986621669ff3d16c6c39beba0abcb59a61 Mon Sep 17 00:00:00 2001 From: Rafael Andrade Date: Wed, 5 Feb 2025 09:33:14 +0000 Subject: [PATCH 13/17] Quartz Add support to .NET Standard & .NET 9.0 --- .../Paramore.Brighter.MessageScheduler.Quartz.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Paramore.Brighter.MessageScheduler.Quartz/Paramore.Brighter.MessageScheduler.Quartz.csproj b/src/Paramore.Brighter.MessageScheduler.Quartz/Paramore.Brighter.MessageScheduler.Quartz.csproj index 7a8039ae0a..d10d609301 100644 --- a/src/Paramore.Brighter.MessageScheduler.Quartz/Paramore.Brighter.MessageScheduler.Quartz.csproj +++ b/src/Paramore.Brighter.MessageScheduler.Quartz/Paramore.Brighter.MessageScheduler.Quartz.csproj @@ -1,7 +1,7 @@  - net8.0 + netstandard2.0;net8.0;net9.0 enable enable From 63400799c1a8fa4fa9c5fb320277c884b7fe6521 Mon Sep 17 00:00:00 2001 From: Rafael Andrade Date: Thu, 6 Feb 2025 08:33:52 +0000 Subject: [PATCH 14/17] Add AWS Scheduler --- Brighter.sln | 14 +++ Directory.Packages.props | 2 + .../AWSClientFactoryExtensions.cs | 24 ++++++ .../AwsMessageSchedulerFactory.cs | 58 +++++++++++++ .../FireSchedulerMessageHandler.cs | 23 +++++ .../FireSchedulerMessageMapper.cs | 21 +++++ .../MessageSchedulerTarget.cs | 25 ++++++ .../OnMissingSchedulerGroup.cs | 17 ++++ .../Scheduler.cs | 44 ++++++++++ .../SchedulerGroup.cs | 24 ++++++ .../AWSClientFactory.cs | 43 +++++++--- .../Scheduler/Events/FireSchedulerMessage.cs | 5 ++ .../Helpers/AWSClientFactory.cs | 71 +++------------ .../Helpers/Role.cs | 51 +++++++++++ .../When_Scheduling_A_Message.cs | 86 +++++++++++++++++++ .../Paramore.Brighter.AWS.Tests.csproj | 2 + ...reating_luggagestore_missing_parameters.cs | 3 +- .../When_unwrapping_a_large_message.cs | 4 +- .../When_uploading_luggage_to_S3.cs | 3 +- .../When_validating_a_luggage_store_exists.cs | 9 +- .../When_wrapping_a_large_message.cs | 5 +- 21 files changed, 444 insertions(+), 90 deletions(-) create mode 100644 src/Paramore.Brighter.MessageScheduler.Aws/AWSClientFactoryExtensions.cs create mode 100644 src/Paramore.Brighter.MessageScheduler.Aws/AwsMessageSchedulerFactory.cs create mode 100644 src/Paramore.Brighter.MessageScheduler.Aws/FireSchedulerMessageHandler.cs create mode 100644 src/Paramore.Brighter.MessageScheduler.Aws/FireSchedulerMessageMapper.cs create mode 100644 src/Paramore.Brighter.MessageScheduler.Aws/MessageSchedulerTarget.cs create mode 100644 src/Paramore.Brighter.MessageScheduler.Aws/OnMissingSchedulerGroup.cs create mode 100644 src/Paramore.Brighter.MessageScheduler.Aws/Scheduler.cs create mode 100644 src/Paramore.Brighter.MessageScheduler.Aws/SchedulerGroup.cs create mode 100644 tests/Paramore.Brighter.AWS.Tests/Helpers/Role.cs create mode 100644 tests/Paramore.Brighter.AWS.Tests/MessageScheduler/When_Scheduling_A_Message.cs diff --git a/Brighter.sln b/Brighter.sln index a14400d442..f3711bbfc4 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -319,6 +319,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.MessageSc EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParamoreBrighter.Quartz.Tests", "tests\ParamoreBrighter.Quartz.Tests\ParamoreBrighter.Quartz.Tests.csproj", "{4469AEE3-B460-4948-A0A5-B9480EE70EA4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.MessageScheduler.Aws", "src\Paramore.Brighter.MessageScheduler.Aws\Paramore.Brighter.MessageScheduler.Aws.csproj", "{28C2529C-EF15-4C86-A2CC-9EE326423A77}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1793,6 +1795,18 @@ Global {4469AEE3-B460-4948-A0A5-B9480EE70EA4}.Release|Mixed Platforms.Build.0 = Release|Any CPU {4469AEE3-B460-4948-A0A5-B9480EE70EA4}.Release|x86.ActiveCfg = Release|Any CPU {4469AEE3-B460-4948-A0A5-B9480EE70EA4}.Release|x86.Build.0 = Release|Any CPU + {28C2529C-EF15-4C86-A2CC-9EE326423A77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28C2529C-EF15-4C86-A2CC-9EE326423A77}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28C2529C-EF15-4C86-A2CC-9EE326423A77}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {28C2529C-EF15-4C86-A2CC-9EE326423A77}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {28C2529C-EF15-4C86-A2CC-9EE326423A77}.Debug|x86.ActiveCfg = Debug|Any CPU + {28C2529C-EF15-4C86-A2CC-9EE326423A77}.Debug|x86.Build.0 = Debug|Any CPU + {28C2529C-EF15-4C86-A2CC-9EE326423A77}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28C2529C-EF15-4C86-A2CC-9EE326423A77}.Release|Any CPU.Build.0 = Release|Any CPU + {28C2529C-EF15-4C86-A2CC-9EE326423A77}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {28C2529C-EF15-4C86-A2CC-9EE326423A77}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {28C2529C-EF15-4C86-A2CC-9EE326423A77}.Release|x86.ActiveCfg = Release|Any CPU + {28C2529C-EF15-4C86-A2CC-9EE326423A77}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Directory.Packages.props b/Directory.Packages.props index 7473c736af..ad6c067e8f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,9 @@ + + diff --git a/src/Paramore.Brighter.MessageScheduler.Aws/AWSClientFactoryExtensions.cs b/src/Paramore.Brighter.MessageScheduler.Aws/AWSClientFactoryExtensions.cs new file mode 100644 index 0000000000..a57d86e50e --- /dev/null +++ b/src/Paramore.Brighter.MessageScheduler.Aws/AWSClientFactoryExtensions.cs @@ -0,0 +1,24 @@ +using Amazon.IdentityManagement; +using Amazon.Scheduler; +using Paramore.Brighter.MessagingGateway.AWSSQS; + +namespace Paramore.Brighter.MessageScheduler.Aws; + +public static class AWSClientFactoryExtensions +{ + public static IAmazonScheduler CreateSchedulerClient(this AWSClientFactory factory) + { + var config = new AmazonSchedulerConfig { RegionEndpoint = factory.RegionEndpoint }; + + factory.ClientConfigAction?.Invoke(config); + + return new AmazonSchedulerClient(factory.Credentials, config); + } + + public static AmazonIdentityManagementServiceClient CreateIdentityClient(this AWSClientFactory factory) + { + var config = new AmazonIdentityManagementServiceConfig { RegionEndpoint = factory.RegionEndpoint }; + factory.ClientConfigAction?.Invoke(config); + return new AmazonIdentityManagementServiceClient(factory.Credentials, config); + } +} diff --git a/src/Paramore.Brighter.MessageScheduler.Aws/AwsMessageSchedulerFactory.cs b/src/Paramore.Brighter.MessageScheduler.Aws/AwsMessageSchedulerFactory.cs new file mode 100644 index 0000000000..71262a9c39 --- /dev/null +++ b/src/Paramore.Brighter.MessageScheduler.Aws/AwsMessageSchedulerFactory.cs @@ -0,0 +1,58 @@ +using Paramore.Brighter.MessagingGateway.AWSSQS; + +namespace Paramore.Brighter.MessageScheduler.Aws; + +/// +/// The Aws message Scheduler factory +/// +public class AwsMessageSchedulerFactory(AWSMessagingGatewayConnection connection, string role, RoutingKey topicOrQueue) + : IAmAMessageSchedulerFactory +{ + /// + /// The AWS Scheduler group + /// + public SchedulerGroup Group { get; set; } = new(); + + /// + /// Get or create a scheduler id + /// + public Func GetOrCreateSchedulerId { get; set; } = _ => Guid.NewGuid().ToString("N"); + + /// + /// The flexible time window + /// + public int? FlexibleTimeWindowMinutes { get; set; } + + /// + /// The topic or queue that Brighter should use for messaging scheduler + /// It can be Topic Name/ARN or Queue Name/Url + /// + public RoutingKey TopicOrQueue { get; set; } = topicOrQueue; + + /// + /// The AWS Role Name/ARN + /// + public string Role { get; set; } = role; + + /// + /// Allow Brighter to give a priority to as destiny topic, in case it exists. + /// + public bool UseMessageTopicAsTarget { get; set; } + + /// + /// Action to be performed when a conflict happen during scheduler creating + /// + public OnSchedulerConflict OnConflict { get; set; } + + public IAmAMessageScheduler Create(IAmACommandProcessor processor) + => new AwsMessageScheduler(new AWSClientFactory(connection), GetOrCreateSchedulerId, + new Scheduler + { + Role = Role, + TopicOrQueue = TopicOrQueue, + UseMessageTopicAsTarget = UseMessageTopicAsTarget, + OnConflict = OnConflict, + FlexibleTimeWindowMinutes = FlexibleTimeWindowMinutes + }, + Group); +} diff --git a/src/Paramore.Brighter.MessageScheduler.Aws/FireSchedulerMessageHandler.cs b/src/Paramore.Brighter.MessageScheduler.Aws/FireSchedulerMessageHandler.cs new file mode 100644 index 0000000000..402bc75be4 --- /dev/null +++ b/src/Paramore.Brighter.MessageScheduler.Aws/FireSchedulerMessageHandler.cs @@ -0,0 +1,23 @@ +using Paramore.Brighter.Scheduler.Events; + +namespace Paramore.Brighter.MessageScheduler.Aws; + +/// +/// The fire scheduler message handler +/// +public class FireSchedulerMessageHandler(IAmACommandProcessor processor) : RequestHandlerAsync +{ + public override async Task HandleAsync(FireSchedulerMessage command, CancellationToken cancellationToken = default) + { + if (command.Async) + { + await processor.PostAsync(command, cancellationToken: cancellationToken); + } + else + { + processor.Post(command); + } + + return await base.HandleAsync(command, cancellationToken); + } +} diff --git a/src/Paramore.Brighter.MessageScheduler.Aws/FireSchedulerMessageMapper.cs b/src/Paramore.Brighter.MessageScheduler.Aws/FireSchedulerMessageMapper.cs new file mode 100644 index 0000000000..4d309a9d10 --- /dev/null +++ b/src/Paramore.Brighter.MessageScheduler.Aws/FireSchedulerMessageMapper.cs @@ -0,0 +1,21 @@ +using System.Text.Json; +using Paramore.Brighter.Scheduler.Events; + +namespace Paramore.Brighter.MessageScheduler.Aws; + +/// +/// The fired scheduler message. +/// +public class FireSchedulerMessageMapper : IAmAMessageMapper +{ + public IRequestContext? Context { get; set; } + public Message MapToMessage(FireSchedulerMessage request, Publication publication) + { + throw new NotImplementedException(); + } + + public FireSchedulerMessage MapToRequest(Message message) + { + return JsonSerializer.Deserialize(message.Body.Value, JsonSerialisationOptions.Options)!; + } +} diff --git a/src/Paramore.Brighter.MessageScheduler.Aws/MessageSchedulerTarget.cs b/src/Paramore.Brighter.MessageScheduler.Aws/MessageSchedulerTarget.cs new file mode 100644 index 0000000000..c5e137cb6a --- /dev/null +++ b/src/Paramore.Brighter.MessageScheduler.Aws/MessageSchedulerTarget.cs @@ -0,0 +1,25 @@ +using Paramore.Brighter.Scheduler.Events; + +namespace Paramore.Brighter.MessageScheduler.Aws; + +/// +/// The source trigger for fired scheduler message +/// +public enum MessageSchedulerTarget +{ + /// + /// The AWS scheduler will scheduler the on Sqs + /// + /// + /// For this case is necessary to configure a subscription to + /// + Sqs, + + /// + /// The AWS scheduler will scheduler the on SNS + /// + /// + /// For this case is necessary to configure a subscription to + /// + Sns +} diff --git a/src/Paramore.Brighter.MessageScheduler.Aws/OnMissingSchedulerGroup.cs b/src/Paramore.Brighter.MessageScheduler.Aws/OnMissingSchedulerGroup.cs new file mode 100644 index 0000000000..0cfe29ce2e --- /dev/null +++ b/src/Paramore.Brighter.MessageScheduler.Aws/OnMissingSchedulerGroup.cs @@ -0,0 +1,17 @@ +namespace Paramore.Brighter.MessageScheduler.Aws; + +/// +/// Action to be performed about scheduler group +/// +public enum OnMissingSchedulerGroup +{ + /// + /// Assume teh message group exists + /// + Assume, + + /// + /// Check if the message group exists, if not create + /// + Create +} diff --git a/src/Paramore.Brighter.MessageScheduler.Aws/Scheduler.cs b/src/Paramore.Brighter.MessageScheduler.Aws/Scheduler.cs new file mode 100644 index 0000000000..c58237a99a --- /dev/null +++ b/src/Paramore.Brighter.MessageScheduler.Aws/Scheduler.cs @@ -0,0 +1,44 @@ +using Amazon.Scheduler; +using Amazon.Scheduler.Model; + +namespace Paramore.Brighter.MessageScheduler.Aws; + +/// +/// The AWS scheduler attributes. +/// +public class Scheduler +{ + /// + /// The AWS Role ARN + /// + public string Role { get; init; } = string.Empty; + + /// + /// The flexible time window + /// + public int? FlexibleTimeWindowMinutes { get; init; } + + /// + /// The topic ARN or Queue Url + /// + public RoutingKey TopicOrQueue { get; init; } = RoutingKey.Empty; + + /// + /// Allow Brighter to give a priority to as destiny topic, in case it exists. + /// + public bool UseMessageTopicAsTarget { get; set; } + + /// + /// Action to be performed when a conflict happen during scheduler creating + /// + public OnSchedulerConflict OnConflict { get; init; } + + internal FlexibleTimeWindow ToFlexibleTimeWindow() + { + return new FlexibleTimeWindow + { + Mode = FlexibleTimeWindowMinutes == null ? FlexibleTimeWindowMode.OFF : FlexibleTimeWindowMode.FLEXIBLE, + MaximumWindowInMinutes = FlexibleTimeWindowMinutes + }; + } +} diff --git a/src/Paramore.Brighter.MessageScheduler.Aws/SchedulerGroup.cs b/src/Paramore.Brighter.MessageScheduler.Aws/SchedulerGroup.cs new file mode 100644 index 0000000000..d9c629dd97 --- /dev/null +++ b/src/Paramore.Brighter.MessageScheduler.Aws/SchedulerGroup.cs @@ -0,0 +1,24 @@ +using Amazon.Scheduler.Model; + +namespace Paramore.Brighter.MessageScheduler.Aws; + +/// +/// The AWS Scheduler group attributes +/// +public class SchedulerGroup +{ + /// + /// The AWS scheduler group. + /// + public string Name { get; set; } = "default"; + + /// + /// The AWS scheduler group tags + /// + public List Tags { get; set; } = [new() {Key = "Source", Value = "Brighter"}]; + + /// + /// The action to be performed during scheduler group creation + /// + public OnMissingSchedulerGroup MakeSchedulerGroup { get; set; } +} diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSClientFactory.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSClientFactory.cs index a02f17fa30..11e89c1da7 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSClientFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSClientFactory.cs @@ -32,40 +32,63 @@ THE SOFTWARE. */ namespace Paramore.Brighter.MessagingGateway.AWSSQS; -internal class AWSClientFactory( +/// +/// The Aws Client factory +/// +/// +/// +/// +public class AWSClientFactory( AWSCredentials credentials, RegionEndpoint region, Action? clientConfigAction) { + public AWSCredentials Credentials => credentials; + + public RegionEndpoint RegionEndpoint => region; + public Action? ClientConfigAction => clientConfigAction; + public AWSClientFactory(AWSMessagingGatewayConnection connection) : this(connection.Credentials, connection.Region, connection.ClientConfigAction) { } + /// + /// Create SNS Client + /// + /// New instance . public AmazonSimpleNotificationServiceClient CreateSnsClient() { - var config = new AmazonSimpleNotificationServiceConfig { RegionEndpoint = region }; + var config = new AmazonSimpleNotificationServiceConfig { RegionEndpoint = RegionEndpoint }; - clientConfigAction?.Invoke(config); + ClientConfigAction?.Invoke(config); - return new AmazonSimpleNotificationServiceClient(credentials, config); + return new AmazonSimpleNotificationServiceClient(Credentials, config); } + /// + /// Create SQS Client + /// + /// New instance . public AmazonSQSClient CreateSqsClient() { - var config = new AmazonSQSConfig { RegionEndpoint = region }; + var config = new AmazonSQSConfig { RegionEndpoint = RegionEndpoint }; - clientConfigAction?.Invoke(config); + ClientConfigAction?.Invoke(config); - return new AmazonSQSClient(credentials, config); + return new AmazonSQSClient(Credentials, config); } + /// + /// Create STS Client + /// + /// New instance . public AmazonSecurityTokenServiceClient CreateStsClient() { - var config = new AmazonSecurityTokenServiceConfig { RegionEndpoint = region }; + var config = new AmazonSecurityTokenServiceConfig { RegionEndpoint = RegionEndpoint }; - clientConfigAction?.Invoke(config); + ClientConfigAction?.Invoke(config); - return new AmazonSecurityTokenServiceClient(credentials, config); + return new AmazonSecurityTokenServiceClient(Credentials, config); } } diff --git a/src/Paramore.Brighter/Scheduler/Events/FireSchedulerMessage.cs b/src/Paramore.Brighter/Scheduler/Events/FireSchedulerMessage.cs index 2f3e4ff471..3c57ed73a4 100644 --- a/src/Paramore.Brighter/Scheduler/Events/FireSchedulerMessage.cs +++ b/src/Paramore.Brighter/Scheduler/Events/FireSchedulerMessage.cs @@ -11,4 +11,9 @@ public class FireSchedulerMessage() : Command(Guid.NewGuid().ToString()) /// The message that will be fire /// public Message Message { get; init; } = new(); + + /// + /// If it should post sync or async + /// + public bool Async { get; set; } } diff --git a/tests/Paramore.Brighter.AWS.Tests/Helpers/AWSClientFactory.cs b/tests/Paramore.Brighter.AWS.Tests/Helpers/AWSClientFactory.cs index ac321abff4..123bb7b853 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Helpers/AWSClientFactory.cs +++ b/tests/Paramore.Brighter.AWS.Tests/Helpers/AWSClientFactory.cs @@ -25,6 +25,7 @@ THE SOFTWARE. */ using System; using Amazon; +using Amazon.IdentityManagement; using Amazon.Runtime; using Amazon.S3; using Amazon.SecurityToken; @@ -34,71 +35,19 @@ THE SOFTWARE. */ namespace Paramore.Brighter.AWS.Tests.Helpers; -internal class AWSClientFactory +public static class AWSClientFactoryExtensions { - private readonly AWSCredentials _credentials; - private readonly RegionEndpoint _region; - private readonly Action? _clientConfigAction; - - public AWSClientFactory(AWSMessagingGatewayConnection connection) - { - _credentials = connection.Credentials; - _region = connection.Region; - _clientConfigAction = connection.ClientConfigAction; - } - - public AWSClientFactory(AWSCredentials credentials, RegionEndpoint region, Action? clientConfigAction) + public static AmazonS3Client CreateS3Client(this AWSClientFactory factory) { - _credentials = credentials; - _region = region; - _clientConfigAction = clientConfigAction; + var config = new AmazonS3Config { RegionEndpoint = factory.RegionEndpoint }; + factory.ClientConfigAction?.Invoke(config); + return new AmazonS3Client(factory.Credentials, config); } - public AmazonSimpleNotificationServiceClient CreateSnsClient() + public static AmazonIdentityManagementServiceClient CreateIdentityClient(this AWSClientFactory factory) { - var config = new AmazonSimpleNotificationServiceConfig { RegionEndpoint = _region }; - - if (_clientConfigAction != null) - { - _clientConfigAction(config); - } - - return new AmazonSimpleNotificationServiceClient(_credentials, config); - } - - public AmazonSQSClient CreateSqsClient() - { - var config = new AmazonSQSConfig { RegionEndpoint = _region }; - - if (_clientConfigAction != null) - { - _clientConfigAction(config); - } - - return new AmazonSQSClient(_credentials, config); - } - - public AmazonSecurityTokenServiceClient CreateStsClient() - { - var config = new AmazonSecurityTokenServiceConfig { RegionEndpoint = _region }; - - if (_clientConfigAction != null) - { - _clientConfigAction(config); - } - - return new AmazonSecurityTokenServiceClient(_credentials, config); - } - - public AmazonS3Client CreateS3Client() - { - var config = new AmazonS3Config { RegionEndpoint = _region }; - - if (_clientConfigAction != null) - { - _clientConfigAction(config); - } - - return new AmazonS3Client(_credentials, config); + var config = new AmazonIdentityManagementServiceConfig { RegionEndpoint = factory.RegionEndpoint }; + factory.ClientConfigAction?.Invoke(config); + return new AmazonIdentityManagementServiceClient(factory.Credentials, config); } } diff --git a/tests/Paramore.Brighter.AWS.Tests/Helpers/Role.cs b/tests/Paramore.Brighter.AWS.Tests/Helpers/Role.cs new file mode 100644 index 0000000000..eeb503c0d5 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/Helpers/Role.cs @@ -0,0 +1,51 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Amazon.IdentityManagement.Model; +using Paramore.Brighter.MessagingGateway.AWSSQS; + +namespace Paramore.Brighter.AWS.Tests.Helpers; + +public static class Role +{ + public static async Task GetOrCreateRoleAsync(AWSClientFactory factory, string roleName) + { + using var client = factory.CreateIdentityClient(); + try + { + var role = await client.GetRoleAsync(new GetRoleRequest { RoleName = roleName }); + if (role.HttpStatusCode == HttpStatusCode.OK) + { + return role.Role.Arn; + } + } + catch + { + // We are going to create case do not exists + } + + var createRoleResponse = await client.CreateRoleAsync(new CreateRoleRequest + { + RoleName = roleName, + AssumeRolePolicyDocument = """ + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "scheduler.amazonaws.com" + }, + "Action": "sts:AssumeRole" + }, + { + "Effect": "Allow", + "Resource": "*", + "Action": "*" + }] + } + """ + }); + return createRoleResponse.Role.Arn; + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/When_Scheduling_A_Message.cs b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/When_Scheduling_A_Message.cs new file mode 100644 index 0000000000..27ab10c982 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/When_Scheduling_A_Message.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Transactions; +using System.Xml; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessageScheduler.Aws; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.Observability; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessageScheduler; + +public class SnsSchedulingMessageTest +{ + private const string ContentType = "text\\plain"; + private readonly SnsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _topicName; + private readonly ChannelFactory _channelFactory; + private readonly AwsMessageSchedulerFactory _factory; + + + public SnsSchedulingMessageTest() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var channelName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _topicName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + + //we need the channel to create the queues and notifications + var routingKey = new RoutingKey(_topicName); + + var channel = _channelFactory.CreateSyncChannel(new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + makeChannels: OnMissingChannel.Create + )); + + //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel + //just for the tests, so create a new consumer from the properties + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName()); + _messageProducer = new SnsMessageProducer(awsConnection, new SnsPublication { MakeChannels = OnMissingChannel.Create }); + + var role = Role + .GetOrCreateRoleAsync(new AWSClientFactory(awsConnection), "test-scheduler") + .GetAwaiter() + .GetResult(); + _factory = new AwsMessageSchedulerFactory(awsConnection, role, new RoutingKey("scheduler")) + { + UseMessageTopicAsTarget = true + }; + } + + [Fact] + public void Test() + { + var routingKey = new RoutingKey(_topicName); + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + ); + + var scheduler = (IAmAMessageSchedulerAsync)_factory.Create(null!)!; + scheduler.ScheduleAsync(message, DateTimeOffset.UtcNow.Add(TimeSpan.FromSeconds(1))); + var messages = _consumer.Receive(TimeSpan.FromSeconds(2)); + messages.Should().ContainSingle(); + messages[0].Body.Value.Should().Be(message.Body.Value); + messages[0].Header.Should().BeEquivalentTo(message.Header); + } + + internal class EmptyHandlerFactorySync : IAmAHandlerFactorySync + { + public IHandleRequests Create(Type handlerType, IAmALifetime lifetime) + { + return null; + } + + public void Release(IHandleRequests handler, IAmALifetime lifetime) { } + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj b/tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj index 6efd2fd56d..35660df737 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj +++ b/tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj @@ -6,6 +6,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -22,6 +23,7 @@ + diff --git a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_creating_luggagestore_missing_parameters.cs b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_creating_luggagestore_missing_parameters.cs index dc1dd5e49f..12be82fbc3 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_creating_luggagestore_missing_parameters.cs +++ b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_creating_luggagestore_missing_parameters.cs @@ -2,14 +2,13 @@ using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; using Amazon.S3; using Amazon.S3.Model; using Amazon.SecurityToken; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.MessagingGateway.AWSSQS; using Paramore.Brighter.Tranformers.AWS; using Xunit; diff --git a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_unwrapping_a_large_message.cs b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_unwrapping_a_large_message.cs index e8643ac374..d030846068 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_unwrapping_a_large_message.cs +++ b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_unwrapping_a_large_message.cs @@ -4,15 +4,13 @@ using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; using Amazon.S3; using Amazon.S3.Model; -using Amazon.SecurityToken; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Paramore.Brighter.AWS.Tests.Helpers; using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; using Paramore.Brighter.Tranformers.AWS; using Paramore.Brighter.Transforms.Transformers; using Xunit; diff --git a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_uploading_luggage_to_S3.cs b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_uploading_luggage_to_S3.cs index b33494b163..7eecea233f 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_uploading_luggage_to_S3.cs +++ b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_uploading_luggage_to_S3.cs @@ -4,14 +4,13 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; using Amazon.S3; using Amazon.S3.Model; using Amazon.SecurityToken; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.MessagingGateway.AWSSQS; using Paramore.Brighter.Tranformers.AWS; using Polly; using Polly.Contrib.WaitAndRetry; diff --git a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_validating_a_luggage_store_exists.cs b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_validating_a_luggage_store_exists.cs index 5efa46e871..d0e30d2965 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_validating_a_luggage_store_exists.cs +++ b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_validating_a_luggage_store_exists.cs @@ -1,23 +1,16 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Net; using System.Net.Http; using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; using Amazon.S3; using Amazon.S3.Model; using Amazon.SecurityToken; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.MessagingGateway.AWSSQS; using Paramore.Brighter.Tranformers.AWS; -using Polly; -using Polly.Contrib.WaitAndRetry; -using Polly.Retry; using Xunit; -using Policy = Polly.Policy; namespace Paramore.Brighter.AWS.Tests.Transformers; diff --git a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_wrapping_a_large_message.cs b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_wrapping_a_large_message.cs index fa0ab9db5b..057f027a53 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_wrapping_a_large_message.cs +++ b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_wrapping_a_large_message.cs @@ -1,16 +1,13 @@ using System; -using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; using Amazon.S3; using Amazon.S3.Model; -using Amazon.SecurityToken; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Paramore.Brighter.AWS.Tests.Helpers; using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; using Paramore.Brighter.Tranformers.AWS; using Paramore.Brighter.Transforms.Transformers; using Xunit; From dad21ca596336e0a396da3710606083a7c394d23 Mon Sep 17 00:00:00 2001 From: Rafael Andrade Date: Thu, 6 Feb 2025 08:34:21 +0000 Subject: [PATCH 15/17] Add Missing file --- .../AwsMessageScheduler.cs | 491 ++++++++++++++++++ ...amore.Brighter.MessageScheduler.Aws.csproj | 19 + 2 files changed, 510 insertions(+) create mode 100644 src/Paramore.Brighter.MessageScheduler.Aws/AwsMessageScheduler.cs create mode 100644 src/Paramore.Brighter.MessageScheduler.Aws/Paramore.Brighter.MessageScheduler.Aws.csproj diff --git a/src/Paramore.Brighter.MessageScheduler.Aws/AwsMessageScheduler.cs b/src/Paramore.Brighter.MessageScheduler.Aws/AwsMessageScheduler.cs new file mode 100644 index 0000000000..93b82128b5 --- /dev/null +++ b/src/Paramore.Brighter.MessageScheduler.Aws/AwsMessageScheduler.cs @@ -0,0 +1,491 @@ +using System.Collections.Concurrent; +using System.Text.Json; +using Amazon; +using Amazon.Scheduler; +using Amazon.Scheduler.Model; +using Amazon.SimpleNotificationService.Model; +using Amazon.SQS.Model; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.Scheduler.Events; +using Paramore.Brighter.Tasks; +using MessageAttributeValue = Amazon.SimpleNotificationService.Model.MessageAttributeValue; +using ResourceNotFoundException = Amazon.Scheduler.Model.ResourceNotFoundException; + +namespace Paramore.Brighter.MessageScheduler.Aws; + +public class AwsMessageScheduler( + AWSClientFactory factory, + Func getOrCreateSchedulerId, + Scheduler scheduler, + SchedulerGroup schedulerGroup) : IAmAMessageSchedulerAsync, IAmAMessageSchedulerSync +{ + private static string? s_schedulerTopicArn; + private static string? s_schedulerQueueUrl; + private static string? s_roleArn; + private static readonly ConcurrentDictionary s_checkedGroup = new(); + private static readonly ConcurrentDictionary s_queueUrl = new(); + private static readonly ConcurrentDictionary s_topic = new(); + + /// + public async Task ScheduleAsync(Message message, DateTimeOffset at, + CancellationToken cancellationToken = default) + => await ScheduleAsync(message, at, true, cancellationToken); + + /// + public Task ScheduleAsync(Message message, TimeSpan delay, CancellationToken cancellationToken = default) + => ScheduleAsync(message, DateTimeOffset.UtcNow.Add(delay), cancellationToken); + + /// + public async Task ReSchedulerAsync(string schedulerId, DateTimeOffset at, + CancellationToken cancellationToken = default) + { + try + { + using var client = factory.CreateSchedulerClient(); + var res = await client.GetScheduleAsync( + new GetScheduleRequest { Name = schedulerId, GroupName = schedulerGroup.Name }, cancellationToken); + + await client.UpdateScheduleAsync( + new UpdateScheduleRequest + { + Name = schedulerId, + GroupName = schedulerGroup.Name, + Target = res.Target, + ScheduleExpression = AtExpression(at), + ScheduleExpressionTimezone = "UTC", + State = ScheduleState.ENABLED, + ActionAfterCompletion = ActionAfterCompletion.DELETE, + FlexibleTimeWindow = scheduler.ToFlexibleTimeWindow() + }, cancellationToken); + + return true; + } + catch (ResourceNotFoundException) + { + // Case the scheduler doesn't exist we are going to ignore it + return false; + } + } + + /// + public async Task ReSchedulerAsync(string schedulerId, TimeSpan delay, + CancellationToken cancellationToken = default) + => await ReSchedulerAsync(schedulerId, DateTimeOffset.UtcNow.Add(delay), cancellationToken); + + /// + public async Task CancelAsync(string id, CancellationToken cancellationToken = default) + { + try + { + using var client = factory.CreateSchedulerClient(); + await client.DeleteScheduleAsync(new DeleteScheduleRequest { Name = id, GroupName = schedulerGroup.Name }, + cancellationToken); + } + catch (ResourceNotFoundException) + { + // Case the scheduler doesn't exist we are going to ignore it + } + } + + private ValueTask EnsureGroupExistsAsync(CancellationToken cancellationToken) + { + if (schedulerGroup.MakeSchedulerGroup == OnMissingSchedulerGroup.Assume || + s_checkedGroup.ContainsKey(schedulerGroup.Name)) + { + return new ValueTask(); + } + + return new ValueTask(CreateSchedulerGroup(cancellationToken)); + } + + + private async Task CreateSchedulerGroup(CancellationToken cancellationToken) + { + var client = factory.CreateSchedulerClient(); + try + { + _ = await client.GetScheduleGroupAsync(new GetScheduleGroupRequest { Name = schedulerGroup.Name }, + cancellationToken); + } + catch (ResourceNotFoundException) + { + try + { + _ = await client.CreateScheduleGroupAsync( + new CreateScheduleGroupRequest { Name = schedulerGroup.Name, Tags = schedulerGroup.Tags }, + cancellationToken); + } + catch (ConflictException) + { + // Ignoring due concurrency issue + } + } + + s_checkedGroup.TryAdd(schedulerGroup.Name, true); + } + + private async Task CreateTargetAsync(string id, Message message, bool async, CancellationToken cancellationToken) + { + var roleArn = await GetRoleArnAsync(cancellationToken); + if (scheduler.UseMessageTopicAsTarget) + { + var topicArn = await GetTopicAsync(message); + if (!string.IsNullOrEmpty(topicArn)) + { + return new Target + { + RoleArn = roleArn, + Arn = "arn:aws:scheduler:::aws-sdk:sns:publish", + Input = JsonSerializer.Serialize(ToPublishRequest(topicArn, message)) + }; + } + + var queueUrl = await GetQueueAsync(message); + if (!string.IsNullOrEmpty(queueUrl)) + { + return new Target + { + RoleArn = roleArn, + Arn = "arn:aws:scheduler:::aws-sdk:sqs:send", + Input = JsonSerializer.Serialize(ToSendMessageRequest(queueUrl, message)) + }; + } + } + + await LoadDefaultTopicOrQueueAsync(); + + var schedulerMessage = new Message + { + Header = new MessageHeader + { + Topic = scheduler.TopicOrQueue, + MessageId = id, + MessageType = MessageType.MT_COMMAND, + Subject = nameof(FireSchedulerMessage), + }, + Body = new MessageBody( + JsonSerializer.Serialize(new FireSchedulerMessage { Id = id, Message = message, Async = async })) + }; + + if (!string.IsNullOrEmpty(s_schedulerTopicArn)) + { + return new Target + { + RoleArn = roleArn, + Arn = "arn:aws:scheduler:::aws-sdk:sns:publish", + Input = JsonSerializer.Serialize(ToPublishRequest(s_schedulerTopicArn, schedulerMessage)) + }; + } + + if (!string.IsNullOrWhiteSpace(s_schedulerQueueUrl)) + { + return new Target + { + RoleArn = roleArn, + Arn = "arn:aws:scheduler:::aws-sdk:sqs:send", + Input = JsonSerializer.Serialize(ToSendMessageRequest(s_schedulerQueueUrl, schedulerMessage)) + }; + } + + throw new InvalidOperationException("Queue or Topic for Scheduler message not found"); + } + + private ValueTask GetTopicAsync(Message message) + { + if (s_topic.TryGetValue(message.Header.Topic, out var topicArn)) + { + return new ValueTask(topicArn); + } + + return new ValueTask(GetTopicArnAsync(message.Header.Topic)); + + + async Task GetTopicArnAsync(string topicName) + { + if (Arn.IsArn(topicName)) + { + s_topic.TryAdd(topicName, topicName); + return topicName; + } + + using var client = factory.CreateSnsClient(); + var topic = await client.FindTopicAsync(topicName); + s_topic.TryAdd(topicName, topic?.TopicArn); + return topic?.TopicArn; + } + } + + private ValueTask GetQueueAsync(Message message) + { + if (s_queueUrl.TryGetValue(message.Header.Topic, out var queueUrl)) + { + return new ValueTask(queueUrl); + } + + return new ValueTask(GetQueueUrlAsync(message.Header.Topic)); + + async Task GetQueueUrlAsync(string queueName) + { + if (Uri.TryCreate(queueName, UriKind.Absolute, out _)) + { + s_queueUrl.TryAdd(queueName, queueName); + return queueName; + } + + using var client = factory.CreateSqsClient(); + try + { + var queue = await client.GetQueueUrlAsync(queueName); + s_queueUrl.TryAdd(queueName, queue.QueueUrl); + return queue.QueueUrl; + } + catch + { + s_queueUrl.TryAdd(queueName, null); + return null; + } + } + } + + private ValueTask GetRoleArnAsync(CancellationToken cancellationToken) + { + if (s_roleArn != null) + { + return new ValueTask(s_roleArn); + } + + return new ValueTask(GetRoleArnAsync(scheduler.Role, cancellationToken)); + + async Task GetRoleArnAsync(string roleName, CancellationToken cancellationToken) + { + if (Arn.IsArn(roleName)) + { + s_roleArn = roleName; + return roleName; + } + + using var client = factory.CreateIdentityClient(); + var role = await client.GetRoleAsync( + new Amazon.IdentityManagement.Model.GetRoleRequest { RoleName = roleName }, + cancellationToken); + s_roleArn = role.Role.Arn; + return s_roleArn; + } + } + + private static PublishRequest ToPublishRequest(string topicArn, Message message) + { + var messageString = message.Body.Value; + var request = new PublishRequest(topicArn, messageString, message.Header.Subject); + + if (string.IsNullOrEmpty(message.Header.CorrelationId)) + { + message.Header.CorrelationId = Guid.NewGuid().ToString(); + } + + var messageAttributes = new Dictionary + { + [HeaderNames.Id] = new() { StringValue = message.Header.MessageId, DataType = "String" }, + [HeaderNames.Topic] = new() { StringValue = topicArn, DataType = "String" }, + [HeaderNames.ContentType] = new() { StringValue = message.Header.ContentType, DataType = "String" }, + [HeaderNames.HandledCount] = + new() { StringValue = Convert.ToString(message.Header.HandledCount), DataType = "String" }, + [HeaderNames.MessageType] = + new() { StringValue = message.Header.MessageType.ToString(), DataType = "String" }, + [HeaderNames.Timestamp] = new() + { + StringValue = Convert.ToString(message.Header.TimeStamp), DataType = "String" + } + }; + + if (!string.IsNullOrEmpty(message.Header.CorrelationId)) + { + messageAttributes[HeaderNames.CorrelationId] = new MessageAttributeValue + { + StringValue = Convert.ToString(message.Header.CorrelationId), DataType = "String" + }; + } + + if (!string.IsNullOrEmpty(message.Header.ReplyTo)) + { + messageAttributes.Add(HeaderNames.ReplyTo, + new MessageAttributeValue + { + StringValue = Convert.ToString(message.Header.ReplyTo), DataType = "String" + }); + } + + var bagJson = JsonSerializer.Serialize(message.Header.Bag, JsonSerialisationOptions.Options); + messageAttributes[HeaderNames.Bag] = new() { StringValue = Convert.ToString(bagJson), DataType = "String" }; + request.MessageAttributes = messageAttributes; + + return request; + } + + private static SendMessageRequest ToSendMessageRequest(string queueUrl, Message message) + { + var request = new SendMessageRequest { QueueUrl = queueUrl, MessageBody = message.Body.Value }; + + var messageAttributes = new Dictionary + { + [HeaderNames.Id] = + new() { StringValue = message.Header.MessageId, DataType = "String" }, + [HeaderNames.Topic] = new() { StringValue = queueUrl, DataType = "String" }, + [HeaderNames.ContentType] = new() { StringValue = message.Header.ContentType, DataType = "String" }, + [HeaderNames.HandledCount] = + new() { StringValue = Convert.ToString(message.Header.HandledCount), DataType = "String" }, + [HeaderNames.MessageType] = + new() { StringValue = message.Header.MessageType.ToString(), DataType = "String" }, + [HeaderNames.Timestamp] = new() + { + StringValue = Convert.ToString(message.Header.TimeStamp), DataType = "String" + } + }; + + if (!string.IsNullOrEmpty(message.Header.ReplyTo)) + { + messageAttributes.Add(HeaderNames.ReplyTo, + new() { StringValue = message.Header.ReplyTo, DataType = "String" }); + } + + if (!string.IsNullOrEmpty(message.Header.Subject)) + { + messageAttributes.Add(HeaderNames.Subject, + new() { StringValue = message.Header.Subject, DataType = "String" }); + } + + if (!string.IsNullOrEmpty(message.Header.CorrelationId)) + { + messageAttributes.Add(HeaderNames.CorrelationId, + new() { StringValue = message.Header.CorrelationId, DataType = "String" }); + } + + // we can set up to 10 attributes; we have set 6 above, so use a single JSON object as the bag + var bagJson = JsonSerializer.Serialize(message.Header.Bag, JsonSerialisationOptions.Options); + messageAttributes[HeaderNames.Bag] = new() { StringValue = bagJson, DataType = "String" }; + request.MessageAttributes = messageAttributes; + return request; + } + + private ValueTask LoadDefaultTopicOrQueueAsync() + { + if (s_schedulerTopicArn != null || s_schedulerQueueUrl != null) + { + return new ValueTask(); + } + + return new ValueTask(LoadDefaultTopicOrQueue()); + + async Task LoadDefaultTopicOrQueue() + { + if (Arn.IsArn(scheduler.TopicOrQueue)) + { + s_schedulerTopicArn = scheduler.TopicOrQueue; + return; + } + + if (Uri.TryCreate(scheduler.TopicOrQueue, UriKind.Absolute, out _)) + { + s_schedulerQueueUrl = scheduler.TopicOrQueue; + return; + } + + using var client = factory.CreateSnsClient(); + var topic = await client.FindTopicAsync(scheduler.TopicOrQueue); + if (topic != null) + { + s_schedulerTopicArn = topic.TopicArn; + return; + } + + try + { + using var sqsClient = factory.CreateSqsClient(); + var queue = await sqsClient.GetQueueUrlAsync(scheduler.TopicOrQueue); + s_schedulerQueueUrl = queue.QueueUrl; + } + catch (NotFoundException) + { + // case we don't find the queue we are going to ignrore it. + } + } + } + + private async Task ScheduleAsync(Message message, DateTimeOffset at, bool async, + CancellationToken cancellationToken = default) + { + await EnsureGroupExistsAsync(cancellationToken); + var id = getOrCreateSchedulerId(message); + var target = await CreateTargetAsync(id, message, async, cancellationToken); + + using var client = factory.CreateSchedulerClient(); + try + { + await client.CreateScheduleAsync( + new CreateScheduleRequest + { + Name = id, + GroupName = schedulerGroup.Name, + Target = target, + ScheduleExpression = AtExpression(at), + ScheduleExpressionTimezone = "UTC", + State = ScheduleState.ENABLED, + ActionAfterCompletion = ActionAfterCompletion.DELETE, + FlexibleTimeWindow = scheduler.ToFlexibleTimeWindow() + }, cancellationToken); + } + catch (ConflictException) + { + if (scheduler.OnConflict == OnSchedulerConflict.Throw) + { + throw; + } + + await client.UpdateScheduleAsync( + new UpdateScheduleRequest + { + Name = id, + GroupName = schedulerGroup.Name, + Target = target, + ScheduleExpression = AtExpression(at), + ScheduleExpressionTimezone = "UTC", + State = ScheduleState.ENABLED, + ActionAfterCompletion = ActionAfterCompletion.DELETE, + FlexibleTimeWindow = scheduler.ToFlexibleTimeWindow() + }, cancellationToken); + } + + return id; + } + + private static string AtExpression(DateTimeOffset publishAt) + => $"at({publishAt.ToUniversalTime():yyyy-MM-ddTHH:mm:ss})"; + + /// + public ValueTask DisposeAsync() => new(); + + /// + public void Dispose() + { + } + + /// + public string Schedule(Message message, DateTimeOffset at) + => BrighterAsyncContext.Run(async () => await ScheduleAsync(message, at, false)); + + /// + public string Schedule(Message message, TimeSpan delay) + => Schedule(message, DateTimeOffset.UtcNow.Add(delay)); + + /// + public bool ReScheduler(string schedulerId, DateTimeOffset at) + => BrighterAsyncContext.Run(async () => await ReSchedulerAsync(schedulerId, at)); + + /// + public bool ReScheduler(string schedulerId, TimeSpan delay) + => BrighterAsyncContext.Run(async () => await ReSchedulerAsync(schedulerId, delay)); + + /// + public void Cancel(string id) + => BrighterAsyncContext.Run(async () => await CancelAsync(id)); +} diff --git a/src/Paramore.Brighter.MessageScheduler.Aws/Paramore.Brighter.MessageScheduler.Aws.csproj b/src/Paramore.Brighter.MessageScheduler.Aws/Paramore.Brighter.MessageScheduler.Aws.csproj new file mode 100644 index 0000000000..a0651b396d --- /dev/null +++ b/src/Paramore.Brighter.MessageScheduler.Aws/Paramore.Brighter.MessageScheduler.Aws.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + From 834cf171e2aee8e0b82202714df86084d099e984 Mon Sep 17 00:00:00 2001 From: Rafael Andrade Date: Thu, 6 Feb 2025 16:08:51 +0000 Subject: [PATCH 16/17] Add AWS Scheduler --- .../AwsMessageScheduler.cs | 179 ++++---------- .../AwsMessageSchedulerFactory.cs | 219 +++++++++++++++++- .../OnMissingRole.cs | 17 ++ .../Scheduler.cs | 22 +- src/Paramore.Brighter/Message.cs | 2 +- .../Helpers/GatewayFactory.cs | 4 +- .../Helpers/Role.cs | 51 ---- .../Sns/When_Scheduling_A_Sns_Message.cs | 103 ++++++++ .../When_Scheduling_A_Sns_Message_Async.cs | 102 ++++++++ ...eduling_A_Sns_Message_Via_FireScheduler.cs | 108 +++++++++ ...g_A_Sns_Message_Via_FireScheduler_Async.cs | 108 +++++++++ .../Sqs/When_Scheduling_A_Sqs_Message.cs | 95 ++++++++ .../When_Scheduling_A_Sqs_Message_Async.cs | 96 ++++++++ ...eduling_A_Sqs_Message_Via_FireScheduler.cs | 105 +++++++++ ...g_A_Sqs_Message_Via_FireScheduler_Async.cs | 103 ++++++++ .../When_Scheduling_A_Message.cs | 86 ------- 16 files changed, 1101 insertions(+), 299 deletions(-) create mode 100644 src/Paramore.Brighter.MessageScheduler.Aws/OnMissingRole.cs delete mode 100644 tests/Paramore.Brighter.AWS.Tests/Helpers/Role.cs create mode 100644 tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sns/When_Scheduling_A_Sns_Message.cs create mode 100644 tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sns/When_Scheduling_A_Sns_Message_Async.cs create mode 100644 tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sns/When_Scheduling_A_Sns_Message_Via_FireScheduler.cs create mode 100644 tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sns/When_Scheduling_A_Sns_Message_Via_FireScheduler_Async.cs create mode 100644 tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sqs/When_Scheduling_A_Sqs_Message.cs create mode 100644 tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sqs/When_Scheduling_A_Sqs_Message_Async.cs create mode 100644 tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sqs/When_Scheduling_A_Sqs_Message_Via_FireScheduler.cs create mode 100644 tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sqs/When_Scheduling_A_Sqs_Message_Via_FireScheduler_Async.cs delete mode 100644 tests/Paramore.Brighter.AWS.Tests/MessageScheduler/When_Scheduling_A_Message.cs diff --git a/src/Paramore.Brighter.MessageScheduler.Aws/AwsMessageScheduler.cs b/src/Paramore.Brighter.MessageScheduler.Aws/AwsMessageScheduler.cs index 93b82128b5..c61aa4040c 100644 --- a/src/Paramore.Brighter.MessageScheduler.Aws/AwsMessageScheduler.cs +++ b/src/Paramore.Brighter.MessageScheduler.Aws/AwsMessageScheduler.cs @@ -3,12 +3,9 @@ using Amazon; using Amazon.Scheduler; using Amazon.Scheduler.Model; -using Amazon.SimpleNotificationService.Model; -using Amazon.SQS.Model; using Paramore.Brighter.MessagingGateway.AWSSQS; using Paramore.Brighter.Scheduler.Events; using Paramore.Brighter.Tasks; -using MessageAttributeValue = Amazon.SimpleNotificationService.Model.MessageAttributeValue; using ResourceNotFoundException = Amazon.Scheduler.Model.ResourceNotFoundException; namespace Paramore.Brighter.MessageScheduler.Aws; @@ -19,9 +16,6 @@ public class AwsMessageScheduler( Scheduler scheduler, SchedulerGroup schedulerGroup) : IAmAMessageSchedulerAsync, IAmAMessageSchedulerSync { - private static string? s_schedulerTopicArn; - private static string? s_schedulerQueueUrl; - private static string? s_roleArn; private static readonly ConcurrentDictionary s_checkedGroup = new(); private static readonly ConcurrentDictionary s_queueUrl = new(); private static readonly ConcurrentDictionary s_topic = new(); @@ -32,8 +26,9 @@ public async Task ScheduleAsync(Message message, DateTimeOffset at, => await ScheduleAsync(message, at, true, cancellationToken); /// - public Task ScheduleAsync(Message message, TimeSpan delay, CancellationToken cancellationToken = default) - => ScheduleAsync(message, DateTimeOffset.UtcNow.Add(delay), cancellationToken); + public async Task ScheduleAsync(Message message, TimeSpan delay, + CancellationToken cancellationToken = default) + => await ScheduleAsync(message, DateTimeOffset.UtcNow.Add(delay), cancellationToken); /// public async Task ReSchedulerAsync(string schedulerId, DateTimeOffset at, @@ -124,9 +119,9 @@ private async Task CreateSchedulerGroup(CancellationToken cancellationToken) s_checkedGroup.TryAdd(schedulerGroup.Name, true); } - private async Task CreateTargetAsync(string id, Message message, bool async, CancellationToken cancellationToken) + private async Task CreateTargetAsync(string id, Message message, bool async) { - var roleArn = await GetRoleArnAsync(cancellationToken); + var roleArn = scheduler.RoleArn; if (scheduler.UseMessageTopicAsTarget) { var topicArn = await GetTopicAsync(message); @@ -146,44 +141,35 @@ private async Task CreateTargetAsync(string id, Message message, bool as return new Target { RoleArn = roleArn, - Arn = "arn:aws:scheduler:::aws-sdk:sqs:send", + Arn = "arn:aws:scheduler:::aws-sdk:sqs:sendMessage", Input = JsonSerializer.Serialize(ToSendMessageRequest(queueUrl, message)) }; } } - await LoadDefaultTopicOrQueueAsync(); - var schedulerMessage = new Message { - Header = new MessageHeader - { - Topic = scheduler.TopicOrQueue, - MessageId = id, - MessageType = MessageType.MT_COMMAND, - Subject = nameof(FireSchedulerMessage), - }, - Body = new MessageBody( - JsonSerializer.Serialize(new FireSchedulerMessage { Id = id, Message = message, Async = async })) + Header = new MessageHeader(id, scheduler.Topic, MessageType.MT_COMMAND, subject: nameof(FireSchedulerMessage)), + Body = new MessageBody(JsonSerializer.Serialize(new FireSchedulerMessage{Id = id, Async = async, Message = message}, JsonSerialisationOptions.Options)) }; - if (!string.IsNullOrEmpty(s_schedulerTopicArn)) + if (!string.IsNullOrEmpty(scheduler.TopicArn)) { return new Target { RoleArn = roleArn, Arn = "arn:aws:scheduler:::aws-sdk:sns:publish", - Input = JsonSerializer.Serialize(ToPublishRequest(s_schedulerTopicArn, schedulerMessage)) + Input = JsonSerializer.Serialize(ToPublishRequest(scheduler.TopicArn, schedulerMessage)) }; } - if (!string.IsNullOrWhiteSpace(s_schedulerQueueUrl)) + if (!string.IsNullOrWhiteSpace(scheduler.QueueUrl)) { return new Target { RoleArn = roleArn, - Arn = "arn:aws:scheduler:::aws-sdk:sqs:send", - Input = JsonSerializer.Serialize(ToSendMessageRequest(s_schedulerQueueUrl, schedulerMessage)) + Arn = "arn:aws:scheduler:::aws-sdk:sqs:sendMessage", + Input = JsonSerializer.Serialize(ToSendMessageRequest(scheduler.QueueUrl, schedulerMessage)) }; } @@ -247,52 +233,23 @@ private async Task CreateTargetAsync(string id, Message message, bool as } } - private ValueTask GetRoleArnAsync(CancellationToken cancellationToken) + private static object ToPublishRequest(string topicArn, Message message) { - if (s_roleArn != null) - { - return new ValueTask(s_roleArn); - } - - return new ValueTask(GetRoleArnAsync(scheduler.Role, cancellationToken)); - - async Task GetRoleArnAsync(string roleName, CancellationToken cancellationToken) - { - if (Arn.IsArn(roleName)) - { - s_roleArn = roleName; - return roleName; - } - - using var client = factory.CreateIdentityClient(); - var role = await client.GetRoleAsync( - new Amazon.IdentityManagement.Model.GetRoleRequest { RoleName = roleName }, - cancellationToken); - s_roleArn = role.Role.Arn; - return s_roleArn; - } - } - - private static PublishRequest ToPublishRequest(string topicArn, Message message) - { - var messageString = message.Body.Value; - var request = new PublishRequest(topicArn, messageString, message.Header.Subject); - if (string.IsNullOrEmpty(message.Header.CorrelationId)) { message.Header.CorrelationId = Guid.NewGuid().ToString(); } - var messageAttributes = new Dictionary + var messageAttributes = new Dictionary { - [HeaderNames.Id] = new() { StringValue = message.Header.MessageId, DataType = "String" }, - [HeaderNames.Topic] = new() { StringValue = topicArn, DataType = "String" }, - [HeaderNames.ContentType] = new() { StringValue = message.Header.ContentType, DataType = "String" }, + [HeaderNames.Id] = new { StringValue = message.Header.MessageId, DataType = "String" }, + [HeaderNames.Topic] = new { StringValue = topicArn, DataType = "String" }, + [HeaderNames.ContentType] = new { StringValue = message.Header.ContentType, DataType = "String" }, [HeaderNames.HandledCount] = - new() { StringValue = Convert.ToString(message.Header.HandledCount), DataType = "String" }, + new { StringValue = Convert.ToString(message.Header.HandledCount), DataType = "String" }, [HeaderNames.MessageType] = - new() { StringValue = message.Header.MessageType.ToString(), DataType = "String" }, - [HeaderNames.Timestamp] = new() + new { StringValue = message.Header.MessageType.ToString(), DataType = "String" }, + [HeaderNames.Timestamp] = new { StringValue = Convert.ToString(message.Header.TimeStamp), DataType = "String" } @@ -300,7 +257,7 @@ private static PublishRequest ToPublishRequest(string topicArn, Message message) if (!string.IsNullOrEmpty(message.Header.CorrelationId)) { - messageAttributes[HeaderNames.CorrelationId] = new MessageAttributeValue + messageAttributes[HeaderNames.CorrelationId] = new { StringValue = Convert.ToString(message.Header.CorrelationId), DataType = "String" }; @@ -309,34 +266,33 @@ private static PublishRequest ToPublishRequest(string topicArn, Message message) if (!string.IsNullOrEmpty(message.Header.ReplyTo)) { messageAttributes.Add(HeaderNames.ReplyTo, - new MessageAttributeValue - { - StringValue = Convert.ToString(message.Header.ReplyTo), DataType = "String" - }); + new { StringValue = Convert.ToString(message.Header.ReplyTo), DataType = "String" }); } var bagJson = JsonSerializer.Serialize(message.Header.Bag, JsonSerialisationOptions.Options); - messageAttributes[HeaderNames.Bag] = new() { StringValue = Convert.ToString(bagJson), DataType = "String" }; - request.MessageAttributes = messageAttributes; + messageAttributes[HeaderNames.Bag] = new { StringValue = Convert.ToString(bagJson), DataType = "String" }; - return request; + return new + { + TopicArn = topicArn, + message.Header.Subject, + Message = message.Body.Value, + MessageAttributes = messageAttributes + }; } - private static SendMessageRequest ToSendMessageRequest(string queueUrl, Message message) + private static object ToSendMessageRequest(string queueUrl, Message message) { - var request = new SendMessageRequest { QueueUrl = queueUrl, MessageBody = message.Body.Value }; - - var messageAttributes = new Dictionary + var messageAttributes = new Dictionary { - [HeaderNames.Id] = - new() { StringValue = message.Header.MessageId, DataType = "String" }, - [HeaderNames.Topic] = new() { StringValue = queueUrl, DataType = "String" }, - [HeaderNames.ContentType] = new() { StringValue = message.Header.ContentType, DataType = "String" }, + [HeaderNames.Id] = new { StringValue = message.Header.MessageId, DataType = "String" }, + [HeaderNames.Topic] = new { StringValue = queueUrl, DataType = "String" }, + [HeaderNames.ContentType] = new { StringValue = message.Header.ContentType, DataType = "String" }, [HeaderNames.HandledCount] = - new() { StringValue = Convert.ToString(message.Header.HandledCount), DataType = "String" }, + new { StringValue = Convert.ToString(message.Header.HandledCount), DataType = "String" }, [HeaderNames.MessageType] = - new() { StringValue = message.Header.MessageType.ToString(), DataType = "String" }, - [HeaderNames.Timestamp] = new() + new { StringValue = message.Header.MessageType.ToString(), DataType = "String" }, + [HeaderNames.Timestamp] = new { StringValue = Convert.ToString(message.Header.TimeStamp), DataType = "String" } @@ -345,70 +301,25 @@ private static SendMessageRequest ToSendMessageRequest(string queueUrl, Message if (!string.IsNullOrEmpty(message.Header.ReplyTo)) { messageAttributes.Add(HeaderNames.ReplyTo, - new() { StringValue = message.Header.ReplyTo, DataType = "String" }); + new { StringValue = message.Header.ReplyTo, DataType = "String" }); } if (!string.IsNullOrEmpty(message.Header.Subject)) { messageAttributes.Add(HeaderNames.Subject, - new() { StringValue = message.Header.Subject, DataType = "String" }); + new { StringValue = message.Header.Subject, DataType = "String" }); } if (!string.IsNullOrEmpty(message.Header.CorrelationId)) { messageAttributes.Add(HeaderNames.CorrelationId, - new() { StringValue = message.Header.CorrelationId, DataType = "String" }); + new { StringValue = message.Header.CorrelationId, DataType = "String" }); } // we can set up to 10 attributes; we have set 6 above, so use a single JSON object as the bag var bagJson = JsonSerializer.Serialize(message.Header.Bag, JsonSerialisationOptions.Options); - messageAttributes[HeaderNames.Bag] = new() { StringValue = bagJson, DataType = "String" }; - request.MessageAttributes = messageAttributes; - return request; - } - - private ValueTask LoadDefaultTopicOrQueueAsync() - { - if (s_schedulerTopicArn != null || s_schedulerQueueUrl != null) - { - return new ValueTask(); - } - - return new ValueTask(LoadDefaultTopicOrQueue()); - - async Task LoadDefaultTopicOrQueue() - { - if (Arn.IsArn(scheduler.TopicOrQueue)) - { - s_schedulerTopicArn = scheduler.TopicOrQueue; - return; - } - - if (Uri.TryCreate(scheduler.TopicOrQueue, UriKind.Absolute, out _)) - { - s_schedulerQueueUrl = scheduler.TopicOrQueue; - return; - } - - using var client = factory.CreateSnsClient(); - var topic = await client.FindTopicAsync(scheduler.TopicOrQueue); - if (topic != null) - { - s_schedulerTopicArn = topic.TopicArn; - return; - } - - try - { - using var sqsClient = factory.CreateSqsClient(); - var queue = await sqsClient.GetQueueUrlAsync(scheduler.TopicOrQueue); - s_schedulerQueueUrl = queue.QueueUrl; - } - catch (NotFoundException) - { - // case we don't find the queue we are going to ignrore it. - } - } + messageAttributes[HeaderNames.Bag] = new { StringValue = bagJson, DataType = "String" }; + return new { QueueUrl = queueUrl, MessageAttributes = messageAttributes, MessageBody = message.Body.Value }; } private async Task ScheduleAsync(Message message, DateTimeOffset at, bool async, @@ -416,7 +327,7 @@ private async Task ScheduleAsync(Message message, DateTimeOffset at, boo { await EnsureGroupExistsAsync(cancellationToken); var id = getOrCreateSchedulerId(message); - var target = await CreateTargetAsync(id, message, async, cancellationToken); + var target = await CreateTargetAsync(id, message, async); using var client = factory.CreateSchedulerClient(); try diff --git a/src/Paramore.Brighter.MessageScheduler.Aws/AwsMessageSchedulerFactory.cs b/src/Paramore.Brighter.MessageScheduler.Aws/AwsMessageSchedulerFactory.cs index 71262a9c39..c24caade97 100644 --- a/src/Paramore.Brighter.MessageScheduler.Aws/AwsMessageSchedulerFactory.cs +++ b/src/Paramore.Brighter.MessageScheduler.Aws/AwsMessageSchedulerFactory.cs @@ -1,13 +1,24 @@ -using Paramore.Brighter.MessagingGateway.AWSSQS; +using System.Net; +using Amazon; +using Amazon.IdentityManagement.Model; +using Amazon.SimpleNotificationService.Model; +using Amazon.SQS.Model; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.Tasks; namespace Paramore.Brighter.MessageScheduler.Aws; /// /// The Aws message Scheduler factory /// -public class AwsMessageSchedulerFactory(AWSMessagingGatewayConnection connection, string role, RoutingKey topicOrQueue) +public class AwsMessageSchedulerFactory(AWSMessagingGatewayConnection connection, string role) : IAmAMessageSchedulerFactory { + private readonly SemaphoreSlim _semaphore = new(1, 1); + private string? _roleArn; + private string? _topicArn; + private string? _queueUrl; + /// /// The AWS Scheduler group /// @@ -24,10 +35,10 @@ public class AwsMessageSchedulerFactory(AWSMessagingGatewayConnection connection public int? FlexibleTimeWindowMinutes { get; set; } /// - /// The topic or queue that Brighter should use for messaging scheduler + /// The topic or queue that Brighter should use for publishing/sending messaging scheduler /// It can be Topic Name/ARN or Queue Name/Url /// - public RoutingKey TopicOrQueue { get; set; } = topicOrQueue; + public RoutingKey SchedulerTopicOrQueue { get; set; } = RoutingKey.Empty; /// /// The AWS Role Name/ARN @@ -37,22 +48,208 @@ public class AwsMessageSchedulerFactory(AWSMessagingGatewayConnection connection /// /// Allow Brighter to give a priority to as destiny topic, in case it exists. /// - public bool UseMessageTopicAsTarget { get; set; } - + public bool UseMessageTopicAsTarget { get; set; } = true; + /// /// Action to be performed when a conflict happen during scheduler creating /// public OnSchedulerConflict OnConflict { get; set; } - public IAmAMessageScheduler Create(IAmACommandProcessor processor) - => new AwsMessageScheduler(new AWSClientFactory(connection), GetOrCreateSchedulerId, + /// + /// Action to be performed when checking role + /// + public OnMissingRole MakeRole { get; set; } + + public IAmAMessageScheduler Create(IAmACommandProcessor processor) + { + var factory = new AWSClientFactory(connection); + var roleArn = GetOrCreateRoleArnAsync(factory); + if (!roleArn.IsCompleted) + { + BrighterAsyncContext.Run(async () => await roleArn); + } + + var topicArn = GetTopicArnAsync(factory); + if (!topicArn.IsCompleted) + { + BrighterAsyncContext.Run(async () => await topicArn); + } + + var queueUrl = GetQueueUrlAsync(factory); + if (!queueUrl.IsCompleted) + { + BrighterAsyncContext.Run(async () => await queueUrl); + } + + return new AwsMessageScheduler(new AWSClientFactory(connection), GetOrCreateSchedulerId, new Scheduler { - Role = Role, - TopicOrQueue = TopicOrQueue, - UseMessageTopicAsTarget = UseMessageTopicAsTarget, + RoleArn = roleArn.Result, + TopicArn = topicArn.Result, + QueueUrl = queueUrl.Result, + Topic = SchedulerTopicOrQueue, OnConflict = OnConflict, + UseMessageTopicAsTarget = UseMessageTopicAsTarget, FlexibleTimeWindowMinutes = FlexibleTimeWindowMinutes }, Group); + } + + private ValueTask GetTopicArnAsync(AWSClientFactory factory) + { + if (_topicArn != null) + { + return new ValueTask(_topicArn); + } + + if (Arn.IsArn(SchedulerTopicOrQueue)) + { + _topicArn = SchedulerTopicOrQueue; + return new ValueTask(_topicArn); + } + + return new ValueTask(GetFromSnsTopicArnAsync()); + + async Task GetFromSnsTopicArnAsync() + { + using var client = factory.CreateSnsClient(); + var topic = await client.FindTopicAsync(SchedulerTopicOrQueue); + _topicArn = topic?.TopicArn ?? ""; + return _topicArn; + } + } + + private ValueTask GetQueueUrlAsync(AWSClientFactory factory) + { + { + if (_queueUrl != null) + { + return new ValueTask(_queueUrl); + } + + if (Uri.TryCreate(SchedulerTopicOrQueue, UriKind.Absolute, out _)) + { + _queueUrl = SchedulerTopicOrQueue; + return new ValueTask(_queueUrl); + } + + return new ValueTask(GetFromSqsQueueUrlAsync()); + + async Task GetFromSqsQueueUrlAsync() + { + using var client = factory.CreateSqsClient(); + try + { + var queue = await client.GetQueueUrlAsync(SchedulerTopicOrQueue); + _queueUrl = queue.QueueUrl; + } + catch (QueueDoesNotExistException) + { + _queueUrl = ""; + } + + return _queueUrl; + } + } + } + + private ValueTask GetOrCreateRoleArnAsync(AWSClientFactory factory) + { + if (_roleArn != null) + { + return new ValueTask(_roleArn); + } + + if (Arn.IsArn(Role)) + { + return new ValueTask(Role); + } + + return new ValueTask(GetOrCreateRoleArnFromIdentityAsync()); + + async Task GetOrCreateRoleArnFromIdentityAsync() + { + await _semaphore.WaitAsync(); + try + { + using var client = factory.CreateIdentityClient(); + var role = await client.GetRoleAsync(new GetRoleRequest { RoleName = Role }); + + if (role.HttpStatusCode == HttpStatusCode.OK) + { + _roleArn = role.Role.Arn; + return _roleArn; + } + + if (MakeRole == OnMissingRole.AssumeRole) + { + throw new InvalidOperationException($"Role '{Role}' not found"); + } + + return await CreateRoleArnAsync(); + } + catch (NoSuchEntityException) + { + if (MakeRole == OnMissingRole.AssumeRole) + { + throw new InvalidOperationException($"Role '{Role}' not found"); + } + + return await CreateRoleArnAsync(); + } + finally + { + _semaphore.Release(); + } + } + + async Task CreateRoleArnAsync() + { + using var client = factory.CreateIdentityClient(); + var role = await client.CreateRoleAsync(new CreateRoleRequest + { + RoleName = Role, + AssumeRolePolicyDocument = """ + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "scheduler.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] + } + """ + }); + + var policy = await client.CreatePolicyAsync(new CreatePolicyRequest + { + PolicyDocument = """ + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "sqs:SendMessage", + "sns:Publish" + ], + "Resource": ["*"] + }] + } + """, + }); + + await client.AttachRolePolicyAsync(new AttachRolePolicyRequest + { + RoleName = Role, PolicyArn = policy.Policy.Arn + }); + + _roleArn = role.Role.Arn; + return _roleArn; + } + } } diff --git a/src/Paramore.Brighter.MessageScheduler.Aws/OnMissingRole.cs b/src/Paramore.Brighter.MessageScheduler.Aws/OnMissingRole.cs new file mode 100644 index 0000000000..5cb2f5efff --- /dev/null +++ b/src/Paramore.Brighter.MessageScheduler.Aws/OnMissingRole.cs @@ -0,0 +1,17 @@ +namespace Paramore.Brighter.MessageScheduler.Aws; + +/// +/// Action to be performed when checking role +/// +public enum OnMissingRole +{ + /// + /// Assume the role if it exists + /// + AssumeRole, + /// + /// Create the role if it does not exist + /// + CreateRole +} + diff --git a/src/Paramore.Brighter.MessageScheduler.Aws/Scheduler.cs b/src/Paramore.Brighter.MessageScheduler.Aws/Scheduler.cs index c58237a99a..70c872f951 100644 --- a/src/Paramore.Brighter.MessageScheduler.Aws/Scheduler.cs +++ b/src/Paramore.Brighter.MessageScheduler.Aws/Scheduler.cs @@ -9,28 +9,22 @@ namespace Paramore.Brighter.MessageScheduler.Aws; public class Scheduler { /// - /// The AWS Role ARN + /// The role ARN /// - public string Role { get; init; } = string.Empty; + public string RoleArn { get; init; } = string.Empty; /// /// The flexible time window /// public int? FlexibleTimeWindowMinutes { get; init; } - - /// - /// The topic ARN or Queue Url - /// - public RoutingKey TopicOrQueue { get; init; } = RoutingKey.Empty; - /// - /// Allow Brighter to give a priority to as destiny topic, in case it exists. - /// + public RoutingKey Topic { get; set; } = RoutingKey.Empty; + + public string TopicArn { get; init; } = string.Empty; + public string QueueUrl { get; init; } = string.Empty; + public bool UseMessageTopicAsTarget { get; set; } - - /// - /// Action to be performed when a conflict happen during scheduler creating - /// + public OnSchedulerConflict OnConflict { get; init; } internal FlexibleTimeWindow ToFlexibleTimeWindow() diff --git a/src/Paramore.Brighter/Message.cs b/src/Paramore.Brighter/Message.cs index 5763a58697..ba48b83a30 100644 --- a/src/Paramore.Brighter/Message.cs +++ b/src/Paramore.Brighter/Message.cs @@ -125,8 +125,8 @@ public Message() [JsonConstructor] public Message(MessageHeader header, MessageBody body) { - Header = header; Body = body; + Header = header; Header.ContentType = string.IsNullOrEmpty(Header.ContentType) ? Body.ContentType: Header.ContentType; } diff --git a/tests/Paramore.Brighter.AWS.Tests/Helpers/GatewayFactory.cs b/tests/Paramore.Brighter.AWS.Tests/Helpers/GatewayFactory.cs index 76cc08cf87..48575c4897 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Helpers/GatewayFactory.cs +++ b/tests/Paramore.Brighter.AWS.Tests/Helpers/GatewayFactory.cs @@ -30,11 +30,11 @@ public static AWSMessagingGatewayConnection CreateFactory( { config?.Invoke(cfg); - var serviceURL = Environment.GetEnvironmentVariable("LOCALSTACK_SERVICE_URL"); + /*var serviceURL = Environment.GetEnvironmentVariable("LOCALSTACK_SERVICE_URL"); if (!string.IsNullOrWhiteSpace(serviceURL)) { cfg.ServiceURL = serviceURL; - } + }*/ }); } } diff --git a/tests/Paramore.Brighter.AWS.Tests/Helpers/Role.cs b/tests/Paramore.Brighter.AWS.Tests/Helpers/Role.cs deleted file mode 100644 index eeb503c0d5..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/Helpers/Role.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Net; -using System.Threading.Tasks; -using Amazon.IdentityManagement.Model; -using Paramore.Brighter.MessagingGateway.AWSSQS; - -namespace Paramore.Brighter.AWS.Tests.Helpers; - -public static class Role -{ - public static async Task GetOrCreateRoleAsync(AWSClientFactory factory, string roleName) - { - using var client = factory.CreateIdentityClient(); - try - { - var role = await client.GetRoleAsync(new GetRoleRequest { RoleName = roleName }); - if (role.HttpStatusCode == HttpStatusCode.OK) - { - return role.Role.Arn; - } - } - catch - { - // We are going to create case do not exists - } - - var createRoleResponse = await client.CreateRoleAsync(new CreateRoleRequest - { - RoleName = roleName, - AssumeRolePolicyDocument = """ - { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": "scheduler.amazonaws.com" - }, - "Action": "sts:AssumeRole" - }, - { - "Effect": "Allow", - "Resource": "*", - "Action": "*" - }] - } - """ - }); - return createRoleResponse.Role.Arn; - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sns/When_Scheduling_A_Sns_Message.cs b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sns/When_Scheduling_A_Sns_Message.cs new file mode 100644 index 0000000000..6d645f5a86 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sns/When_Scheduling_A_Sns_Message.cs @@ -0,0 +1,103 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessageScheduler.Aws; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessageScheduler.Sns; + +public class SnsSchedulingMessageTest : IDisposable +{ + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private readonly SnsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _topicName; + private readonly ChannelFactory _channelFactory; + private readonly IAmAMessageSchedulerFactory _factory; + + public SnsSchedulingMessageTest() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + //we need the channel to create the queues and notifications + _topicName = $"Producer-Scheduler-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var channelName = $"Producer-Scheduler-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(_topicName); + + var channel = _channelFactory.CreateSyncChannel(new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + bufferSize: BufferSize, + makeChannels: OnMissingChannel.Create + )); + + //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel + //just for the tests, so create a new consumer from the properties + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(), BufferSize); + _messageProducer = new SnsMessageProducer(awsConnection, + new SnsPublication { MakeChannels = OnMissingChannel.Create }); + + // Enforce topic to be created + _messageProducer.Send(new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + )); + + _consumer.Purge(); + + _factory = new AwsMessageSchedulerFactory(awsConnection, "brighter-scheduler") + { + UseMessageTopicAsTarget = true, MakeRole = OnMissingRole.CreateRole + }; + } + + [Fact] + public void When_Scheduling_A_Sns_Message() + { + var routingKey = new RoutingKey(_topicName); + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + ); + + var scheduler = (IAmAMessageSchedulerSync)_factory.Create(null!); + scheduler.Schedule(message, TimeSpan.FromMinutes(1)); + + Task.Delay(TimeSpan.FromMinutes(1)).Wait(); + + var stopAt = DateTimeOffset.UtcNow.AddMinutes(2); + while (stopAt > DateTimeOffset.UtcNow) + { + var messages = _consumer.Receive(); + messages.Should().ContainSingle(); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + { + messages[0].Body.Value.Should().Be(message.Body.Value); + messages[0].Header.Should().BeEquivalentTo(message.Header); + _consumer.Acknowledge(messages[0]); + return; + } + + Task.Delay(TimeSpan.FromSeconds(1)).Wait(); + } + + Assert.Fail("The message wasn't fired"); + } + + public void Dispose() + { + _channelFactory.DeleteQueueAsync().GetAwaiter().GetResult(); + _channelFactory.DeleteTopicAsync().GetAwaiter().GetResult(); + _messageProducer.Dispose(); + _consumer.Dispose(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sns/When_Scheduling_A_Sns_Message_Async.cs b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sns/When_Scheduling_A_Sns_Message_Async.cs new file mode 100644 index 0000000000..cfc5bbf830 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sns/When_Scheduling_A_Sns_Message_Async.cs @@ -0,0 +1,102 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessageScheduler.Aws; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessageScheduler.Sns; + +public class SnsSchedulingAsyncMessageTest : IAsyncDisposable +{ + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private readonly SnsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _topicName; + private readonly ChannelFactory _channelFactory; + private readonly IAmAMessageSchedulerFactory _factory; + + public SnsSchedulingAsyncMessageTest() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + //we need the channel to create the queues and notifications + _topicName = $"Producer-Scheduler-Async-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var channelName = $"Producer-Scheduler-Async-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(_topicName); + + var channel = _channelFactory.CreateAsyncChannelAsync(new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + bufferSize: BufferSize, + makeChannels: OnMissingChannel.Create + )).GetAwaiter().GetResult(); + + //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel + //just for the tests, so create a new consumer from the properties + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(), BufferSize); + _messageProducer = new SnsMessageProducer(awsConnection, + new SnsPublication { MakeChannels = OnMissingChannel.Create }); + + // Enforce topic to be created + _messageProducer.SendAsync(new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + )).GetAwaiter().GetResult(); + + _consumer.Purge(); + + _factory = new AwsMessageSchedulerFactory(awsConnection, "brighter-scheduler") + { + UseMessageTopicAsTarget = true, + MakeRole = OnMissingRole.CreateRole + }; + } + + [Fact] + public async Task When_Scheduling_A_Sns_Message_Async() + { + var routingKey = new RoutingKey(_topicName); + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + ); + + var scheduler = (IAmAMessageSchedulerAsync)_factory.Create(null!); + await scheduler.ScheduleAsync(message, TimeSpan.FromMinutes(1)); + await Task.Delay(TimeSpan.FromMinutes(1)); + var stopAt = DateTimeOffset.UtcNow.AddMinutes(2); + while (stopAt > DateTimeOffset.UtcNow) + { + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMinutes(1)); + messages.Should().ContainSingle(); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + { + messages[0].Body.Value.Should().Be(message.Body.Value); + messages[0].Header.Should().BeEquivalentTo(message.Header, opt => opt.Excluding(x => x.Bag)); + await _consumer.AcknowledgeAsync(messages[0]); + return; + } + + await Task.Delay(TimeSpan.FromSeconds(1)); + } + + Assert.Fail("The message wasn't fired"); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteQueueAsync(); + await _channelFactory.DeleteTopicAsync(); + await _messageProducer.DisposeAsync(); + await _consumer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sns/When_Scheduling_A_Sns_Message_Via_FireScheduler.cs b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sns/When_Scheduling_A_Sns_Message_Via_FireScheduler.cs new file mode 100644 index 0000000000..c20040dfde --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sns/When_Scheduling_A_Sns_Message_Via_FireScheduler.cs @@ -0,0 +1,108 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.MessageScheduler.Aws; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.Scheduler.Events; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessageScheduler.Sns; + +public class SnsSchedulingMessageViaFireSchedulerTest : IDisposable +{ + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private readonly SnsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _topicName; + private readonly ChannelFactory _channelFactory; + private readonly IAmAMessageSchedulerFactory _factory; + + public SnsSchedulingMessageViaFireSchedulerTest() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + //we need the channel to create the queues and notifications + _topicName = $"Producer-Fire-Scheduler-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var channelName = $"Producer-Fire-Scheduler-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(_topicName); + + var channel = _channelFactory.CreateSyncChannel(new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + bufferSize: BufferSize, + makeChannels: OnMissingChannel.Create + )); + + //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel + //just for the tests, so create a new consumer from the properties + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(), BufferSize); + _messageProducer = + new SnsMessageProducer(awsConnection, new SnsPublication { MakeChannels = OnMissingChannel.Create }); + + // Enforce topic to be created + _messageProducer.Send(new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + )); + _consumer.Purge(); + + _factory = new AwsMessageSchedulerFactory(awsConnection, "brighter-scheduler") + { + UseMessageTopicAsTarget = false, MakeRole = OnMissingRole.CreateRole, SchedulerTopicOrQueue = routingKey + }; + } + + [Fact] + public void When_Scheduling_A_Sns_Message_With_Delay_Via_FireScheduler() + { + var routingKey = new RoutingKey(_topicName); + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + ); + + var scheduler = (IAmAMessageSchedulerSync)_factory.Create(null!); + scheduler.Schedule(message, TimeSpan.FromMinutes(1)); + + Task.Delay(TimeSpan.FromMinutes(1)).Wait(); + + var stopAt = DateTimeOffset.UtcNow.AddMinutes(2); + while (stopAt > DateTimeOffset.UtcNow) + { + var messages = _consumer.Receive(TimeSpan.FromMinutes(1)); + messages.Should().ContainSingle(); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + { + messages[0].Header.MessageType.Should().Be(MessageType.MT_COMMAND); + messages[0].Body.Value.Should().NotBeNullOrEmpty(); + var m = JsonSerializer.Deserialize(messages[0].Body.Value, + JsonSerialisationOptions.Options); + m.Should().NotBeNull(); + m.Message.Should().BeEquivalentTo(message); + m.Async.Should().BeFalse(); + _consumer.Acknowledge(messages[0]); + return; + } + + Task.Delay(TimeSpan.FromSeconds(1)).Wait(); + } + + Assert.Fail("The message wasn't fired"); + } + + public void Dispose() + { + _channelFactory.DeleteQueueAsync().GetAwaiter().GetResult(); + _channelFactory.DeleteTopicAsync().GetAwaiter().GetResult(); + _messageProducer.Dispose(); + _consumer.Dispose(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sns/When_Scheduling_A_Sns_Message_Via_FireScheduler_Async.cs b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sns/When_Scheduling_A_Sns_Message_Via_FireScheduler_Async.cs new file mode 100644 index 0000000000..55d05af70f --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sns/When_Scheduling_A_Sns_Message_Via_FireScheduler_Async.cs @@ -0,0 +1,108 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.MessageScheduler.Aws; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.Scheduler.Events; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessageScheduler.Sns; + +public class SnsSchedulingMessageViaFireSchedulerAsyncTest : IDisposable +{ + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private readonly SnsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _topicName; + private readonly ChannelFactory _channelFactory; + private readonly IAmAMessageSchedulerFactory _factory; + + public SnsSchedulingMessageViaFireSchedulerAsyncTest() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + //we need the channel to create the queues and notifications + _topicName = $"Producer-Fire-Scheduler-Async-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var channelName = $"Producer-Fire-Scheduler-Async-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(_topicName); + + var channel = _channelFactory.CreateAsyncChannel(new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + bufferSize: BufferSize, + makeChannels: OnMissingChannel.Create + )); + + //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel + //just for the tests, so create a new consumer from the properties + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(), BufferSize); + _messageProducer = + new SnsMessageProducer(awsConnection, new SnsPublication { MakeChannels = OnMissingChannel.Create }); + + // Enforce topic to be created + _messageProducer.Send(new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + )); + _consumer.Purge(); + + _factory = new AwsMessageSchedulerFactory(awsConnection, "brighter-scheduler") + { + UseMessageTopicAsTarget = false, MakeRole = OnMissingRole.CreateRole, SchedulerTopicOrQueue = routingKey + }; + } + + [Fact] + public async Task When_Scheduling_A_Sns_Message_Async() + { + var routingKey = new RoutingKey(_topicName); + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + ); + + var scheduler = (IAmAMessageSchedulerAsync)_factory.Create(null!); + await scheduler.ScheduleAsync(message, TimeSpan.FromMinutes(1)); + + await Task.Delay(TimeSpan.FromMinutes(1)); + + var stopAt = DateTimeOffset.UtcNow.AddMinutes(2); + while (stopAt > DateTimeOffset.UtcNow) + { + var messages = await _consumer.ReceiveAsync(); + messages.Should().ContainSingle(); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + { + messages[0].Header.MessageType.Should().Be(MessageType.MT_COMMAND); + messages[0].Body.Value.Should().NotBeNullOrEmpty(); + var m = JsonSerializer.Deserialize(messages[0].Body.Value, + JsonSerialisationOptions.Options); + m.Should().NotBeNull(); + m.Message.Should().BeEquivalentTo(message); + m.Async.Should().BeTrue(); + await _consumer.AcknowledgeAsync(messages[0]); + return; + } + + await Task.Delay(TimeSpan.FromSeconds(1)); + } + + Assert.Fail("The message wasn't fired"); + } + + public void Dispose() + { + _channelFactory.DeleteQueueAsync().GetAwaiter().GetResult(); + _channelFactory.DeleteTopicAsync().GetAwaiter().GetResult(); + _messageProducer.Dispose(); + _consumer.Dispose(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sqs/When_Scheduling_A_Sqs_Message.cs b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sqs/When_Scheduling_A_Sqs_Message.cs new file mode 100644 index 0000000000..1b454e136d --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sqs/When_Scheduling_A_Sqs_Message.cs @@ -0,0 +1,95 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessageScheduler.Aws; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessageScheduler.Sqs; + +public class SqsSchedulingMessageTest : IDisposable +{ + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private readonly SqsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _queueName; + private readonly ChannelFactory _channelFactory; + private readonly IAmAMessageSchedulerFactory _factory; + + public SqsSchedulingMessageTest() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var subscriptionName = $"Buffered-Scheduler-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _queueName = $"Buffered-Scheduler-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + + //we need the channel to create the queues and notifications + var routingKey = new RoutingKey(_queueName); + + var channel = _channelFactory.CreateSyncChannel(new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(_queueName), + routingKey: routingKey, + bufferSize: BufferSize, + makeChannels: OnMissingChannel.Create, + channelType: ChannelType.PointToPoint + )); + + //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel + //just for the tests, so create a new consumer from the properties + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(), BufferSize); + _messageProducer = new SqsMessageProducer(awsConnection, + new SqsPublication { MakeChannels = OnMissingChannel.Create }); + + _factory = new AwsMessageSchedulerFactory(awsConnection, "brighter-scheduler") + { + UseMessageTopicAsTarget = true, MakeRole = OnMissingRole.CreateRole + }; + } + + [Fact] + public void When_Scheduling_A_Sqs_Message() + { + var routingKey = new RoutingKey(_queueName); + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + ); + + var scheduler = (IAmAMessageSchedulerSync)_factory.Create(null!)!; + scheduler.Schedule(message, TimeSpan.FromMinutes(1)); + + Task.Delay(TimeSpan.FromMinutes(1)).Wait(); + + var stopAt = DateTimeOffset.UtcNow.AddMinutes(2); + while (stopAt > DateTimeOffset.UtcNow) + { + var messages = _consumer.Receive(TimeSpan.FromMinutes(1)); + messages.Should().ContainSingle(); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + { + messages[0].Body.Value.Should().Be(message.Body.Value); + messages[0].Header.Should().BeEquivalentTo(message.Header); + _consumer.Acknowledge(messages[0]); + return; + } + + Task.Delay(TimeSpan.FromSeconds(1)).Wait(); + } + + Assert.Fail("The message wasn't fired"); + } + + public void Dispose() + { + _channelFactory.DeleteQueueAsync().GetAwaiter().GetResult(); + _messageProducer.Dispose(); + _consumer.Dispose(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sqs/When_Scheduling_A_Sqs_Message_Async.cs b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sqs/When_Scheduling_A_Sqs_Message_Async.cs new file mode 100644 index 0000000000..3bc6b8b1a0 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sqs/When_Scheduling_A_Sqs_Message_Async.cs @@ -0,0 +1,96 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessageScheduler.Aws; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessageScheduler.Sqs; + +public class SqsSchedulingAsyncMessageTest : IAsyncDisposable +{ + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private readonly SqsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _queueName; + private readonly ChannelFactory _channelFactory; + private readonly IAmAMessageSchedulerFactory _factory; + + public SqsSchedulingAsyncMessageTest() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var subscriptionName = $"Buffered-Scheduler-Async-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _queueName = $"Buffered-Scheduler-Async-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + + //we need the channel to create the queues and notifications + var routingKey = new RoutingKey(_queueName); + + var channel = _channelFactory.CreateAsyncChannelAsync(new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(_queueName), + routingKey: routingKey, + bufferSize: BufferSize, + makeChannels: OnMissingChannel.Create, + channelType: ChannelType.PointToPoint + )).GetAwaiter().GetResult(); + + //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel + //just for the tests, so create a new consumer from the properties + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(), BufferSize); + _messageProducer = new SqsMessageProducer(awsConnection, + new SqsPublication { MakeChannels = OnMissingChannel.Create }); + + _factory = new AwsMessageSchedulerFactory(awsConnection, "brighter-scheduler") + { + UseMessageTopicAsTarget = true + }; + } + + [Fact] + public async Task When_Scheduling_A_Sqs_Message_Async() + { + var routingKey = new RoutingKey(_queueName); + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + ); + + var scheduler = (IAmAMessageSchedulerAsync)_factory.Create(null!)!; + await scheduler.ScheduleAsync(message, TimeSpan.FromMinutes(1)); + + await Task.Delay(TimeSpan.FromMinutes(1)); + + var stopAt = DateTimeOffset.UtcNow.AddMinutes(2); + while (stopAt > DateTimeOffset.UtcNow) + { + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMinutes(1)); + messages.Should().ContainSingle(); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + { + messages[0].Body.Value.Should().Be(message.Body.Value); + messages[0].Header.Should().BeEquivalentTo(message.Header); + await _consumer.AcknowledgeAsync(messages[0]); + return; + } + + + await Task.Delay(TimeSpan.FromSeconds(1)); + } + + Assert.Fail("The message wasn't fired"); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteQueueAsync(); + await _messageProducer.DisposeAsync(); + await _consumer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sqs/When_Scheduling_A_Sqs_Message_Via_FireScheduler.cs b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sqs/When_Scheduling_A_Sqs_Message_Via_FireScheduler.cs new file mode 100644 index 0000000000..5c61750ec6 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sqs/When_Scheduling_A_Sqs_Message_Via_FireScheduler.cs @@ -0,0 +1,105 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessageScheduler.Aws; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.Scheduler.Events; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessageScheduler.Sqs; + +[Collection("Scheduler SQS")] +public class SqsSchedulingMessageViaFireSchedulerTest : IDisposable +{ + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private readonly SqsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _queueName; + private readonly ChannelFactory _channelFactory; + private readonly IAmAMessageSchedulerFactory _factory; + + public SqsSchedulingMessageViaFireSchedulerTest() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var subscriptionName = $"Buffered-Scheduler-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _queueName = $"Buffered-Scheduler-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + + //we need the channel to create the queues and notifications + var routingKey = new RoutingKey(_queueName); + + var channel = _channelFactory.CreateSyncChannel(new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(_queueName), + routingKey: routingKey, + bufferSize: BufferSize, + makeChannels: OnMissingChannel.Create, + channelType: ChannelType.PointToPoint + )); + + //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel + //just for the tests, so create a new consumer from the properties + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(), BufferSize); + _messageProducer = new SqsMessageProducer(awsConnection, + new SqsPublication { MakeChannels = OnMissingChannel.Create }); + + _factory = new AwsMessageSchedulerFactory(awsConnection, "brighter-scheduler") + { + UseMessageTopicAsTarget = false, + MakeRole = OnMissingRole.CreateRole, + SchedulerTopicOrQueue = routingKey + }; + } + + [Fact] + public void When_Scheduling_A_Sqs_Message_Via_FireScheduler() + { + var routingKey = new RoutingKey(_queueName); + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + ); + + var scheduler = (IAmAMessageSchedulerSync)_factory.Create(null!)!; + scheduler.Schedule(message, TimeSpan.FromMinutes(1)); + + Task.Delay(TimeSpan.FromMinutes(1)).Wait(); + + var stopAt = DateTimeOffset.UtcNow.AddMinutes(2); + while (stopAt > DateTimeOffset.UtcNow) + { + var messages = _consumer.Receive(TimeSpan.FromMinutes(1)); + messages.Should().ContainSingle(); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + { + messages[0].Header.MessageType.Should().Be(MessageType.MT_COMMAND); + messages[0].Body.Value.Should().NotBeNullOrEmpty(); + var m = JsonSerializer.Deserialize(messages[0].Body.Value, + JsonSerialisationOptions.Options); + m.Should().NotBeNull(); + m.Message.Should().BeEquivalentTo(message); + m.Async.Should().BeFalse(); + _consumer.Acknowledge(messages[0]); + return; + } + + Task.Delay(TimeSpan.FromSeconds(1)).Wait(); + } + + Assert.Fail("The message wasn't fired"); + } + + public void Dispose() + { + _channelFactory.DeleteQueueAsync().GetAwaiter().GetResult(); + _messageProducer.Dispose(); + _consumer.Dispose(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sqs/When_Scheduling_A_Sqs_Message_Via_FireScheduler_Async.cs b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sqs/When_Scheduling_A_Sqs_Message_Via_FireScheduler_Async.cs new file mode 100644 index 0000000000..8de54d3c9d --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/Sqs/When_Scheduling_A_Sqs_Message_Via_FireScheduler_Async.cs @@ -0,0 +1,103 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessageScheduler.Aws; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.Scheduler.Events; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessageScheduler.Sqs; + +public class SqsSchedulingAsyncMessageViaFireSchedulerTest : IAsyncDisposable +{ + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private readonly SqsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _queueName; + private readonly ChannelFactory _channelFactory; + private readonly IAmAMessageSchedulerFactory _factory; + + public SqsSchedulingAsyncMessageViaFireSchedulerTest() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var subscriptionName = $"Buffered-Scheduler-Async-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _queueName = $"Buffered-Scheduler-Async-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + + //we need the channel to create the queues and notifications + var routingKey = new RoutingKey(_queueName); + + var channel = _channelFactory.CreateAsyncChannelAsync(new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(_queueName), + routingKey: routingKey, + bufferSize: BufferSize, + makeChannels: OnMissingChannel.Create, + channelType: ChannelType.PointToPoint + )).GetAwaiter().GetResult(); + + //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel + //just for the tests, so create a new consumer from the properties + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(), BufferSize); + _messageProducer = new SqsMessageProducer(awsConnection, + new SqsPublication { MakeChannels = OnMissingChannel.Create }); + + _factory = new AwsMessageSchedulerFactory(awsConnection, "brighter-scheduler") + { + UseMessageTopicAsTarget = false, MakeRole = OnMissingRole.CreateRole, SchedulerTopicOrQueue = routingKey + }; + } + + [Fact] + public async Task When_Scheduling_A_Sqs_Message_Via_FireScheduler_Async() + { + var routingKey = new RoutingKey(_queueName); + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + ); + + var scheduler = (IAmAMessageSchedulerAsync)_factory.Create(null!)!; + await scheduler.ScheduleAsync(message, TimeSpan.FromMinutes(1)); + + await Task.Delay(TimeSpan.FromMinutes(1)); + + var stopAt = DateTimeOffset.UtcNow.AddMinutes(2); + while (stopAt > DateTimeOffset.UtcNow) + { + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMinutes(1)); + messages.Should().ContainSingle(); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + { + messages[0].Header.MessageType.Should().Be(MessageType.MT_COMMAND); + messages[0].Body.Value.Should().NotBeNullOrEmpty(); + var m = JsonSerializer.Deserialize(messages[0].Body.Value, + JsonSerialisationOptions.Options); + m.Should().NotBeNull(); + m.Message.Should().BeEquivalentTo(message); + m.Async.Should().BeTrue(); + await _consumer.AcknowledgeAsync(messages[0]); + return; + } + + + await Task.Delay(TimeSpan.FromSeconds(1)); + } + + Assert.Fail("The message wasn't fired"); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteQueueAsync(); + await _messageProducer.DisposeAsync(); + await _consumer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/When_Scheduling_A_Message.cs b/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/When_Scheduling_A_Message.cs deleted file mode 100644 index 27ab10c982..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessageScheduler/When_Scheduling_A_Message.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Transactions; -using System.Xml; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessageScheduler.Aws; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Paramore.Brighter.Observability; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessageScheduler; - -public class SnsSchedulingMessageTest -{ - private const string ContentType = "text\\plain"; - private readonly SnsMessageProducer _messageProducer; - private readonly SqsMessageConsumer _consumer; - private readonly string _topicName; - private readonly ChannelFactory _channelFactory; - private readonly AwsMessageSchedulerFactory _factory; - - - public SnsSchedulingMessageTest() - { - var awsConnection = GatewayFactory.CreateFactory(); - - _channelFactory = new ChannelFactory(awsConnection); - var channelName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - _topicName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - - //we need the channel to create the queues and notifications - var routingKey = new RoutingKey(_topicName); - - var channel = _channelFactory.CreateSyncChannel(new SqsSubscription( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - makeChannels: OnMissingChannel.Create - )); - - //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel - //just for the tests, so create a new consumer from the properties - _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName()); - _messageProducer = new SnsMessageProducer(awsConnection, new SnsPublication { MakeChannels = OnMissingChannel.Create }); - - var role = Role - .GetOrCreateRoleAsync(new AWSClientFactory(awsConnection), "test-scheduler") - .GetAwaiter() - .GetResult(); - _factory = new AwsMessageSchedulerFactory(awsConnection, role, new RoutingKey("scheduler")) - { - UseMessageTopicAsTarget = true - }; - } - - [Fact] - public void Test() - { - var routingKey = new RoutingKey(_topicName); - var message = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, - correlationId: Guid.NewGuid().ToString(), contentType: ContentType), - new MessageBody("test content one") - ); - - var scheduler = (IAmAMessageSchedulerAsync)_factory.Create(null!)!; - scheduler.ScheduleAsync(message, DateTimeOffset.UtcNow.Add(TimeSpan.FromSeconds(1))); - var messages = _consumer.Receive(TimeSpan.FromSeconds(2)); - messages.Should().ContainSingle(); - messages[0].Body.Value.Should().Be(message.Body.Value); - messages[0].Header.Should().BeEquivalentTo(message.Header); - } - - internal class EmptyHandlerFactorySync : IAmAHandlerFactorySync - { - public IHandleRequests Create(Type handlerType, IAmALifetime lifetime) - { - return null; - } - - public void Release(IHandleRequests handler, IAmALifetime lifetime) { } - } -} From b908bb9fbf37d0feb571b71b7ae7240679aaa70b Mon Sep 17 00:00:00 2001 From: Rafael Andrade Date: Fri, 7 Feb 2025 11:09:09 +0000 Subject: [PATCH 17/17] Add Scheduler Samples --- Brighter.sln | 99 ++++++++++++++ Directory.Packages.props | 1 + docker-compose-localstack.yaml | 3 +- .../AwsTaskQueue/Greetings/Greetings.csproj | 15 +++ .../CommandHandlers/FarewellEventHandler.cs | 24 ++++ .../CommandHandlers/GreetingEventHandler.cs | 43 ++++++ .../Greetings/Ports/Commands/FarewellEvent.cs | 19 +++ .../Greetings/Ports/Commands/GreetingEvent.cs | 41 ++++++ .../Mappers/FarewellEventMessageMapper.cs | 51 +++++++ .../Mappers/GreetingEventMessageMapper.cs | 51 +++++++ .../GreetingsPumper/GreetingsPumper.csproj | 23 ++++ .../AwsTaskQueue/GreetingsPumper/Program.cs | 116 ++++++++++++++++ .../GreetingsReceiverConsole.csproj | 21 +++ .../GreetingsReceiverConsole/Program.cs | 115 ++++++++++++++++ .../Greetings/Greetings.csproj | 15 +++ .../CommandHandlers/FarewellEventHandler.cs | 23 ++++ .../CommandHandlers/GreetingEventHandler.cs | 42 ++++++ .../Greetings/Ports/Commands/FarewellEvent.cs | 18 +++ .../Greetings/Ports/Commands/GreetingEvent.cs | 40 ++++++ .../Mappers/FarewellEventMessageMapper.cs | 50 +++++++ .../Mappers/GreetingEventMessageMapper.cs | 50 +++++++ .../GreetingsPumper/GreetingsPumper.csproj | 24 ++++ .../GreetingsPumper/Program.cs | 124 ++++++++++++++++++ .../GreetingsReceiverConsole.csproj | 20 +++ .../GreetingsReceiverConsole/Program.cs | 108 +++++++++++++++ .../ServiceCollectionExtensions.cs | 20 ++- .../QuartzBrighterJob.cs | 16 +-- .../QuartzMessageScheduler.cs | 15 ++- 28 files changed, 1165 insertions(+), 22 deletions(-) create mode 100644 samples/Scheduler/AwsTaskQueue/Greetings/Greetings.csproj create mode 100644 samples/Scheduler/AwsTaskQueue/Greetings/Ports/CommandHandlers/FarewellEventHandler.cs create mode 100644 samples/Scheduler/AwsTaskQueue/Greetings/Ports/CommandHandlers/GreetingEventHandler.cs create mode 100644 samples/Scheduler/AwsTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs create mode 100644 samples/Scheduler/AwsTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs create mode 100644 samples/Scheduler/AwsTaskQueue/Greetings/Ports/Mappers/FarewellEventMessageMapper.cs create mode 100644 samples/Scheduler/AwsTaskQueue/Greetings/Ports/Mappers/GreetingEventMessageMapper.cs create mode 100644 samples/Scheduler/AwsTaskQueue/GreetingsPumper/GreetingsPumper.csproj create mode 100644 samples/Scheduler/AwsTaskQueue/GreetingsPumper/Program.cs create mode 100644 samples/Scheduler/AwsTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj create mode 100644 samples/Scheduler/AwsTaskQueue/GreetingsReceiverConsole/Program.cs create mode 100644 samples/Scheduler/QuartzTaskQueue/Greetings/Greetings.csproj create mode 100644 samples/Scheduler/QuartzTaskQueue/Greetings/Ports/CommandHandlers/FarewellEventHandler.cs create mode 100644 samples/Scheduler/QuartzTaskQueue/Greetings/Ports/CommandHandlers/GreetingEventHandler.cs create mode 100644 samples/Scheduler/QuartzTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs create mode 100644 samples/Scheduler/QuartzTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs create mode 100644 samples/Scheduler/QuartzTaskQueue/Greetings/Ports/Mappers/FarewellEventMessageMapper.cs create mode 100644 samples/Scheduler/QuartzTaskQueue/Greetings/Ports/Mappers/GreetingEventMessageMapper.cs create mode 100644 samples/Scheduler/QuartzTaskQueue/GreetingsPumper/GreetingsPumper.csproj create mode 100644 samples/Scheduler/QuartzTaskQueue/GreetingsPumper/Program.cs create mode 100644 samples/Scheduler/QuartzTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj create mode 100644 samples/Scheduler/QuartzTaskQueue/GreetingsReceiverConsole/Program.cs diff --git a/Brighter.sln b/Brighter.sln index f3711bbfc4..b311584b93 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -321,6 +321,24 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParamoreBrighter.Quartz.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.MessageScheduler.Aws", "src\Paramore.Brighter.MessageScheduler.Aws\Paramore.Brighter.MessageScheduler.Aws.csproj", "{28C2529C-EF15-4C86-A2CC-9EE326423A77}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scheduler", "Scheduler", "{8E414D7F-8DF0-4608-B47C-19213DB7E2B0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Quartz", "Quartz", "{D5F5A100-F524-4020-B157-298EDC0910F4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreetingsReceiverConsole", "samples\Scheduler\QuartzTaskQueue\GreetingsReceiverConsole\GreetingsReceiverConsole.csproj", "{3779A9C1-2E47-4CB4-B569-75752CA8C419}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreetingsPumper", "samples\Scheduler\QuartzTaskQueue\GreetingsPumper\GreetingsPumper.csproj", "{FADD3869-F9A1-41EA-8DB2-04AFB61A6635}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings", "samples\Scheduler\QuartzTaskQueue\Greetings\Greetings.csproj", "{50A08BF4-7B24-42EF-B1EA-3CB9546EB8AE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Aws", "Aws", "{966314C4-D989-4E50-85C4-8091189495E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings", "samples\Scheduler\AwsTaskQueue\Greetings\Greetings.csproj", "{85AB9510-93CC-4D10-A179-A30654765E59}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreetingsPumper", "samples\Scheduler\AwsTaskQueue\GreetingsPumper\GreetingsPumper.csproj", "{5C27F959-455A-408C-8248-D7EC848B633E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreetingsReceiverConsole", "samples\Scheduler\AwsTaskQueue\GreetingsReceiverConsole\GreetingsReceiverConsole.csproj", "{F8A3AE7E-B232-4794-B23F-772F48D8636F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1807,6 +1825,78 @@ Global {28C2529C-EF15-4C86-A2CC-9EE326423A77}.Release|Mixed Platforms.Build.0 = Release|Any CPU {28C2529C-EF15-4C86-A2CC-9EE326423A77}.Release|x86.ActiveCfg = Release|Any CPU {28C2529C-EF15-4C86-A2CC-9EE326423A77}.Release|x86.Build.0 = Release|Any CPU + {3779A9C1-2E47-4CB4-B569-75752CA8C419}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3779A9C1-2E47-4CB4-B569-75752CA8C419}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3779A9C1-2E47-4CB4-B569-75752CA8C419}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {3779A9C1-2E47-4CB4-B569-75752CA8C419}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {3779A9C1-2E47-4CB4-B569-75752CA8C419}.Debug|x86.ActiveCfg = Debug|Any CPU + {3779A9C1-2E47-4CB4-B569-75752CA8C419}.Debug|x86.Build.0 = Debug|Any CPU + {3779A9C1-2E47-4CB4-B569-75752CA8C419}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3779A9C1-2E47-4CB4-B569-75752CA8C419}.Release|Any CPU.Build.0 = Release|Any CPU + {3779A9C1-2E47-4CB4-B569-75752CA8C419}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {3779A9C1-2E47-4CB4-B569-75752CA8C419}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {3779A9C1-2E47-4CB4-B569-75752CA8C419}.Release|x86.ActiveCfg = Release|Any CPU + {3779A9C1-2E47-4CB4-B569-75752CA8C419}.Release|x86.Build.0 = Release|Any CPU + {FADD3869-F9A1-41EA-8DB2-04AFB61A6635}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FADD3869-F9A1-41EA-8DB2-04AFB61A6635}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FADD3869-F9A1-41EA-8DB2-04AFB61A6635}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {FADD3869-F9A1-41EA-8DB2-04AFB61A6635}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {FADD3869-F9A1-41EA-8DB2-04AFB61A6635}.Debug|x86.ActiveCfg = Debug|Any CPU + {FADD3869-F9A1-41EA-8DB2-04AFB61A6635}.Debug|x86.Build.0 = Debug|Any CPU + {FADD3869-F9A1-41EA-8DB2-04AFB61A6635}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FADD3869-F9A1-41EA-8DB2-04AFB61A6635}.Release|Any CPU.Build.0 = Release|Any CPU + {FADD3869-F9A1-41EA-8DB2-04AFB61A6635}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {FADD3869-F9A1-41EA-8DB2-04AFB61A6635}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {FADD3869-F9A1-41EA-8DB2-04AFB61A6635}.Release|x86.ActiveCfg = Release|Any CPU + {FADD3869-F9A1-41EA-8DB2-04AFB61A6635}.Release|x86.Build.0 = Release|Any CPU + {50A08BF4-7B24-42EF-B1EA-3CB9546EB8AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50A08BF4-7B24-42EF-B1EA-3CB9546EB8AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50A08BF4-7B24-42EF-B1EA-3CB9546EB8AE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {50A08BF4-7B24-42EF-B1EA-3CB9546EB8AE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {50A08BF4-7B24-42EF-B1EA-3CB9546EB8AE}.Debug|x86.ActiveCfg = Debug|Any CPU + {50A08BF4-7B24-42EF-B1EA-3CB9546EB8AE}.Debug|x86.Build.0 = Debug|Any CPU + {50A08BF4-7B24-42EF-B1EA-3CB9546EB8AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50A08BF4-7B24-42EF-B1EA-3CB9546EB8AE}.Release|Any CPU.Build.0 = Release|Any CPU + {50A08BF4-7B24-42EF-B1EA-3CB9546EB8AE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {50A08BF4-7B24-42EF-B1EA-3CB9546EB8AE}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {50A08BF4-7B24-42EF-B1EA-3CB9546EB8AE}.Release|x86.ActiveCfg = Release|Any CPU + {50A08BF4-7B24-42EF-B1EA-3CB9546EB8AE}.Release|x86.Build.0 = Release|Any CPU + {85AB9510-93CC-4D10-A179-A30654765E59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85AB9510-93CC-4D10-A179-A30654765E59}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85AB9510-93CC-4D10-A179-A30654765E59}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {85AB9510-93CC-4D10-A179-A30654765E59}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {85AB9510-93CC-4D10-A179-A30654765E59}.Debug|x86.ActiveCfg = Debug|Any CPU + {85AB9510-93CC-4D10-A179-A30654765E59}.Debug|x86.Build.0 = Debug|Any CPU + {85AB9510-93CC-4D10-A179-A30654765E59}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85AB9510-93CC-4D10-A179-A30654765E59}.Release|Any CPU.Build.0 = Release|Any CPU + {85AB9510-93CC-4D10-A179-A30654765E59}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {85AB9510-93CC-4D10-A179-A30654765E59}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {85AB9510-93CC-4D10-A179-A30654765E59}.Release|x86.ActiveCfg = Release|Any CPU + {85AB9510-93CC-4D10-A179-A30654765E59}.Release|x86.Build.0 = Release|Any CPU + {5C27F959-455A-408C-8248-D7EC848B633E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C27F959-455A-408C-8248-D7EC848B633E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C27F959-455A-408C-8248-D7EC848B633E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {5C27F959-455A-408C-8248-D7EC848B633E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {5C27F959-455A-408C-8248-D7EC848B633E}.Debug|x86.ActiveCfg = Debug|Any CPU + {5C27F959-455A-408C-8248-D7EC848B633E}.Debug|x86.Build.0 = Debug|Any CPU + {5C27F959-455A-408C-8248-D7EC848B633E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C27F959-455A-408C-8248-D7EC848B633E}.Release|Any CPU.Build.0 = Release|Any CPU + {5C27F959-455A-408C-8248-D7EC848B633E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {5C27F959-455A-408C-8248-D7EC848B633E}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {5C27F959-455A-408C-8248-D7EC848B633E}.Release|x86.ActiveCfg = Release|Any CPU + {5C27F959-455A-408C-8248-D7EC848B633E}.Release|x86.Build.0 = Release|Any CPU + {F8A3AE7E-B232-4794-B23F-772F48D8636F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8A3AE7E-B232-4794-B23F-772F48D8636F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8A3AE7E-B232-4794-B23F-772F48D8636F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {F8A3AE7E-B232-4794-B23F-772F48D8636F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {F8A3AE7E-B232-4794-B23F-772F48D8636F}.Debug|x86.ActiveCfg = Debug|Any CPU + {F8A3AE7E-B232-4794-B23F-772F48D8636F}.Debug|x86.Build.0 = Debug|Any CPU + {F8A3AE7E-B232-4794-B23F-772F48D8636F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8A3AE7E-B232-4794-B23F-772F48D8636F}.Release|Any CPU.Build.0 = Release|Any CPU + {F8A3AE7E-B232-4794-B23F-772F48D8636F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {F8A3AE7E-B232-4794-B23F-772F48D8636F}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {F8A3AE7E-B232-4794-B23F-772F48D8636F}.Release|x86.ActiveCfg = Release|Any CPU + {F8A3AE7E-B232-4794-B23F-772F48D8636F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1905,6 +1995,15 @@ Global {7D8CE752-CCBB-4868-ADF0-30FF94CA611C} = {11935469-A062-4CFF-9F72-F4F41E14C2B4} {FBAF452E-C0AB-4C4B-9A81-F1ED9616DE2A} = {202BA107-89D5-4868-AC5A-3527114C0109} {4469AEE3-B460-4948-A0A5-B9480EE70EA4} = {329736D2-BF92-4D06-A7BF-19F4B6B64EDD} + {8E414D7F-8DF0-4608-B47C-19213DB7E2B0} = {235DE1F1-E71B-4817-8E27-3B34FF006E4C} + {D5F5A100-F524-4020-B157-298EDC0910F4} = {8E414D7F-8DF0-4608-B47C-19213DB7E2B0} + {3779A9C1-2E47-4CB4-B569-75752CA8C419} = {D5F5A100-F524-4020-B157-298EDC0910F4} + {FADD3869-F9A1-41EA-8DB2-04AFB61A6635} = {D5F5A100-F524-4020-B157-298EDC0910F4} + {50A08BF4-7B24-42EF-B1EA-3CB9546EB8AE} = {D5F5A100-F524-4020-B157-298EDC0910F4} + {966314C4-D989-4E50-85C4-8091189495E9} = {8E414D7F-8DF0-4608-B47C-19213DB7E2B0} + {85AB9510-93CC-4D10-A179-A30654765E59} = {966314C4-D989-4E50-85C4-8091189495E9} + {5C27F959-455A-408C-8248-D7EC848B633E} = {966314C4-D989-4E50-85C4-8091189495E9} + {F8A3AE7E-B232-4794-B23F-772F48D8636F} = {966314C4-D989-4E50-85C4-8091189495E9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8B7C7E31-2E32-4E0D-9426-BC9AF22E9F4C} diff --git a/Directory.Packages.props b/Directory.Packages.props index ad6c067e8f..4ea3eca016 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -82,6 +82,7 @@ + diff --git a/docker-compose-localstack.yaml b/docker-compose-localstack.yaml index 6efa721fd8..5940681f2f 100644 --- a/docker-compose-localstack.yaml +++ b/docker-compose-localstack.yaml @@ -5,9 +5,10 @@ services: image: localstack/localstack environment: # LocalStack configuration: https://docs.localstack.cloud/references/configuration/ - - "SERVICES=s3,sqs,sns,sts,dynamodb" + - "SERVICES=s3,sqs,sns,sts,dynamodb,iam,scheduler" - "DEFAULT_REGION=eu-west-1" - "DEBUG=1" + - "PROVIDER_OVERRIDE_EVENTS=v2" ports: - "4566:4566" # LocalStack Gateway - "4510-4559:4510-4559" # External services port range diff --git a/samples/Scheduler/AwsTaskQueue/Greetings/Greetings.csproj b/samples/Scheduler/AwsTaskQueue/Greetings/Greetings.csproj new file mode 100644 index 0000000000..6306699275 --- /dev/null +++ b/samples/Scheduler/AwsTaskQueue/Greetings/Greetings.csproj @@ -0,0 +1,15 @@ + + + net8.0 + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/Scheduler/AwsTaskQueue/Greetings/Ports/CommandHandlers/FarewellEventHandler.cs b/samples/Scheduler/AwsTaskQueue/Greetings/Ports/CommandHandlers/FarewellEventHandler.cs new file mode 100644 index 0000000000..ec8e17b48c --- /dev/null +++ b/samples/Scheduler/AwsTaskQueue/Greetings/Ports/CommandHandlers/FarewellEventHandler.cs @@ -0,0 +1,24 @@ +using System; +using Greetings.Ports.Commands; +using Paramore.Brighter; + +namespace Greetings.Ports.CommandHandlers +{ + public class FarewellEventHandler : RequestHandler + { + public override FarewellEvent Handle(FarewellEvent @event) + { + Console.BackgroundColor = ConsoleColor.Blue; + Console.ForegroundColor = ConsoleColor.White; + + Console.WriteLine("Received Farewell. Message Follows"); + Console.WriteLine("----------------------------------"); + Console.WriteLine(@event.Farewell); + Console.WriteLine("----------------------------------"); + Console.WriteLine("Message Ends"); + + Console.ResetColor(); + return base.Handle(@event); + } + } +} diff --git a/samples/Scheduler/AwsTaskQueue/Greetings/Ports/CommandHandlers/GreetingEventHandler.cs b/samples/Scheduler/AwsTaskQueue/Greetings/Ports/CommandHandlers/GreetingEventHandler.cs new file mode 100644 index 0000000000..a5f5fa2a1b --- /dev/null +++ b/samples/Scheduler/AwsTaskQueue/Greetings/Ports/CommandHandlers/GreetingEventHandler.cs @@ -0,0 +1,43 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using Greetings.Ports.Commands; +using Paramore.Brighter; + +namespace Greetings.Ports.CommandHandlers +{ + public class GreetingEventHandler : RequestHandler + { + public override GreetingEvent Handle(GreetingEvent @event) + { + Console.WriteLine("Received Greeting. Message Follows"); + Console.WriteLine("----------------------------------"); + Console.WriteLine(@event.Greeting); + Console.WriteLine("----------------------------------"); + Console.WriteLine("Message Ends"); + return base.Handle(@event); + } + } +} diff --git a/samples/Scheduler/AwsTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs b/samples/Scheduler/AwsTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs new file mode 100644 index 0000000000..8f107f3f87 --- /dev/null +++ b/samples/Scheduler/AwsTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs @@ -0,0 +1,19 @@ +using System; +using Paramore.Brighter; + +namespace Greetings.Ports.Commands +{ + public class FarewellEvent : Event + { + public FarewellEvent() : base(Guid.NewGuid()) + { + } + + public FarewellEvent(string farewell) : base(Guid.NewGuid()) + { + Farewell = farewell; + } + + public string Farewell { get; set; } + } +} diff --git a/samples/Scheduler/AwsTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs b/samples/Scheduler/AwsTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs new file mode 100644 index 0000000000..7b07b481f8 --- /dev/null +++ b/samples/Scheduler/AwsTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs @@ -0,0 +1,41 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2015 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using Paramore.Brighter; + +namespace Greetings.Ports.Commands +{ + public class GreetingEvent : Event + { + public GreetingEvent() : base(Guid.NewGuid()) { } + + public GreetingEvent(string greeting) : base(Guid.NewGuid()) + { + Greeting = greeting; + } + + public string Greeting { get; set; } + } +} diff --git a/samples/Scheduler/AwsTaskQueue/Greetings/Ports/Mappers/FarewellEventMessageMapper.cs b/samples/Scheduler/AwsTaskQueue/Greetings/Ports/Mappers/FarewellEventMessageMapper.cs new file mode 100644 index 0000000000..f59152f38d --- /dev/null +++ b/samples/Scheduler/AwsTaskQueue/Greetings/Ports/Mappers/FarewellEventMessageMapper.cs @@ -0,0 +1,51 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System.Text.Json; +using Greetings.Ports.Commands; +using Paramore.Brighter; +using Paramore.Brighter.MessagingGateway.AWSSQS; + +namespace Greetings.Ports.Mappers +{ + public class FarewellEventMessageMapper : IAmAMessageMapper + { + public IRequestContext Context { get; set; } + + public Message MapToMessage(FarewellEvent request, Publication publication) + { + var header = new MessageHeader(messageId: request.Id, topic: publication.Topic, messageType: MessageType.MT_EVENT); + var body = new MessageBody(JsonSerializer.Serialize(request, JsonSerialisationOptions.Options)); + var message = new Message(header, body); + return message; + } + + public FarewellEvent MapToRequest(Message message) + { + var greetingCommand = JsonSerializer.Deserialize(message.Body.Value, JsonSerialisationOptions.Options); + + return greetingCommand; + } + } +} diff --git a/samples/Scheduler/AwsTaskQueue/Greetings/Ports/Mappers/GreetingEventMessageMapper.cs b/samples/Scheduler/AwsTaskQueue/Greetings/Ports/Mappers/GreetingEventMessageMapper.cs new file mode 100644 index 0000000000..c5eeb3f260 --- /dev/null +++ b/samples/Scheduler/AwsTaskQueue/Greetings/Ports/Mappers/GreetingEventMessageMapper.cs @@ -0,0 +1,51 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System.Text.Json; +using Greetings.Ports.Commands; +using Paramore.Brighter; +using Paramore.Brighter.MessagingGateway.AWSSQS; + +namespace Greetings.Ports.Mappers +{ + public class GreetingEventMessageMapper : IAmAMessageMapper + { + public IRequestContext Context { get; set; } + + public Message MapToMessage(GreetingEvent request, Publication publication) + { + var header = new MessageHeader(messageId: request.Id, topic: publication.Topic, messageType: MessageType.MT_EVENT); + var body = new MessageBody(JsonSerializer.Serialize(request, JsonSerialisationOptions.Options)); + var message = new Message(header, body); + return message; + } + + public GreetingEvent MapToRequest(Message message) + { + var greetingCommand = JsonSerializer.Deserialize(message.Body.Value, JsonSerialisationOptions.Options); + + return greetingCommand; + } + } +} diff --git a/samples/Scheduler/AwsTaskQueue/GreetingsPumper/GreetingsPumper.csproj b/samples/Scheduler/AwsTaskQueue/GreetingsPumper/GreetingsPumper.csproj new file mode 100644 index 0000000000..2529e9d4dc --- /dev/null +++ b/samples/Scheduler/AwsTaskQueue/GreetingsPumper/GreetingsPumper.csproj @@ -0,0 +1,23 @@ + + + + Exe + net8.0 + + + + + + + + + + + + + + + + + + diff --git a/samples/Scheduler/AwsTaskQueue/GreetingsPumper/Program.cs b/samples/Scheduler/AwsTaskQueue/GreetingsPumper/Program.cs new file mode 100644 index 0000000000..a04c562428 --- /dev/null +++ b/samples/Scheduler/AwsTaskQueue/GreetingsPumper/Program.cs @@ -0,0 +1,116 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime.CredentialManagement; +using Greetings.Ports.Commands; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Paramore.Brighter; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.MessageScheduler.Aws; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.Scheduler.Events; +using Serilog; + +namespace GreetingsPumper; + +class Program +{ + private static async Task Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + + var host = new HostBuilder() + .ConfigureServices((hostContext, services) => + + { + if (new CredentialProfileStoreChain().TryGetAWSCredentials("default", out var credentials)) + { + var awsConnection = new AWSMessagingGatewayConnection(credentials, RegionEndpoint.USEast1, + cfg => + { + var serviceURL = + "http://localhost:4566/"; // Environment.GetEnvironmentVariable("LOCALSTACK_SERVICE_URL"); + if (!string.IsNullOrWhiteSpace(serviceURL)) + { + cfg.ServiceURL = serviceURL; + } + }); + + var producerRegistry = new SnsProducerRegistryFactory( + awsConnection, + [ + new SnsPublication + { + Topic = new RoutingKey(typeof(GreetingEvent).FullName + .ToValidSNSTopicName()), + RequestType = typeof(GreetingEvent) + }, + new SnsPublication + { + Topic = new RoutingKey( + typeof(FarewellEvent).FullName.ToValidSNSTopicName(true)), + SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo }, + RequestType = typeof(FarewellEvent) + }, + new SnsPublication + { + Topic = new RoutingKey(typeof(FireSchedulerMessage).FullName.ToValidSNSTopicName()), + RequestType = typeof(FireSchedulerMessage) + } + ] + ).Create(); + + services.AddBrighter() + .UseExternalBus(configure => + { + configure.ProducerRegistry = producerRegistry; + }) + .UseMessageScheduler(new AwsMessageSchedulerFactory(awsConnection, "brighter-scheduler") + { + SchedulerTopicOrQueue = new RoutingKey(typeof(FireSchedulerMessage).FullName.ToValidSNSTopicName()), + OnConflict = OnSchedulerConflict.Overwrite, + GetOrCreateSchedulerId = message => message.Id + }) + .AutoFromAssemblies(typeof(GreetingEvent).Assembly); + } + + services.AddHostedService(); + } + ) + .UseConsoleLifetime() + .UseSerilog() + .Build(); + + await host.RunAsync(); + } + + internal class RunCommandProcessor(IAmACommandProcessor commandProcessor, ILogger logger) : IHostedService + { + public async Task StartAsync(CancellationToken cancellationToken) + { + long loop = 0; + while (!cancellationToken.IsCancellationRequested) + { + loop++; + + logger.LogInformation("Scheduling message #{Loop}", loop); + commandProcessor.Post(new GreetingEvent($"Ian #{loop}")); + + if (loop % 100 != 0) + continue; + + logger.LogInformation("Pausing for breath..."); + await Task.Delay(4000, cancellationToken); + } + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/samples/Scheduler/AwsTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj b/samples/Scheduler/AwsTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj new file mode 100644 index 0000000000..4ad5c39849 --- /dev/null +++ b/samples/Scheduler/AwsTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj @@ -0,0 +1,21 @@ + + + net8.0 + Exe + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/Scheduler/AwsTaskQueue/GreetingsReceiverConsole/Program.cs b/samples/Scheduler/AwsTaskQueue/GreetingsReceiverConsole/Program.cs new file mode 100644 index 0000000000..514b1d910c --- /dev/null +++ b/samples/Scheduler/AwsTaskQueue/GreetingsReceiverConsole/Program.cs @@ -0,0 +1,115 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime.CredentialManagement; +using Greetings.Ports.Commands; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Paramore.Brighter; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.Scheduler.Events; +using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection; +using Paramore.Brighter.ServiceActivator.Extensions.Hosting; +using Serilog; + +namespace GreetingsReceiverConsole; + +public class Program +{ + public static async Task Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + + var host = new HostBuilder() + .ConfigureServices((_, services) => + + { + var subscriptions = new Subscription[] + { + new SqsSubscription( + new SubscriptionName("paramore.example.fire-scheduler"), + new ChannelName(typeof(FireSchedulerMessage).FullName.ToValidSNSTopicName()), + new RoutingKey(typeof(FireSchedulerMessage).FullName.ToValidSNSTopicName()), + bufferSize: 10, + timeOut: TimeSpan.FromMilliseconds(20), + lockTimeout: 30), + new SqsSubscription( + new SubscriptionName("paramore.example.greeting"), + new ChannelName(typeof(GreetingEvent).FullName.ToValidSNSTopicName()), + new RoutingKey(typeof(GreetingEvent).FullName.ToValidSNSTopicName()), + bufferSize: 10, + timeOut: TimeSpan.FromMilliseconds(20), + lockTimeout: 30), + new SqsSubscription(new SubscriptionName("paramore.example.farewell"), + new ChannelName(typeof(FarewellEvent).FullName.ToValidSNSTopicName(true)), + new RoutingKey(typeof(FarewellEvent).FullName.ToValidSNSTopicName(true)), + bufferSize: 10, + timeOut: TimeSpan.FromMilliseconds(20), + lockTimeout: 30, + sqsType: SnsSqsType.Fifo, + snsAttributes: new SnsAttributes { Type = SnsSqsType.Fifo }) + }; + + //create the gateway + if (new CredentialProfileStoreChain().TryGetAWSCredentials("default", out var credentials)) + { + var serviceURL = + "http://localhost:4566/"; // Environment.GetEnvironmentVariable("LOCALSTACK_SERVICE_URL"); + var region = string.IsNullOrWhiteSpace(serviceURL) + ? RegionEndpoint.EUWest1 + : RegionEndpoint.USEast1; + var awsConnection = new AWSMessagingGatewayConnection(credentials, region, + cfg => + { + if (!string.IsNullOrWhiteSpace(serviceURL)) + { + cfg.ServiceURL = serviceURL; + } + }); + + services.AddServiceActivator(options => + { + options.Subscriptions = subscriptions; + options.DefaultChannelFactory = new ChannelFactory(awsConnection); + }) + .AutoFromAssemblies(); + } + + services.AddHostedService(); + }) + .UseConsoleLifetime() + .UseSerilog() + .Build(); + + await host.RunAsync(); + } +} diff --git a/samples/Scheduler/QuartzTaskQueue/Greetings/Greetings.csproj b/samples/Scheduler/QuartzTaskQueue/Greetings/Greetings.csproj new file mode 100644 index 0000000000..6306699275 --- /dev/null +++ b/samples/Scheduler/QuartzTaskQueue/Greetings/Greetings.csproj @@ -0,0 +1,15 @@ + + + net8.0 + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/CommandHandlers/FarewellEventHandler.cs b/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/CommandHandlers/FarewellEventHandler.cs new file mode 100644 index 0000000000..3853b318ca --- /dev/null +++ b/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/CommandHandlers/FarewellEventHandler.cs @@ -0,0 +1,23 @@ +using System; +using Greetings.Ports.Commands; +using Paramore.Brighter; + +namespace Greetings.Ports.CommandHandlers; + +public class FarewellEventHandler : RequestHandler +{ + public override FarewellEvent Handle(FarewellEvent @event) + { + Console.BackgroundColor = ConsoleColor.Blue; + Console.ForegroundColor = ConsoleColor.White; + + Console.WriteLine("Received Farewell. Message Follows"); + Console.WriteLine("----------------------------------"); + Console.WriteLine(@event.Farewell); + Console.WriteLine("----------------------------------"); + Console.WriteLine("Message Ends"); + + Console.ResetColor(); + return base.Handle(@event); + } +} \ No newline at end of file diff --git a/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/CommandHandlers/GreetingEventHandler.cs b/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/CommandHandlers/GreetingEventHandler.cs new file mode 100644 index 0000000000..d374134ba0 --- /dev/null +++ b/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/CommandHandlers/GreetingEventHandler.cs @@ -0,0 +1,42 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using Greetings.Ports.Commands; +using Paramore.Brighter; + +namespace Greetings.Ports.CommandHandlers; + +public class GreetingEventHandler : RequestHandler +{ + public override GreetingEvent Handle(GreetingEvent @event) + { + Console.WriteLine("Received Greeting. Message Follows"); + Console.WriteLine("----------------------------------"); + Console.WriteLine(@event.Greeting); + Console.WriteLine("----------------------------------"); + Console.WriteLine("Message Ends"); + return base.Handle(@event); + } +} \ No newline at end of file diff --git a/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs b/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs new file mode 100644 index 0000000000..a0a6340b9b --- /dev/null +++ b/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs @@ -0,0 +1,18 @@ +using System; +using Paramore.Brighter; + +namespace Greetings.Ports.Commands; + +public class FarewellEvent : Event +{ + public FarewellEvent() : base(Guid.NewGuid()) + { + } + + public FarewellEvent(string farewell) : base(Guid.NewGuid()) + { + Farewell = farewell; + } + + public string Farewell { get; set; } +} \ No newline at end of file diff --git a/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs b/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs new file mode 100644 index 0000000000..bffaff012b --- /dev/null +++ b/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs @@ -0,0 +1,40 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2015 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using Paramore.Brighter; + +namespace Greetings.Ports.Commands; + +public class GreetingEvent : Event +{ + public GreetingEvent() : base(Guid.NewGuid()) { } + + public GreetingEvent(string greeting) : base(Guid.NewGuid()) + { + Greeting = greeting; + } + + public string Greeting { get; set; } +} diff --git a/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/Mappers/FarewellEventMessageMapper.cs b/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/Mappers/FarewellEventMessageMapper.cs new file mode 100644 index 0000000000..ddffaabed8 --- /dev/null +++ b/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/Mappers/FarewellEventMessageMapper.cs @@ -0,0 +1,50 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System.Text.Json; +using Greetings.Ports.Commands; +using Paramore.Brighter; +using Paramore.Brighter.MessagingGateway.AWSSQS; + +namespace Greetings.Ports.Mappers; + +public class FarewellEventMessageMapper : IAmAMessageMapper +{ + public IRequestContext? Context { get; set; } + + public Message MapToMessage(FarewellEvent request, Publication publication) + { + var header = new MessageHeader(messageId: request.Id, topic: publication.Topic, messageType: MessageType.MT_EVENT); + var body = new MessageBody(JsonSerializer.Serialize(request, JsonSerialisationOptions.Options)); + var message = new Message(header, body); + return message; + } + + public FarewellEvent MapToRequest(Message message) + { + var greetingCommand = JsonSerializer.Deserialize(message.Body.Value, JsonSerialisationOptions.Options); + + return greetingCommand; + } +} diff --git a/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/Mappers/GreetingEventMessageMapper.cs b/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/Mappers/GreetingEventMessageMapper.cs new file mode 100644 index 0000000000..f56cfe37c4 --- /dev/null +++ b/samples/Scheduler/QuartzTaskQueue/Greetings/Ports/Mappers/GreetingEventMessageMapper.cs @@ -0,0 +1,50 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System.Text.Json; +using Greetings.Ports.Commands; +using Paramore.Brighter; +using Paramore.Brighter.MessagingGateway.AWSSQS; + +namespace Greetings.Ports.Mappers; + +public class GreetingEventMessageMapper : IAmAMessageMapper +{ + public IRequestContext? Context { get; set; } + + public Message MapToMessage(GreetingEvent request, Publication publication) + { + var header = new MessageHeader(messageId: request.Id, topic: publication.Topic, messageType: MessageType.MT_EVENT); + var body = new MessageBody(JsonSerializer.Serialize(request, JsonSerialisationOptions.Options)); + var message = new Message(header, body); + return message; + } + + public GreetingEvent MapToRequest(Message message) + { + var greetingCommand = JsonSerializer.Deserialize(message.Body.Value, JsonSerialisationOptions.Options); + + return greetingCommand; + } +} diff --git a/samples/Scheduler/QuartzTaskQueue/GreetingsPumper/GreetingsPumper.csproj b/samples/Scheduler/QuartzTaskQueue/GreetingsPumper/GreetingsPumper.csproj new file mode 100644 index 0000000000..f380e315b7 --- /dev/null +++ b/samples/Scheduler/QuartzTaskQueue/GreetingsPumper/GreetingsPumper.csproj @@ -0,0 +1,24 @@ + + + + Exe + net8.0 + + + + + + + + + + + + + + + + + + + diff --git a/samples/Scheduler/QuartzTaskQueue/GreetingsPumper/Program.cs b/samples/Scheduler/QuartzTaskQueue/GreetingsPumper/Program.cs new file mode 100644 index 0000000000..6ecb286867 --- /dev/null +++ b/samples/Scheduler/QuartzTaskQueue/GreetingsPumper/Program.cs @@ -0,0 +1,124 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime.CredentialManagement; +using Greetings.Ports.Commands; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Paramore.Brighter; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.MessageScheduler.Quartz; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Quartz; +using Serilog; + +namespace GreetingsPumper; + +class Program +{ + private static async Task Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + + var host = new HostBuilder() + .ConfigureServices((hostContext, services) => + { + services + .AddSingleton() + .AddQuartz(opt => + { + opt.SchedulerId = "QuartzBrighter"; + opt.SchedulerName = "QuartzBrighter"; + opt.UseSimpleTypeLoader(); + opt.UseInMemoryStore(); + }) + .AddQuartzHostedService(opt => + { + opt.WaitForJobsToComplete = true; + }); + + if (new CredentialProfileStoreChain().TryGetAWSCredentials("default", out var credentials)) + { + var awsConnection = new AWSMessagingGatewayConnection(credentials, RegionEndpoint.USEast1, + cfg => + { + var serviceURL = + "http://localhost:4566/"; // Environment.GetEnvironmentVariable("LOCALSTACK_SERVICE_URL"); + if (!string.IsNullOrWhiteSpace(serviceURL)) + { + cfg.ServiceURL = serviceURL; + } + }); + + var producerRegistry = new SnsProducerRegistryFactory( + awsConnection, + [ + new SnsPublication + { + Topic = new RoutingKey(typeof(GreetingEvent).FullName.ToValidSNSTopicName()), + RequestType = typeof(GreetingEvent) + }, + new SnsPublication + { + Topic = + new RoutingKey(typeof(FarewellEvent).FullName.ToValidSNSTopicName(true)), + SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + } + ] + ).Create(); + + services.AddBrighter() + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + }) + .UseMessageScheduler(provider => + { + var factory = provider.GetRequiredService(); + return new QuartzMessageSchedulerFactory( + factory.GetScheduler().GetAwaiter().GetResult()); + }) + .AutoFromAssemblies(typeof(GreetingEvent).Assembly); + } + + services.AddHostedService(); + } + ) + .UseConsoleLifetime() + .UseSerilog() + .Build(); + + await host.RunAsync(); + } + + internal class RunCommandProcessor(IAmACommandProcessor commandProcessor, ILogger logger) + : BackgroundService + { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + long loop = 0; + while (!stoppingToken.IsCancellationRequested) + { + loop++; + + logger.LogInformation("Scheduling message #{Loop}", loop); + commandProcessor.SchedulerPost(new GreetingEvent($"Scheduler message Ian #{loop}"), + TimeSpan.FromMinutes(1)); + + if (loop % 100 != 0) + { + continue; + } + + logger.LogInformation("Pausing for breath..."); + await Task.Delay(4000, stoppingToken); + } + } + } +} diff --git a/samples/Scheduler/QuartzTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj b/samples/Scheduler/QuartzTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj new file mode 100644 index 0000000000..c105a1f402 --- /dev/null +++ b/samples/Scheduler/QuartzTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj @@ -0,0 +1,20 @@ + + + net8.0 + Exe + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/Scheduler/QuartzTaskQueue/GreetingsReceiverConsole/Program.cs b/samples/Scheduler/QuartzTaskQueue/GreetingsReceiverConsole/Program.cs new file mode 100644 index 0000000000..01b06720db --- /dev/null +++ b/samples/Scheduler/QuartzTaskQueue/GreetingsReceiverConsole/Program.cs @@ -0,0 +1,108 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime.CredentialManagement; +using Greetings.Ports.Commands; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Paramore.Brighter; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection; +using Paramore.Brighter.ServiceActivator.Extensions.Hosting; +using Serilog; + +namespace GreetingsReceiverConsole; + +public class Program +{ + public static async Task Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + + var host = new HostBuilder() + .ConfigureServices((_, services) => + + { + var subscriptions = new Subscription[] + { + new SqsSubscription( + new SubscriptionName("paramore.example.greeting"), + new ChannelName(typeof(GreetingEvent).FullName.ToValidSNSTopicName()), + new RoutingKey(typeof(GreetingEvent).FullName.ToValidSNSTopicName()), + bufferSize: 10, + timeOut: TimeSpan.FromMilliseconds(20), + lockTimeout: 30, + messagePumpType: MessagePumpType.Reactor), + new SqsSubscription(new SubscriptionName("paramore.example.farewell"), + new ChannelName(typeof(FarewellEvent).FullName.ToValidSNSTopicName(true)), + new RoutingKey(typeof(FarewellEvent).FullName.ToValidSNSTopicName(true)), + bufferSize: 10, + timeOut: TimeSpan.FromMilliseconds(20), + lockTimeout: 30, + sqsType: SnsSqsType.Fifo, + snsAttributes: new SnsAttributes { Type = SnsSqsType.Fifo }, + messagePumpType: MessagePumpType.Reactor) + }; + + //create the gateway + if (new CredentialProfileStoreChain().TryGetAWSCredentials("default", out var credentials)) + { + var serviceURL = "http://localhost:4566/"; // Environment.GetEnvironmentVariable("LOCALSTACK_SERVICE_URL"); + var region = string.IsNullOrWhiteSpace(serviceURL) + ? RegionEndpoint.EUWest1 + : RegionEndpoint.USEast1; + var awsConnection = new AWSMessagingGatewayConnection(credentials, region, + cfg => + { + if (!string.IsNullOrWhiteSpace(serviceURL)) + { + cfg.ServiceURL = serviceURL; + } + }); + + services.AddServiceActivator(options => + { + options.Subscriptions = subscriptions; + options.DefaultChannelFactory = new ChannelFactory(awsConnection); + }) + .AutoFromAssemblies(); + } + + services.AddHostedService(); + }) + .UseConsoleLifetime() + .UseSerilog() + .Build(); + + await host.RunAsync(); + } +} diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index ec0e7c9e53..0c488bd86e 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -245,6 +245,18 @@ public static IBrighterBuilder UseMessageScheduler(this IBrighterBuilder builder return builder; } + /// + /// An external message scheduler factory + /// + /// The builder. + /// The message scheduler factory + /// + public static IBrighterBuilder UseMessageScheduler(this IBrighterBuilder builder, Func factory) + { + builder.Services.AddSingleton(factory); + return builder; + } + private static INeedInstrumentation AddEventBus( IServiceProvider provider, INeedMessaging messagingBuilder, @@ -253,7 +265,7 @@ private static INeedInstrumentation AddEventBus( var eventBus = provider.GetService(); var eventBusConfiguration = provider.GetService(); var serviceActivatorOptions = provider.GetService(); - var messageSchedulerFactory = eventBusConfiguration.MessageSchedulerFactory ?? provider.GetService(); + var messageSchedulerFactory = eventBusConfiguration?.MessageSchedulerFactory ?? provider.GetService(); INeedInstrumentation instrumentationBuilder = null; var hasEventBus = eventBus != null; @@ -266,7 +278,7 @@ private static INeedInstrumentation AddEventBus( instrumentationBuilder = messagingBuilder.ExternalBus( ExternalBusType.FireAndForget, eventBus, - eventBusConfiguration.ResponseChannelFactory, + eventBusConfiguration!.ResponseChannelFactory, eventBusConfiguration.ReplyQueueSubscriptions, serviceActivatorOptions?.InboxConfiguration, messageSchedulerFactory); @@ -277,7 +289,7 @@ private static INeedInstrumentation AddEventBus( instrumentationBuilder = messagingBuilder.ExternalBus( ExternalBusType.RPC, eventBus, - eventBusConfiguration.ResponseChannelFactory, + eventBusConfiguration!.ResponseChannelFactory, eventBusConfiguration.ReplyQueueSubscriptions, serviceActivatorOptions?.InboxConfiguration, messageSchedulerFactory @@ -332,8 +344,8 @@ private static IAmACommandProcessor BuildCommandProcessor(IServiceProvider provi .Build(); var eventBusConfiguration = provider.GetService(); - var messageSchedulerFactory = eventBusConfiguration.MessageSchedulerFactory ?? provider.GetService(); var producerRegistry = provider.GetService(); + var messageSchedulerFactory = eventBusConfiguration?.MessageSchedulerFactory ?? provider.GetService(); if (messageSchedulerFactory != null && producerRegistry != null) { producerRegistry diff --git a/src/Paramore.Brighter.MessageScheduler.Quartz/QuartzBrighterJob.cs b/src/Paramore.Brighter.MessageScheduler.Quartz/QuartzBrighterJob.cs index 00e79ba556..30ccf253b9 100644 --- a/src/Paramore.Brighter.MessageScheduler.Quartz/QuartzBrighterJob.cs +++ b/src/Paramore.Brighter.MessageScheduler.Quartz/QuartzBrighterJob.cs @@ -14,23 +14,17 @@ public async Task Execute(IJobExecutionContext context) { if (!context.JobDetail.JobDataMap.TryGetString("message", out var obj)) { - return; + throw new InvalidOperationException("Not message, something is wrong with this job scheduler"); } - if (!context.JobDetail.JobDataMap.TryGetBooleanValue("async", out var async)) + var fireScheduler = JsonSerializer.Deserialize(obj!, JsonSerialisationOptions.Options)!; + if (fireScheduler.Async) { - return; - } - - var id = context.JobDetail.Key.Name; - var message = JsonSerializer.Deserialize(obj!, JsonSerialisationOptions.Options)!; - if (async) - { - await processor.PostAsync(new FireSchedulerMessage { Id = id, Message = message }); + await processor.PostAsync(fireScheduler); } else { - processor.Post(new FireSchedulerMessage { Id = id, Message = message }); + processor.Post(fireScheduler); } } } diff --git a/src/Paramore.Brighter.MessageScheduler.Quartz/QuartzMessageScheduler.cs b/src/Paramore.Brighter.MessageScheduler.Quartz/QuartzMessageScheduler.cs index 8c70a7e323..447771f5f7 100644 --- a/src/Paramore.Brighter.MessageScheduler.Quartz/QuartzMessageScheduler.cs +++ b/src/Paramore.Brighter.MessageScheduler.Quartz/QuartzMessageScheduler.cs @@ -1,4 +1,5 @@ -using Paramore.Brighter.Tasks; +using Paramore.Brighter.Scheduler.Events; +using Paramore.Brighter.Tasks; using Quartz; using JsonSerializer = System.Text.Json.JsonSerializer; @@ -19,8 +20,9 @@ public string Schedule(Message message, DateTimeOffset at) var id = getOrCreateSchedulerId(message); var job = JobBuilder.Create() .WithIdentity(getOrCreateSchedulerId(message), group!) - .UsingJobData("message", JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)) - .UsingJobData("async", false) + .UsingJobData("message", JsonSerializer.Serialize( + new FireSchedulerMessage { Id = id, Async = false, Message = message }, + JsonSerialisationOptions.Options)) .Build(); var trigger = TriggerBuilder.Create() @@ -28,7 +30,7 @@ public string Schedule(Message message, DateTimeOffset at) .StartAt(at) .Build(); - BrighterAsyncContext.Run(async () => await scheduler.ScheduleJob(job, trigger)); + var tmp = BrighterAsyncContext.Run(async () => await scheduler.ScheduleJob(job, trigger)); return id; } @@ -54,8 +56,9 @@ public async Task ScheduleAsync(Message message, DateTimeOffset at, var id = getOrCreateSchedulerId(message); var job = JobBuilder.Create() .WithIdentity(getOrCreateSchedulerId(message), group!) - .UsingJobData("message", JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)) - .UsingJobData("async", true) + .UsingJobData("message", JsonSerializer.Serialize( + new FireSchedulerMessage { Id = id, Async = false, Message = message }, + JsonSerialisationOptions.Options)) .Build(); var trigger = TriggerBuilder.Create()