diff --git a/Directory.Packages.props b/Directory.Packages.props index 6309f4c8eb..675a2354fb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -77,10 +77,10 @@ - - - - + + + + @@ -90,7 +90,7 @@ - + diff --git a/README.md b/README.md index 43c3beaff5..af4caeeeb3 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,9 @@ We release the build artefacts (NuGet packages) to [NuGet](http://nuget.org) on src="https://scan.coverity.com/projects/2900/badge.svg"/> +## Sources + +Portions of this code are based on Stephen Cleary's [AsyncEx's AsyncContext](https://github.com/StephenCleary/AsyncEx/blob/master/doc/AsyncContext.md) ![CodeScene Code Health](https://codescene.io/projects/32198/status-badges/code-health) diff --git a/docs/adr/0002-use-a-single-threaded-message-pump.md b/docs/adr/0002-use-a-single-threaded-message-pump.md index 8efffdc0fa..a50723df56 100644 --- a/docs/adr/0002-use-a-single-threaded-message-pump.md +++ b/docs/adr/0002-use-a-single-threaded-message-pump.md @@ -10,15 +10,17 @@ Accepted Any service activator pattern will have a message pump, which reads from a queue. -There are different strategies we could use, a common one for example is to use a BlockingCollection to hold messages read from the queue, and then use threads from the thread pool to process those messages. However, a multi-threaded pump has the issue that it will de-order an otherwise ordered queue, as the threads will pull items from the blocking collection in parallel, not sequentially. In addition, where we have multiple threads it becomes difficult to create resources used by the pump without protecting them from race conditions. +There are different strategies we could use, a common one for example is to use a BlockingCollection to hold messages read from the queue, and then use threads from the thread pool to process those messages. However, a multithreaded pump has the issue that it will de-order an otherwise ordered queue, as the threads will pull items from the blocking collection in parallel, not sequentially. In addition, where we have multiple threads it becomes difficult to create resources used by the pump without protecting them from race conditions. The other option would be to use the thread pool to service requests, creating a thread for each incoming message. This would not scale, as we would quickly run out of threads in the pool. To avoid this issue, solutions that rely on the thread pool typically have to govern the number of thread pool threads that can be used for concurrent requests. The problem becomes that at scale the semaphore that governs the number of threads becomes a bottleneck. -The alternative to these multi-threaded approaches is to use a single-threaded message pump that reads from the queue, processes the message, and only when it has processed that message, processes the next item. This prevents de-ordering of the queue, because items are read in sequence. +The alternative to these multithreaded approaches is to use a single-threaded message pump that reads from the queue, processes the message, and only when it has processed that message, processes the next item. This prevents de-ordering of the queue, because items are read in sequence. This approach is the [Reactor](https://en.wikipedia.org/wiki/Reactor_pattern) pattern. The [Reactor](http://reactors.io/tutorialdocs//reactors/why-reactors/index.html) pattern uses a single thread to read from the queue, and then dispatches the message to a handler. If a higher throughput is desired with a single threaded pump, then you can create multiple pumps. In essence, this is the competing consumers pattern, each performer is its own message pump. -To make the Reactor pattern more performant, we can choose not to block on I/O by using asynchronous handlers. This is the [Proactor](https://en.wikipedia.org/wiki/Proactor_pattern) pattern. Brighter provides a SynchronizationContext so that asynchronous handlers can be used, and the message pump will not block on I/O, whilst still preserving ordering. Using an asynchronous handler switches you to a Proactor approach from Reactor for [performance](https://www.artima.com/articles/comparing-two-high-performance-io-design-patterns#part2). +To give the Reactor pattern higher throughput, we can choose not to block on I/O by using asynchronous handlers. This is the [Proactor](https://en.wikipedia.org/wiki/Proactor_pattern) pattern. Brighter provides a SynchronizationContext so that asynchronous handlers can be used, and the message pump will not block on I/O, whilst still preserving ordering. Using an asynchronous handler switches you to a Proactor approach from Reactor for [performance](https://www.artima.com/articles/comparing-two-high-performance-io-design-patterns#part2). + +Note, that the Reactor pattern may be more performant, because it does not require the overhead of the thread pool, and the context switching that occurs when using the thread pool. The Reactor pattern is also more predictable, as it does not rely on the thread pool, which can be unpredictable in terms of the number of threads available. The Proactor pattern however may offer greater throughput because it does not block on I/O. The message pump performs the usual sequence of actions: diff --git a/docs/adr/0022-reactor-and-nonblocking-io.md b/docs/adr/0022-reactor-and-nonblocking-io.md new file mode 100644 index 0000000000..d852e0714e --- /dev/null +++ b/docs/adr/0022-reactor-and-nonblocking-io.md @@ -0,0 +1,125 @@ +# 22. Reactor and Nonblocking IO, Proactor and Blocking IO + +Date: 2019-08-01 + +## Status + +Accepted + +## Context + +### Reactor and Proactor + +As [outlined](0002-use-a-single-threaded-message-pump.md), Brighter offers two concurrency models, a Reactor model and a Proactor model. + +The Reactor model uses a single-threaded message pump to read from the queue, and then dispatches the message to a handler. If a higher throughput is desired with a single threaded pump, then you can create multiple pumps (Peformers in Brighter's taxonomy). For a queue this is the competing consumers pattern, each Performer is its own message pump and another consumer; for a stream this is the partition worker pattern, each Performer is a single thread reading from one of your stream's partitions. + +The Proactor model uses the same single-threaded message pump, or Performer, but uses non-blocking I/O. As the message pump waits for the non-blocking I/O to complete, it will not process additional messages whilst waiting for the I/O to complete; instead it will yield to other Peformers, which will process messages whilst the I/O is waiting to complete. + +The benefit of the Proactor approach is throughput, as the Performer shares resources better. If you run multiple performers, each in their own thread, such as competing consumers of a queue, or consumers of individual partitions of a stream, the Proactor model ensures that when one is awaiting I/O, the others can continue to process messages. + +The trade-off here is the Reactor model can offer better performance, as it does not require the overhead of waiting for I/O completion. + +For the Reactor model there is a cost to using transports and stores that do non-blocking I/O, that is an additional thread will be needed to run the continuation. This is because the message pump thread is blocked on I/O, and cannot run the continuation. As our message pump is single-threaded, this will be the maximum number of threads required though for the Reactor model. With the message pump thread suspended, awaiting, during non-blocking I/O, there will be no additional messages processed, until after the I/O completes. + +This is not a significant issue but if you use an SDK that does not support blocking I/O natively (Azure Service Bus, SNS/SQS, RabbitMQ), then you need to be aware of the additional cost for those SDKs (an additional thread pool thread). You may be better off explicity using the Proactor model with these transports, unless your own application cannot support that concurrency model. + +For the Proactor model there is a cost to using blocking I/O, as the Performer cannot yield to other Performers whilst waiting for I/O to complete. This means the Proactor has both the throughput issue of the Reactor, but does not gain the performance benefit of the Reactor, as it is forced to use an additional thread to provide sync over async. + +In versions before V10, the Proactor message pump already supports user code in transformers and handlers running asynchronously (including the Outbox and Inbox), so we can take advantage of non-blocking I/O in the Proactor model. However, prior to V10 it did not take advantage of asynchronous code in a transport SDK, where the SDK supported non-blocking I/O. Brighter treated all transports as having blocking I/O, and so we blocked on the non-blocking I/O. + +### Thread Pool vs. Long Running Threads + +A web server, receiving HTTP requests can schedule an incoming request to a pool of threads, and threads can be returned to the pool to service other requests whilst I/O is occuring. This allows it to make the most efficient usage of resources because non-blocking I/O returns threads to the pool to service new requests. + +If I want to maintain ordering, I need to use a single-threaded message pump. Nothing else guarantees that I will read and process those messages in sequence. This is particularly important if I am doing stream processing, as I need to maintain the order of messages in the stream. + +A consumer of a stream has a constrained choice if it needs to maintain its sequence. In that case, only one thread can consume the stream at a time. When a consumer is processing a record, we block consuming other records on the same stream so that we process them sequentially. To scale, we partition the stream, and allow up to as many threads as we have partitions. Kafka is a good example of this, where the consumer can choose to read from multiple partitions, but within a partition, it can only use a single thread to process the stream. + +When consuming messages from a queue, where we do not care about ordering, we can use the competing consumers pattern, where each consumer is a single-threaded message pump. However, we do want to be able to throttle the rate at which we read from the queue, in order to be able to apply backpressure, and slow the rate of consumption. So again, we only tend to use a limited number of threads, and we can find value in being able to explicitly choose that value. + +For our Performer, which runs the message pump, we choose to have threads are long-running, and not thread pool threads. The danger of a thread pool thread, for long-running work, is that work could become stuck in a message pump thread's local queue, and not be processed. As we do not use the thread pool for our Performers and those threads are never returned to the pool. So the only thread pool threads we use are for non-blocking I/O. + +For us then, non-blocking I/O in either user code, a handler or tranfomer, or transport code, retrieving or acknowledging work, that is called by the message pump thread performs I/O, mainly has the benefit that we can yield to another Performer. + +### Synchronization Context + +Brighter has a custom SynchronizationContext, BrighterSynchronizationContext, that forces continuations to run on the message pump thread. This prevents non-blocking I/O waiting on the thread pool, with potential deadlocks. This synchronization context is used within the Performer for both the non-blocking I/O of the message pump. Because our performer's only thread processes a single message at a time, there is no danger of this synchronization context deadlocking. + +However, if someone uses .ConfigureAwait(false) on their call, which is advice for library code, then the continuation will run on a thread pool thread. Now, this won't tend to exhaust the pool, as we only process a single message at a time, and any given task is unlikely to require enough additional threads to exhaust the pool. But it does mean that we have not control over the order in which continuations run. This is a problem for any stream scenario where it is important to process work in sequence. + +## Decision + +### Reactor and Proactor + +We have chosen to support both the Reactor and Proactor models across all of our transports. We do this to avoid forcing a concurrency model onto users of Brighter. As we cannot know your context, we do not want to make decisions for you: the performace of blocking i/o or the throughput of non-blocking I/O. + +To make the two models more explicit, within the code, we have decided to rename the derived message pump classes to Proactor and Reactor, from Blocking and NonBlocking. + +In addition, within a Subscription, rather than the slightly confusing runAsync flag, we have decided to use the more explicit MessagePumpType flag. This makes it clear whether the Reactor or Proactor model is being used, and that non-blocking I/O or blocking I/O should be used. + +Within the Subscription for a specific transport, we set the default to the type that the transport natively supports, Proactor if it supports both. + +| Transport | Supports Reactor Natively | Supports Proactor Natively | +| ------------- | ------------- |-------------| +| Azure Service Bus | Sync over Async | Native | +| AWS (SNS/SQS)| Sync over Async | Native | +| Kafka| Native | Async over Sync (either thread pool thread or exploiting no wait calls) | +| MQTT | Sync over Async/Event Based | Event Based | +| MSSQL | Native | Native | +| Rabbit MQ (AMQP 0-9-1) | After V6, Sync over Async | Native from V7| +| Redis | Native | Native | + +### In Setup accept Blocking I/O + +Within our setup code our API can safely perovide a common abstraction using blocking I/O. Where the underlying SDK only supports non-blocking I/O, we should author an asynchronous version of the method and then use our SynchronizationContext to ensure that the continuation runs on the message pump thread. This class BrighterSynchronizationContext helper will be modelled on [AsyncEx's AsyncContext](https://github.com/StephenCleary/AsyncEx/blob/master/doc/AsyncContext.md) with its Run method, which ensures that the continuation runs on the message pump thread. + +### Use Blocking I/O in Reactor + +For the Performer, within the Proactor message pump, we want to use blocking I/O if the transport supports it. This should be the most performant way to use the transport, although it comes at the cost of not yielding to other Performers whilst waiting for I/O to complete. This has less impact when running in a container in production environments. + +If the underlying SDK does not support blocking I/O, then the Reactor model is forced to use non-blocking I/O. In Reactor code paths we should avoid blocking constructs such as ```Wait()```, ```.Result```, ```.GetAwaiter().GetResult()``` and so on, for wrapping anywhere we need to be sync-over-async, because there is no sync path. So how do we do sync-over-async? By using ```BrighterSynchronizationHelper.Run``` we can run an async method, without bubbling up await (see below). This helps us to avoid deadlocks from thread pool exhaustion, caused by no threads being available for the continuation - instead we just queue the continuations onto our single-threaded Performer. Whilst the latter isn't strictly necessary, as we only process a single message at a time, it does help us to avoid deadlocks, and to ensure that we process work in the order it was received. + +### Use Non-Blocking I/O in Proactor + +For the Performer, within the Proactor message pump, we want to use non-blocking I/O if the transport supports it. This will allow us to yield to other Performers whilst waiting for I/O to complete. This allows us to share resources better. + +In the Proactor code paths production code we should avoid blocking constructs such as ```Wait()```, ```.Result```, ```.GetAwaiter().GetResult()``` and so on. These will prevent us from yielding to other threads. Our Performers don't run on Thread Pool threads, so the issue here is not thread pool exhaustion and resulting deadlocks. However, the fact that we will not be able to yield to another Performer (or other work on the same machine). This is an inefficient usage of resources; if someone has chosen to use the Proactor model to share resources better. In a sense, it's violating the promise that our Proactor makes. So we should avoid these calls in production code that would be exercised by the Proactor. + +If the underlying SDK does not support non-blocking I/O then we will need to use ```Thread.Run(() => //...async method)```. Although this uses an extra thread, the impact is minimal as we only process a single message at a time. It is unlikely we will hit starvation of the thread pool. + +### Improve the Single-Threaded Synchronization Context with a Task Scheduler + +Our custom SynchronizationContext, BrighterSynchronizationContext, can ensure that continuations run on the message pump thread, and not on the thread pool. + +in V9, we have only use the synchronization context for user code: the transformer and handler calls. From V10 we want to extend the support to calls to the transport, whist we are waiting for I/O. + +At this point we have chosen to adopt Stephen Cleary's [AsyncEx's AsyncContext](https://github.com/StephenCleary/AsyncEx/blob/master/doc/AsyncContext.md) project over further developing our own. However, AsyncEx is not strong named, making it difficult to use directly. In addition, we want to modify it. So we will create our own internal fork of AsyncEx - it is MIT licensed so we can do this - and then add any bug fixes we need for our context to that. This class BrighterSynchronizationContext helper will be modelled on [AsyncEx's AsyncContext](https://github.com/StephenCleary/AsyncEx/blob/master/doc/AsyncContext.md) with its Run method, which ensures that the continuation runs on the message pump thread. + +This allows us to simplify running the Proactor message pump, and to take advantage of non-blocking I/O where possible. In particular, we can write an async EventLoop method, that means the Proactor can take advantage of non-blocking I/O in the transport SDKs, transformers and user defined handlers where they support it. Then in our Proactors's Run method we just wrap the call to EventLoop in ```BrighterSunchronizationContext.Run```, to terminate the async path, bubble up exceptions etc. This allows a single path for both ```Performer.Run``` and ```Consumer.Open``` regardless of whether they are working with a Proactor or Reactor. + +This allows to simplify working with sync-over-async for the Reactor. We can just author an async method and then use ```BrigherSynchronizationContext.Run``` to run it. This will ensure that the continuation runs on the message pump thread, and that we do not deadlock. + + However, the implication of Stephen Toub's article [here](https://devblogs.microsoft.com/dotnet/configureawait-faq/) that there is no route around ConfigureAwait(false), so it looks we will have to document the risks of using ConfigureAwait(false) in our code (out of order handling). See Consequences, for more on this. + +### Extending Transport Support for Async + +We will need to make changes to the Proactor to use async code within the pump, and to use the non-blocking I/O where possible. Currently, Brighter only supports an IAmAMessageConsumer interface and does not support an IAmAMessageConsumerAsync interface. This means that within a Proactor, we cannot take advantage of the non-blocking I/O; we are forced to block on the non-blocking I/O. We will address this by adding an IAmAMessageConsumerAsync interface, which will allow a Proactor to take advantage of non-blocking I/O. To do this we need to add an async version of the IAmAChannel interface, IAmAChannelAsync. This also means that we need to implement a ChannelAsync which derives from that. + +As a result we will have a different Channel for the Proactor and Reactor models. As a result we need to extend the ChannelFactory to create both Channel and ChannelAsync, reflecting the needs of the chosen pipeline. However, there is underlying commonality that we can factor into a base class. This helps the Performer. It only needs to be able to Stop the Channel. By extracting that into a common interface we can avoid having to duplicate the Peformer code. + +### Tests for Proactor and Reactor, Async Transport Paths + +As we will have additional interfaces, we will need to duplicate some tests, to exercise those interfaces. In addition, we will need to ensure that the coverage of Proactor and Reactor is complete, and that we have tests for the async paths in the Proactor. + +## Consequences + +### Reactor and Proactor + +Brighter offers you explicit control, through the number of Performers you run, over how many threads are required, instead of implicit scaling through the pool. This has significant advantages for messaging consumers, as it allows you to maintain ordering, such as when consuming a stream instead of a queue. + +### Synchronization Context + +The BrighterSynchronizationContext will lead to some complicated debugging issues where we interact with the async/await pattern. This code is not easy, and errors may manifest in new ways when they propogate through the context. We could decide that as we cannot control ConfigureAwait, we should just choose to queue on the threadpool and use the default synchronizationcontext and task scheduler. + +Another alternative would be to use the [Visual Studio Synchronization Context](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.threading.singlethreadedsynchronizationcontext?view=visualstudiosdk-2022) instead of modelling from Stephen Cleary's AsyncEx. diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsReceiverConsole/Program.cs b/samples/TaskQueue/ASBTaskQueue/GreetingsReceiverConsole/Program.cs index ee2d846c4a..9652c3d265 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsReceiverConsole/Program.cs +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsReceiverConsole/Program.cs @@ -36,7 +36,7 @@ public async static Task Main(string[] args) timeOut: TimeSpan.FromMilliseconds(400), makeChannels: OnMissingChannel.Create, requeueCount: 3, - isAsync: true), + messagePumpType: MessagePumpType.Proactor), new AzureServiceBusSubscription( new SubscriptionName("Event"), @@ -45,7 +45,7 @@ public async static Task Main(string[] args) timeOut: TimeSpan.FromMilliseconds(400), makeChannels: OnMissingChannel.Create, requeueCount: 3, - isAsync: false) + messagePumpType: MessagePumpType.Reactor) }; //TODO: add your ASB qualified name here diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsScopedReceiverConsole/Program.cs b/samples/TaskQueue/ASBTaskQueue/GreetingsScopedReceiverConsole/Program.cs index 432a3ff6d2..a57cfe199b 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsScopedReceiverConsole/Program.cs +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsScopedReceiverConsole/Program.cs @@ -38,7 +38,7 @@ public static async Task Main(string[] args) timeOut: TimeSpan.FromMilliseconds(400), makeChannels: OnMissingChannel.Create, requeueCount: 3, - isAsync: true), + messagePumpType: MessagePumpType.Proactor), new AzureServiceBusSubscription( new SubscriptionName("Event"), @@ -47,7 +47,7 @@ public static async Task Main(string[] args) timeOut: TimeSpan.FromMilliseconds(400), makeChannels: OnMissingChannel.Create, requeueCount: 3, - isAsync: false) + messagePumpType: MessagePumpType.Proactor) }; //TODO: add your ASB qualified name here diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsWorker/Program.cs b/samples/TaskQueue/ASBTaskQueue/GreetingsWorker/Program.cs index 0a9171624d..2a607272b3 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsWorker/Program.cs +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsWorker/Program.cs @@ -36,7 +36,7 @@ timeOut: TimeSpan.FromMilliseconds(400), makeChannels: OnMissingChannel.Create, requeueCount: 3, - isAsync: true, + messagePumpType: MessagePumpType.Proactor, noOfPerformers: 2, unacceptableMessageLimit: 1), new AzureServiceBusSubscription( new SubscriptionName("Greeting Async Event"), @@ -45,7 +45,7 @@ timeOut: TimeSpan.FromMilliseconds(400), makeChannels: OnMissingChannel.Create, requeueCount: 3, - isAsync: false, + messagePumpType: MessagePumpType.Reactor, noOfPerformers: 2), new AzureServiceBusSubscription( new SubscriptionName("Greeting Command"), @@ -54,7 +54,7 @@ timeOut: TimeSpan.FromMilliseconds(400), makeChannels: OnMissingChannel.Create, requeueCount: 3, - isAsync: true, + messagePumpType: MessagePumpType.Reactor, noOfPerformers: 2) }; diff --git a/samples/TaskQueue/KafkaSchemaRegistry/GreetingsReceiverConsole/Program.cs b/samples/TaskQueue/KafkaSchemaRegistry/GreetingsReceiverConsole/Program.cs index 9b8c83ec6a..2f3546076e 100644 --- a/samples/TaskQueue/KafkaSchemaRegistry/GreetingsReceiverConsole/Program.cs +++ b/samples/TaskQueue/KafkaSchemaRegistry/GreetingsReceiverConsole/Program.cs @@ -62,7 +62,7 @@ THE SOFTWARE. */ offsetDefault: AutoOffsetReset.Earliest, commitBatchSize: 5, sweepUncommittedOffsetsInterval: TimeSpan.FromMilliseconds(10000), - runAsync: true) + messagePumpType: MessagePumpType.Proactor) }; //We take a direct dependency on the schema registry in the message mapper diff --git a/samples/TaskQueue/KafkaTaskQueue/GreetingsReceiverConsole/Program.cs b/samples/TaskQueue/KafkaTaskQueue/GreetingsReceiverConsole/Program.cs index 39832f44a6..c5071c39ab 100644 --- a/samples/TaskQueue/KafkaTaskQueue/GreetingsReceiverConsole/Program.cs +++ b/samples/TaskQueue/KafkaTaskQueue/GreetingsReceiverConsole/Program.cs @@ -62,7 +62,7 @@ THE SOFTWARE. */ offsetDefault: AutoOffsetReset.Earliest, commitBatchSize: 5, sweepUncommittedOffsetsInterval: TimeSpan.FromMilliseconds(10000), - runAsync:true) + messagePumpType: MessagePumpType.Proactor) }; //create the gateway diff --git a/samples/WebAPI/WebAPI_Common/TransportMaker/ConfigureTransport.cs b/samples/WebAPI/WebAPI_Common/TransportMaker/ConfigureTransport.cs index b0c02d81d9..8148a14472 100644 --- a/samples/WebAPI/WebAPI_Common/TransportMaker/ConfigureTransport.cs +++ b/samples/WebAPI/WebAPI_Common/TransportMaker/ConfigureTransport.cs @@ -177,7 +177,7 @@ static Subscription[] GetRmqSubscriptions() where T : class, IRequest new SubscriptionName(typeof(T).Name), new ChannelName(typeof(T).Name), new RoutingKey(typeof(T).Name), - runAsync: true, + messagePumpType: MessagePumpType.Proactor, timeOut: TimeSpan.FromMilliseconds(200), isDurable: true, makeChannels: OnMissingChannel.Create) @@ -198,7 +198,7 @@ static Subscription[] GetKafkaSubscriptions() where T : class, IRequest offsetDefault: AutoOffsetReset.Earliest, commitBatchSize: 5, sweepUncommittedOffsetsInterval: TimeSpan.FromMilliseconds(10000), - runAsync: true, + messagePumpType: MessagePumpType.Proactor, makeChannels: OnMissingChannel.Create) }; return subscriptions; diff --git a/samples/WebAPI/WebAPI_Dynamo/SalutationAnalytics/Program.cs b/samples/WebAPI/WebAPI_Dynamo/SalutationAnalytics/Program.cs index 3d515359dc..49581eb9c6 100644 --- a/samples/WebAPI/WebAPI_Dynamo/SalutationAnalytics/Program.cs +++ b/samples/WebAPI/WebAPI_Dynamo/SalutationAnalytics/Program.cs @@ -63,7 +63,7 @@ static void ConfigureBrighter( new SubscriptionName("paramore.sample.salutationanalytics"), new ChannelName("SalutationAnalytics"), new RoutingKey("GreetingMade"), - runAsync: true, + messagePumpType: MessagePumpType.Proactor, timeOut: TimeSpan.FromMilliseconds(200), isDurable: true, makeChannels: OnMissingChannel diff --git a/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Handlers/GreetingMadeHandler.cs b/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Handlers/GreetingMadeHandlerAsync.cs similarity index 100% rename from samples/WebAPI/WebAPI_Dynamo/SalutationApp/Handlers/GreetingMadeHandler.cs rename to samples/WebAPI/WebAPI_Dynamo/SalutationApp/Handlers/GreetingMadeHandlerAsync.cs diff --git a/samples/WebAPI/WebAPI_EFCore/SalutationAnalytics/Program.cs b/samples/WebAPI/WebAPI_EFCore/SalutationAnalytics/Program.cs index 054f0e1be9..90b193f9fa 100644 --- a/samples/WebAPI/WebAPI_EFCore/SalutationAnalytics/Program.cs +++ b/samples/WebAPI/WebAPI_EFCore/SalutationAnalytics/Program.cs @@ -97,7 +97,7 @@ static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection new SubscriptionName("paramore.sample.salutationanalytics"), new ChannelName("SalutationAnalytics"), new RoutingKey("GreetingMade"), - runAsync: true, + messagePumpType: MessagePumpType.Proactor, timeOut: TimeSpan.FromMilliseconds(200), isDurable: true, makeChannels: OnMissingChannel diff --git a/samples/WebAPI/WebAPI_EFCore/SalutationApp/Handlers/GreetingMadeHandler.cs b/samples/WebAPI/WebAPI_EFCore/SalutationApp/Handlers/GreetingMadeHandlerAsync.cs similarity index 100% rename from samples/WebAPI/WebAPI_EFCore/SalutationApp/Handlers/GreetingMadeHandler.cs rename to samples/WebAPI/WebAPI_EFCore/SalutationApp/Handlers/GreetingMadeHandlerAsync.cs diff --git a/src/Paramore.Brighter.DynamoDb/DynamoDbUnitOfWork.cs b/src/Paramore.Brighter.DynamoDb/DynamoDbUnitOfWork.cs index eb56f7cc90..3f2f1ecc48 100644 --- a/src/Paramore.Brighter.DynamoDb/DynamoDbUnitOfWork.cs +++ b/src/Paramore.Brighter.DynamoDb/DynamoDbUnitOfWork.cs @@ -1,32 +1,51 @@ -using System; +#region Licence +/* The MIT License (MIT) +Copyright © 2022 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.Net; using System.Threading; using System.Threading.Tasks; using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; +using Paramore.Brighter.Tasks; namespace Paramore.Brighter.DynamoDb { - public class DynamoDbUnitOfWork : IAmADynamoDbTransactionProvider, IDisposable + public class DynamoDbUnitOfWork(IAmazonDynamoDB dynamoDb) : IAmADynamoDbTransactionProvider, IDisposable { private TransactWriteItemsRequest _tx; /// /// The AWS client for dynamoDb /// - public IAmazonDynamoDB DynamoDb { get; } - + public IAmazonDynamoDB DynamoDb { get; } = dynamoDb; + /// /// The response for the last transaction commit /// public TransactWriteItemsResponse LastResponse { get; set; } - public DynamoDbUnitOfWork(IAmazonDynamoDB dynamoDb) - { - DynamoDb = dynamoDb; - } - public void Close() { _tx = null; @@ -34,15 +53,9 @@ public void Close() /// /// Commit a transaction, performing all associated write actions + /// Will block thread and use second thread for callback /// - public void Commit() - { - if (!HasOpenTransaction) - throw new InvalidOperationException("No transaction to commit"); - - LastResponse = DynamoDb.TransactWriteItemsAsync(_tx).GetAwaiter().GetResult(); - - } + public void Commit() => BrighterSynchronizationHelper.Run(() => DynamoDb.TransactWriteItemsAsync(_tx)); /// /// Commit a transaction, performing all associated write actions diff --git a/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxSweeper.cs b/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxSweeper.cs index b29ebdee9a..d698cdc1f1 100644 --- a/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxSweeper.cs +++ b/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxSweeper.cs @@ -129,7 +129,10 @@ private void Sweep(object state) } finally { - _distributedLock.ReleaseLockAsync(LockingResourceName, lockId, CancellationToken.None).Wait(); + //on a timer thread, so blocking is OK + _distributedLock.ReleaseLockAsync(LockingResourceName, lockId, CancellationToken.None) + .GetAwaiter() + .GetResult(); scope.Dispose(); } } diff --git a/src/Paramore.Brighter.Inbox.DynamoDB/DynamoDbInbox.cs b/src/Paramore.Brighter.Inbox.DynamoDB/DynamoDbInbox.cs index 4063fef699..ba22cd6117 100644 --- a/src/Paramore.Brighter.Inbox.DynamoDB/DynamoDbInbox.cs +++ b/src/Paramore.Brighter.Inbox.DynamoDB/DynamoDbInbox.cs @@ -56,7 +56,8 @@ public DynamoDbInbox(IAmazonDynamoDB client, DynamoDbInboxConfiguration configur } /// - /// Adds a command to the store + /// Adds a command to the store + /// Will block, and consume another thread for callback on threadpool; use within sync pipeline only /// /// /// The command to be stored @@ -71,7 +72,7 @@ public void Add(T command, string contextKey, int timeoutInMilliseconds = -1) } /// - /// Finds a command with the specified identifier. + /// Finds a command with the specified identifier. /// /// /// The identifier. diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSClientFactory.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSClientFactory.cs index b7dd7f3a85..87a80ca9e9 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSClientFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSClientFactory.cs @@ -1,75 +1,98 @@ -using System; +#region Licence +/* The MIT License (MIT) +Copyright © 2022 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 Amazon; using Amazon.Runtime; using Amazon.SecurityToken; using Amazon.SimpleNotificationService; using Amazon.SQS; -namespace Paramore.Brighter.MessagingGateway.AWSSQS +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +internal class AWSClientFactory { - internal class AWSClientFactory + private readonly AWSCredentials _credentials; + private readonly RegionEndpoint _region; + private readonly Action? _clientConfigAction; + + public AWSClientFactory(AWSMessagingGatewayConnection connection) { - private AWSCredentials _credentials; - private RegionEndpoint _region; - private Action _clientConfigAction; + _credentials = connection.Credentials; + _region = connection.Region; + _clientConfigAction = connection.ClientConfigAction; + } - public AWSClientFactory(AWSMessagingGatewayConnection connection) - { - _credentials = connection.Credentials; - _region = connection.Region; - _clientConfigAction = connection.ClientConfigAction; - } + public AWSClientFactory(AWSCredentials credentials, RegionEndpoint region, Action? clientConfigAction) + { + _credentials = credentials; + _region = region; + _clientConfigAction = clientConfigAction; + } - public AWSClientFactory(AWSCredentials credentials, RegionEndpoint region, Action clientConfigAction) + public AmazonSimpleNotificationServiceClient CreateSnsClient() + { + var config = new AmazonSimpleNotificationServiceConfig { - _credentials = credentials; - _region = region; - _clientConfigAction = clientConfigAction; - } + RegionEndpoint = _region + }; - public AmazonSimpleNotificationServiceClient CreateSnsClient() + if (_clientConfigAction != null) { - var config = new AmazonSimpleNotificationServiceConfig - { - RegionEndpoint = _region - }; - - if (_clientConfigAction != null) - { - _clientConfigAction(config); - } - - return new AmazonSimpleNotificationServiceClient(_credentials, config); + _clientConfigAction(config); } - public AmazonSQSClient CreateSqsClient() - { - var config = new AmazonSQSConfig - { - RegionEndpoint = _region - }; + return new AmazonSimpleNotificationServiceClient(_credentials, config); + } - if (_clientConfigAction != null) - { - _clientConfigAction(config); - } + public AmazonSQSClient CreateSqsClient() + { + var config = new AmazonSQSConfig + { + RegionEndpoint = _region + }; - return new AmazonSQSClient(_credentials, config); + if (_clientConfigAction != null) + { + _clientConfigAction(config); } - public AmazonSecurityTokenServiceClient CreateStsClient() - { - var config = new AmazonSecurityTokenServiceConfig - { - RegionEndpoint = _region - }; + return new AmazonSQSClient(_credentials, config); + } - if (_clientConfigAction != null) - { - _clientConfigAction(config); - } + public AmazonSecurityTokenServiceClient CreateStsClient() + { + var config = new AmazonSecurityTokenServiceConfig + { + RegionEndpoint = _region + }; - return new AmazonSecurityTokenServiceClient(_credentials, config); + if (_clientConfigAction != null) + { + _clientConfigAction(config); } + + return new AmazonSecurityTokenServiceClient(_credentials, config); } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSMessagingGateway.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSMessagingGateway.cs index 27f6624254..73e8cde782 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSMessagingGateway.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSMessagingGateway.cs @@ -24,86 +24,84 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; -using Amazon.SimpleNotificationService; using Amazon.SimpleNotificationService.Model; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; -namespace Paramore.Brighter.MessagingGateway.AWSSQS -{ - public class AWSMessagingGateway - { - protected static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - protected AWSMessagingGatewayConnection _awsConnection; - protected string ChannelTopicArn; +namespace Paramore.Brighter.MessagingGateway.AWSSQS; - private AWSClientFactory _awsClientFactory; +public class AWSMessagingGateway(AWSMessagingGatewayConnection awsConnection) +{ + protected static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + + private readonly AWSClientFactory _awsClientFactory = new(awsConnection); + protected readonly AWSMessagingGatewayConnection AwsConnection = awsConnection; + protected string? ChannelTopicArn; - public AWSMessagingGateway(AWSMessagingGatewayConnection awsConnection) - { - _awsConnection = awsConnection; - _awsClientFactory = new AWSClientFactory(awsConnection); - } + protected async Task EnsureTopicAsync( + RoutingKey topic, + TopicFindBy topicFindBy, + SnsAttributes? attributes, + OnMissingChannel makeTopic = OnMissingChannel.Create, + CancellationToken cancellationToken = default) + { + //on validate or assume, turn a routing key into a topicARN + if ((makeTopic == OnMissingChannel.Assume) || (makeTopic == OnMissingChannel.Validate)) + await ValidateTopicAsync(topic, topicFindBy, cancellationToken); + else if (makeTopic == OnMissingChannel.Create) await CreateTopicAsync(topic, attributes); + return ChannelTopicArn; + } - protected async Task EnsureTopicAsync(RoutingKey topic, SnsAttributes attributes, TopicFindBy topicFindBy, OnMissingChannel makeTopic) + private async Task CreateTopicAsync(RoutingKey topicName, SnsAttributes? snsAttributes) + { + using var snsClient = _awsClientFactory.CreateSnsClient(); + var attributes = new Dictionary(); + if (snsAttributes != null) { - //on validate or assume, turn a routing key into a topicARN - if ((makeTopic == OnMissingChannel.Assume) || (makeTopic == OnMissingChannel.Validate)) - await ValidateTopicAsync(topic, topicFindBy, makeTopic); - else if (makeTopic == OnMissingChannel.Create) CreateTopic(topic, attributes); - return ChannelTopicArn; + if (!string.IsNullOrEmpty(snsAttributes.DeliveryPolicy)) attributes.Add("DeliveryPolicy", snsAttributes.DeliveryPolicy); + if (!string.IsNullOrEmpty(snsAttributes.Policy)) attributes.Add("Policy", snsAttributes.Policy); } - private void CreateTopic(RoutingKey topicName, SnsAttributes snsAttributes) + var createTopicRequest = new CreateTopicRequest(topicName) { - using var snsClient = _awsClientFactory.CreateSnsClient(); - var attributes = new Dictionary(); - if (snsAttributes != null) - { - if (!string.IsNullOrEmpty(snsAttributes.DeliveryPolicy)) attributes.Add("DeliveryPolicy", snsAttributes.DeliveryPolicy); - if (!string.IsNullOrEmpty(snsAttributes.Policy)) attributes.Add("Policy", snsAttributes.Policy); - } - - var createTopicRequest = new CreateTopicRequest(topicName) - { - Attributes = attributes, - Tags = new List {new Tag {Key = "Source", Value = "Brighter"}} - }; + Attributes = attributes, + Tags = new List {new Tag {Key = "Source", Value = "Brighter"}} + }; - //create topic is idempotent, so safe to call even if topic already exists - var createTopic = snsClient.CreateTopicAsync(createTopicRequest).Result; + //create topic is idempotent, so safe to call even if topic already exists + var createTopic = await snsClient.CreateTopicAsync(createTopicRequest); - if (!string.IsNullOrEmpty(createTopic.TopicArn)) - ChannelTopicArn = createTopic.TopicArn; - else - throw new InvalidOperationException($"Could not create Topic topic: {topicName} on {_awsConnection.Region}"); - } + if (!string.IsNullOrEmpty(createTopic.TopicArn)) + ChannelTopicArn = createTopic.TopicArn; + else + throw new InvalidOperationException($"Could not create Topic topic: {topicName} on {AwsConnection.Region}"); + } - private async Task ValidateTopicAsync(RoutingKey topic, TopicFindBy findTopicBy, OnMissingChannel onMissingChannel) - { - IValidateTopic topicValidationStrategy = GetTopicValidationStrategy(findTopicBy); - (bool exists, string topicArn) = await topicValidationStrategy.ValidateAsync(topic); - if (exists) - ChannelTopicArn = topicArn; - else - throw new BrokerUnreachableException( - $"Topic validation error: could not find topic {topic}. Did you want Brighter to create infrastructure?"); - } + private async Task ValidateTopicAsync(RoutingKey topic, TopicFindBy findTopicBy, CancellationToken cancellationToken = default) + { + IValidateTopic topicValidationStrategy = GetTopicValidationStrategy(findTopicBy); + (bool exists, string? topicArn) = await topicValidationStrategy.ValidateAsync(topic); + if (exists) + ChannelTopicArn = topicArn; + else + throw new BrokerUnreachableException( + $"Topic validation error: could not find topic {topic}. Did you want Brighter to create infrastructure?"); + } - private IValidateTopic GetTopicValidationStrategy(TopicFindBy findTopicBy) + private IValidateTopic GetTopicValidationStrategy(TopicFindBy findTopicBy) + { + switch (findTopicBy) { - switch (findTopicBy) - { - case TopicFindBy.Arn: - return new ValidateTopicByArn(_awsConnection.Credentials, _awsConnection.Region, _awsConnection.ClientConfigAction); - case TopicFindBy.Convention: - return new ValidateTopicByArnConvention(_awsConnection.Credentials, _awsConnection.Region, _awsConnection.ClientConfigAction); - case TopicFindBy.Name: - return new ValidateTopicByName(_awsConnection.Credentials, _awsConnection.Region, _awsConnection.ClientConfigAction); - default: - throw new ConfigurationException("Unknown TopicFindBy used to determine how to read RoutingKey"); - } + case TopicFindBy.Arn: + return new ValidateTopicByArn(AwsConnection.Credentials, AwsConnection.Region, AwsConnection.ClientConfigAction); + case TopicFindBy.Convention: + return new ValidateTopicByArnConvention(AwsConnection.Credentials, AwsConnection.Region, AwsConnection.ClientConfigAction); + case TopicFindBy.Name: + return new ValidateTopicByName(AwsConnection.Credentials, AwsConnection.Region, AwsConnection.ClientConfigAction); + default: + throw new ConfigurationException("Unknown TopicFindBy used to determine how to read RoutingKey"); } } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSMessagingGatewayConfiguration.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSMessagingGatewayConfiguration.cs index 35ae111199..f7234f3fdf 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSMessagingGatewayConfiguration.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSMessagingGatewayConfiguration.cs @@ -39,7 +39,7 @@ public class AWSMessagingGatewayConnection : IAmGatewayConfiguration /// A credentials object for an AWS service /// The AWS region to connect to /// An optional action to apply to the configuration of AWS service clients - public AWSMessagingGatewayConnection(AWSCredentials credentials, RegionEndpoint region, Action clientConfigAction = null) + public AWSMessagingGatewayConnection(AWSCredentials credentials, RegionEndpoint region, Action? clientConfigAction = null) { Credentials = credentials; Region = region; @@ -48,6 +48,6 @@ public AWSMessagingGatewayConnection(AWSCredentials credentials, RegionEndpoint public AWSCredentials Credentials { get; } public RegionEndpoint Region { get; } - public Action ClientConfigAction { get; } + public Action? ClientConfigAction { get; } } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSNameExtensions.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSNameExtensions.cs index fc1d94bb3f..222521766c 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSNameExtensions.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSNameExtensions.cs @@ -25,8 +25,11 @@ namespace Paramore.Brighter.MessagingGateway.AWSSQS { public static class AWSNameExtensions { - public static ChannelName ToValidSQSQueueName(this ChannelName channelName, bool isFifo = false) + public static ChannelName ToValidSQSQueueName(this ChannelName? channelName, bool isFifo = false) { + if (channelName is null) + return new ChannelName(string.Empty); + //SQS only allows 80 characters alphanumeric, hyphens, and underscores, but we might use a period in a //default typename strategy var name = channelName.Value; diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ChannelFactory.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ChannelFactory.cs index e267b850da..1f552977d6 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ChannelFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ChannelFactory.cs @@ -27,420 +27,458 @@ THE SOFTWARE. */ using System.Net; using System.Text.Json; using System.Threading; +using System.Threading.Tasks; using Amazon.Runtime.Internal; using Amazon.SimpleNotificationService; using Amazon.SimpleNotificationService.Model; using Amazon.SQS; using Amazon.SQS.Model; using Microsoft.Extensions.Logging; +using Paramore.Brighter.Tasks; using Polly; using Polly.Contrib.WaitAndRetry; using Polly.Retry; -namespace Paramore.Brighter.MessagingGateway.AWSSQS +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +/// +/// The class is responsible for creating and managing SQS channels. +/// +public class ChannelFactory : AWSMessagingGateway, IAmAChannelFactory { - public class ChannelFactory : AWSMessagingGateway, IAmAChannelFactory + private readonly SqsMessageConsumerFactory _messageConsumerFactory; + private SqsSubscription? _subscription; + private string? _queueUrl; + private string? _dlqARN; + private readonly AsyncRetryPolicy _retryPolicy; + + /// + /// Initializes a new instance of the class. + /// + /// The details of the subscription to AWS. + public ChannelFactory(AWSMessagingGatewayConnection awsConnection) + : base(awsConnection) { - private readonly SqsMessageConsumerFactory _messageConsumerFactory; - private SqsSubscription _subscription; - private string _queueUrl; - private string _dlqARN; - private readonly RetryPolicy _retryPolicy; - /// - /// Initializes a new instance of the class. - /// - /// The details of the subscription to AWS - public ChannelFactory( - AWSMessagingGatewayConnection awsConnection) - : base(awsConnection) - { - _messageConsumerFactory = new SqsMessageConsumerFactory(awsConnection); - var delay = Backoff.LinearBackoff(TimeSpan.FromSeconds(2), retryCount: 3, factor: 2.0, fastFirst: true); - _retryPolicy = Policy - .Handle() - .WaitAndRetry(new[] - { - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10) - }); - } - - /// - /// Creates the input channel. - /// With SQS we can ensure that queues exist ahead of creating the consumer, as there is no non-durable queue model - /// to create ephemeral queues, nor are there non-mirrored queues (on a single node in the cluster) where nodes - /// failing mean we want to create anew as we recreate. So the input factory creates the queue - /// - /// An SqsSubscription, the subscription parameter so create the channel with - /// IAmAnInputChannel. - public IAmAChannel CreateChannel(Subscription subscription) - { - var channel = _retryPolicy.Execute(() => + _messageConsumerFactory = new SqsMessageConsumerFactory(awsConnection); + _retryPolicy = Policy + .Handle() + .WaitAndRetryAsync(new[] { - SqsSubscription sqsSubscription = subscription as SqsSubscription; - _subscription = sqsSubscription ?? throw new ConfigurationException("We expect an SqsSubscription or SqsSubscription as a parameter"); - - EnsureTopicAsync(_subscription.RoutingKey, _subscription.SnsAttributes, _subscription.FindTopicBy, _subscription.MakeChannels).Wait(); - EnsureQueue(); - - return new Channel( - subscription.ChannelName.ToValidSQSQueueName(), - subscription.RoutingKey.ToValidSNSTopicName(), - _messageConsumerFactory.Create(subscription), - subscription.BufferSize - ); + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) }); + } - return channel; - } - - private void EnsureQueue() + /// + /// Creates the input channel. + /// Sync over Async is used here; should be alright in context of channel creation. + /// + /// An SqsSubscription, the subscription parameter to create the channel with. + /// An instance of . + /// Thrown when the subscription is not an SqsSubscription. + public IAmAChannelSync CreateSyncChannel(Subscription subscription) => BrighterSynchronizationHelper.Run(async () => await CreateSyncChannelAsync(subscription)); + + /// + /// Creates the input channel. + /// + /// + /// Sync over Async is used here; should be alright in context of channel creation. + /// + /// An SqsSubscription, the subscription parameter to create the channel with. + /// An instance of . + /// Thrown when the subscription is not an SqsSubscription. + public IAmAChannelAsync CreateAsyncChannel(Subscription subscription) => BrighterSynchronizationHelper.Run(async () => await CreateAsyncChannelAsync(subscription)); + + /// + /// Creates the input channel. + /// + /// An SqsSubscription, the subscription parameter to create the channel with. + /// Cancels the creation operation + /// An instance of . + /// Thrown when the subscription is not an SqsSubscription. + public async Task CreateAsyncChannelAsync(Subscription subscription, CancellationToken ct = default) + { + var channel = await _retryPolicy.ExecuteAsync(async () => { - if (_subscription.MakeChannels == OnMissingChannel.Assume) - return; + SqsSubscription? sqsSubscription = subscription as SqsSubscription; + _subscription = sqsSubscription ?? throw new ConfigurationException("We expect an SqsSubscription or SqsSubscription as a parameter"); + + await EnsureTopicAsync(_subscription.RoutingKey, _subscription.FindTopicBy, _subscription.SnsAttributes, _subscription.MakeChannels); + await EnsureQueueAsync(); + + return new ChannelAsync( + subscription.ChannelName.ToValidSQSQueueName(), + subscription.RoutingKey.ToValidSNSTopicName(), + _messageConsumerFactory.CreateAsync(subscription), + subscription.BufferSize + ); + }); - using var sqsClient = new AmazonSQSClient(_awsConnection.Credentials, _awsConnection.Region); - //Does the queue exist - this is an HTTP call, we should cache the results for a period of time - var queueName = _subscription.ChannelName.ToValidSQSQueueName(); - var topicName = _subscription.RoutingKey.ToValidSNSTopicName(); + return channel; + } + + /// + /// Deletes the queue. + /// + public async Task DeleteQueueAsync() + { + if (_subscription?.ChannelName is null) + return; - (bool exists, _) = QueueExists(sqsClient, queueName); - if (!exists) + using var sqsClient = new AmazonSQSClient(AwsConnection.Credentials, AwsConnection.Region); + (bool exists, string? queueUrl) queueExists = await QueueExistsAsync(sqsClient, _subscription.ChannelName.ToValidSQSQueueName()); + + if (queueExists.exists && queueExists.queueUrl != null) + { + try { - if (_subscription.MakeChannels == OnMissingChannel.Create) - { - if (_subscription.RedrivePolicy != null) - { - CreateDLQ(sqsClient); - } - - CreateQueue(sqsClient); - - } - else if (_subscription.MakeChannels == OnMissingChannel.Validate) - { - var message = $"Queue does not exist: {queueName} for {topicName} on {_awsConnection.Region}"; - s_logger.LogDebug("Queue does not exist: {ChannelName} for {Topic} on {Region}", queueName, - topicName, _awsConnection.Region); - throw new QueueDoesNotExistException(message); - } + sqsClient.DeleteQueueAsync(queueExists.queueUrl) + .GetAwaiter() + .GetResult(); } - else + catch (Exception) { - s_logger.LogDebug("Queue exists: {ChannelName} subscribed to {Topic} on {Region}", - queueName, topicName, _awsConnection.Region); + s_logger.LogError("Could not delete queue {ChannelName}", queueExists.queueUrl); } } + } - private void CreateQueue(AmazonSQSClient sqsClient) + /// + /// Deletes the topic. + /// + public async Task DeleteTopicAsync() + { + if (_subscription == null) + return; + + if (ChannelTopicArn == null) + return; + + using var snsClient = new AmazonSimpleNotificationServiceClient(AwsConnection.Credentials, AwsConnection.Region); + (bool exists, string? _) = await new ValidateTopicByArn(snsClient).ValidateAsync(ChannelTopicArn); + if (exists) { - s_logger.LogDebug( - "Queue does not exist, creating queue: {ChannelName} subscribed to {Topic} on {Region}", - _subscription.ChannelName.Value, _subscription.RoutingKey.Value, _awsConnection.Region); - _queueUrl = null; try { - var attributes = new Dictionary(); - if (_subscription.RedrivePolicy != null && _dlqARN != null) - { - var policy = new {maxReceiveCount = _subscription.RedrivePolicy.MaxReceiveCount, deadLetterTargetArn = _dlqARN}; - attributes.Add("RedrivePolicy", JsonSerializer.Serialize(policy, JsonSerialisationOptions.Options)); - } - - attributes.Add("DelaySeconds", _subscription.DelaySeconds.ToString()); - attributes.Add("MessageRetentionPeriod", _subscription.MessageRetentionPeriod.ToString()); - if (_subscription.IAMPolicy != null )attributes.Add("Policy", _subscription.IAMPolicy); - attributes.Add("ReceiveMessageWaitTimeSeconds", _subscription.TimeOut.Seconds.ToString()); - attributes.Add("VisibilityTimeout", _subscription.LockTimeout.ToString()); - - var tags = new Dictionary(); - tags.Add("Source","Brighter"); - if (_subscription.Tags != null) - { - foreach (var tag in _subscription.Tags) - { - tags.Add(tag.Key, tag.Value); - } - } - - var request = new CreateQueueRequest(_subscription.ChannelName.Value) - { - Attributes = attributes, - Tags = tags - }; - var response = sqsClient.CreateQueueAsync(request).GetAwaiter().GetResult(); - _queueUrl = response.QueueUrl; - - if (!string.IsNullOrEmpty(_queueUrl)) - { - s_logger.LogDebug("Queue created: {URL}", _queueUrl); - using var snsClient = new AmazonSimpleNotificationServiceClient(_awsConnection.Credentials, _awsConnection.Region); - CheckSubscription(_subscription.MakeChannels, sqsClient, snsClient); - } - else - { - throw new InvalidOperationException($"Could not create queue: {_subscription.ChannelName.Value} subscribed to {ChannelTopicArn} on {_awsConnection.Region}"); - } + await UnsubscribeFromTopicAsync(snsClient); + await snsClient.DeleteTopicAsync(ChannelTopicArn); } - catch (QueueDeletedRecentlyException ex) + catch (Exception) { - //QueueDeletedRecentlyException - wait 30 seconds then retry - //Although timeout is 60s, we could be partway through that, so apply Copernican Principle - //and assume we are halfway through - var error = $"Could not create queue {_subscription.ChannelName.Value} because {ex.Message} waiting 60s to retry"; - s_logger.LogError(ex, "Could not create queue {ChannelName} because {ErrorMessage} waiting 60s to retry", _subscription.ChannelName.Value, ex.Message); - Thread.Sleep(TimeSpan.FromSeconds(30)); - throw new ChannelFailureException(error, ex); + s_logger.LogError("Could not delete topic {TopicResourceName}", ChannelTopicArn); } - catch (AmazonSQSException ex) + } + } + + private async Task CreateSyncChannelAsync(Subscription subscription) + { + var channel = await _retryPolicy.ExecuteAsync(async () => + { + SqsSubscription? sqsSubscription = subscription as SqsSubscription; + _subscription = sqsSubscription ?? throw new ConfigurationException("We expect an SqsSubscription or SqsSubscription as a parameter"); + + await EnsureTopicAsync(_subscription.RoutingKey, _subscription.FindTopicBy, _subscription.SnsAttributes, + _subscription.MakeChannels); + await EnsureQueueAsync(); + + return new Channel( + subscription.ChannelName.ToValidSQSQueueName(), + subscription.RoutingKey.ToValidSNSTopicName(), + _messageConsumerFactory.Create(subscription), + subscription.BufferSize + ); + }); + + return channel; + } + + private async Task EnsureQueueAsync() + { + if (_subscription is null) + throw new InvalidOperationException("ChannelFactory: Subscription cannot be null"); + + if (_subscription.MakeChannels == OnMissingChannel.Assume) + return; + + using var sqsClient = new AmazonSQSClient(AwsConnection.Credentials, AwsConnection.Region); + var queueName = _subscription.ChannelName.ToValidSQSQueueName(); + var topicName = _subscription.RoutingKey.ToValidSNSTopicName(); + + (bool exists, _) = await QueueExistsAsync(sqsClient, queueName); + if (!exists) + { + if (_subscription.MakeChannels == OnMissingChannel.Create) { - var error = $"Could not create queue {_queueUrl} subscribed to topic {_subscription.RoutingKey.Value} in region {_awsConnection.Region.DisplayName} because {ex.Message}"; - s_logger.LogError(ex, - "Could not create queue {URL} subscribed to topic {Topic} in region {Region} because {ErrorMessage}", - _queueUrl, _subscription.RoutingKey.Value, _awsConnection.Region.DisplayName, ex.Message); - throw new InvalidOperationException(error, ex); + if (_subscription.RedrivePolicy != null) + { + await CreateDLQAsync(sqsClient); + } + + await CreateQueueAsync(sqsClient); } - catch (HttpErrorResponseException ex) + else if (_subscription.MakeChannels == OnMissingChannel.Validate) { - var error = $"Could not create queue {_queueUrl} subscribed to topic {_subscription.RoutingKey.Value} in region {_awsConnection.Region.DisplayName} because {ex.Message}"; - s_logger.LogError(ex, - "Could not create queue {URL} subscribed to topic {Topic} in region {Region} because {ErrorMessage}", - _queueUrl, _subscription.RoutingKey.Value, _awsConnection.Region.DisplayName, ex.Message); - throw new InvalidOperationException(error, ex); + var message = $"Queue does not exist: {queueName} for {topicName} on {AwsConnection.Region}"; + s_logger.LogDebug("Queue does not exist: {ChannelName} for {Topic} on {Region}", queueName, topicName, AwsConnection.Region); + throw new QueueDoesNotExistException(message); } } + else + { + s_logger.LogDebug("Queue exists: {ChannelName} subscribed to {Topic} on {Region}", queueName, topicName, AwsConnection.Region); + } + } - private void CreateDLQ(AmazonSQSClient sqsClient) + private async Task CreateQueueAsync(AmazonSQSClient sqsClient) + { + if (_subscription is null) + throw new InvalidOperationException("ChannelFactory: Subscription cannot be null"); + + s_logger.LogDebug("Queue does not exist, creating queue: {ChannelName} subscribed to {Topic} on {Region}", _subscription.ChannelName.Value, _subscription.RoutingKey.Value, AwsConnection.Region); + _queueUrl = null; + try { - try + var attributes = new Dictionary(); + if (_subscription.RedrivePolicy != null && _dlqARN != null) { - var request = new CreateQueueRequest(_subscription.RedrivePolicy.DeadlLetterQueueName.Value); - - var createDeadLetterQueueResponse = sqsClient.CreateQueueAsync(request).GetAwaiter().GetResult(); + var policy = new { maxReceiveCount = _subscription.RedrivePolicy.MaxReceiveCount, deadLetterTargetArn = _dlqARN }; + attributes.Add("RedrivePolicy", JsonSerializer.Serialize(policy, JsonSerialisationOptions.Options)); + } - var queueUrl = createDeadLetterQueueResponse.QueueUrl; + attributes.Add("DelaySeconds", _subscription.DelaySeconds.ToString()); + attributes.Add("MessageRetentionPeriod", _subscription.MessageRetentionPeriod.ToString()); + if (_subscription.IAMPolicy != null) attributes.Add("Policy", _subscription.IAMPolicy); + attributes.Add("ReceiveMessageWaitTimeSeconds", _subscription.TimeOut.Seconds.ToString()); + attributes.Add("VisibilityTimeout", _subscription.LockTimeout.ToString()); - if (!string.IsNullOrEmpty(queueUrl)) + var tags = new Dictionary { { "Source", "Brighter" } }; + if (_subscription.Tags != null) + { + foreach (var tag in _subscription.Tags) { - //We need the ARN of the dead letter queue to configure the queue redrive policy, not the name - var attributesRequest = new GetQueueAttributesRequest - { - QueueUrl = queueUrl, - AttributeNames = new List {"QueueArn"} - }; - var attributesResponse = sqsClient.GetQueueAttributesAsync(attributesRequest).GetAwaiter().GetResult(); - - if (attributesResponse.HttpStatusCode != HttpStatusCode.OK) - throw new InvalidOperationException($"Could not find ARN of DLQ, status: {attributesResponse.HttpStatusCode}"); - - _dlqARN = attributesResponse.QueueARN; + tags.Add(tag.Key, tag.Value); } - else - throw new InvalidOperationException($"Could not find create DLQ, status: {createDeadLetterQueueResponse.HttpStatusCode}"); } - catch (QueueDeletedRecentlyException ex) + + var request = new CreateQueueRequest(_subscription.ChannelName.Value) { - //QueueDeletedRecentlyException - wait 30 seconds then retry - //Although timeout is 60s, we could be partway through that, so apply Copernican Principle - //and assume we are halfway through - var error = $"Could not create queue {_subscription.ChannelName.Value} because {ex.Message} waiting 60s to retry"; - s_logger.LogError(ex, - "Could not create queue {ChannelName} because {ErrorMessage} waiting 60s to retry", - _subscription.ChannelName.Value, ex.Message); - Thread.Sleep(TimeSpan.FromSeconds(30)); - throw new ChannelFailureException(error, ex); - } - catch (AmazonSQSException ex) + Attributes = attributes, + Tags = tags + }; + var response = await sqsClient.CreateQueueAsync(request); + _queueUrl = response.QueueUrl; + + if (!string.IsNullOrEmpty(_queueUrl)) { - var error = $"Could not create queue {_queueUrl} subscribed to topic {_subscription.RoutingKey.Value} in region {_awsConnection.Region.DisplayName} because {ex.Message}"; - s_logger.LogError(ex, - "Could not create queue {URL} subscribed to topic {Topic} in region {Region} because {ErrorMessage}", - _queueUrl, _subscription.RoutingKey.Value, _awsConnection.Region.DisplayName, ex.Message); - throw new InvalidOperationException(error, ex); + s_logger.LogDebug("Queue created: {URL}", _queueUrl); + using var snsClient = new AmazonSimpleNotificationServiceClient(AwsConnection.Credentials, AwsConnection.Region); + await CheckSubscriptionAsync(_subscription.MakeChannels, sqsClient, snsClient); } - catch (HttpErrorResponseException ex) + else { - var error = $"Could not create queue {_queueUrl} subscribed to topic {_subscription.RoutingKey.Value} in region {_awsConnection.Region.DisplayName} because {ex.Message}"; - s_logger.LogError(ex, "Could not create queue {URL} subscribed to topic {Topic} in region {Region} because {ErrorMessage}", - _queueUrl, _subscription.RoutingKey.Value, _awsConnection.Region.DisplayName, ex.Message); - throw new InvalidOperationException(error, ex); + throw new InvalidOperationException($"Could not create queue: {_subscription.ChannelName.Value} subscribed to {ChannelTopicArn} on {AwsConnection.Region}"); } } + catch (QueueDeletedRecentlyException ex) + { + var error = $"Could not create queue {_subscription.ChannelName.Value} because {ex.Message} waiting 60s to retry"; + s_logger.LogError(ex, "Could not create queue {ChannelName} because {ErrorMessage} waiting 60s to retry", _subscription.ChannelName.Value, ex.Message); + Thread.Sleep(TimeSpan.FromSeconds(30)); + throw new ChannelFailureException(error, ex); + } + catch (AmazonSQSException ex) + { + var error = $"Could not create queue {_queueUrl} subscribed to topic {_subscription.RoutingKey.Value} in region {AwsConnection.Region.DisplayName} because {ex.Message}"; + s_logger.LogError(ex, "Could not create queue {URL} subscribed to topic {Topic} in region {Region} because {ErrorMessage}", _queueUrl, _subscription.RoutingKey.Value, AwsConnection.Region.DisplayName, ex.Message); + throw new InvalidOperationException(error, ex); + } + catch (HttpErrorResponseException ex) + { + var error = $"Could not create queue {_queueUrl} subscribed to topic {_subscription.RoutingKey.Value} in region {AwsConnection.Region.DisplayName} because {ex.Message}"; + s_logger.LogError(ex, "Could not create queue {URL} subscribed to topic {Topic} in region {Region} because {ErrorMessage}", _queueUrl, _subscription.RoutingKey.Value, AwsConnection.Region.DisplayName, ex.Message); + throw new InvalidOperationException(error, ex); + } + } - private void CheckSubscription(OnMissingChannel makeSubscriptions, AmazonSQSClient sqsClient, AmazonSimpleNotificationServiceClient snsClient) + private async Task CreateDLQAsync(AmazonSQSClient sqsClient) + { + if (_subscription is null) + throw new InvalidOperationException("ChannelFactory: Subscription cannot be null"); + + if (_subscription.RedrivePolicy == null) + throw new InvalidOperationException("ChannelFactory: RedrivePolicy cannot be null when creating a DLQ"); + + try { - if (makeSubscriptions == OnMissingChannel.Assume) - return; + var request = new CreateQueueRequest(_subscription.RedrivePolicy.DeadlLetterQueueName.Value); + var createDeadLetterQueueResponse = await sqsClient.CreateQueueAsync(request); + var queueUrl = createDeadLetterQueueResponse.QueueUrl; - if (!SubscriptionExists(sqsClient, snsClient)) + if (!string.IsNullOrEmpty(queueUrl)) { - if (makeSubscriptions == OnMissingChannel.Validate) - { - throw new BrokerUnreachableException($"Subscription validation error: could not find subscription for {_queueUrl}"); - } - else if (makeSubscriptions == OnMissingChannel.Create) + var attributesRequest = new GetQueueAttributesRequest { - SubscribeToTopic(sqsClient, snsClient); - } + QueueUrl = queueUrl, + AttributeNames = ["QueueArn"] + }; + var attributesResponse = await sqsClient.GetQueueAttributesAsync(attributesRequest); + + if (attributesResponse.HttpStatusCode != HttpStatusCode.OK) + throw new InvalidOperationException($"Could not find ARN of DLQ, status: {attributesResponse.HttpStatusCode}"); + + _dlqARN = attributesResponse.QueueARN; } + else + throw new InvalidOperationException($"Could not find create DLQ, status: {createDeadLetterQueueResponse.HttpStatusCode}"); + } + catch (QueueDeletedRecentlyException ex) + { + var error = $"Could not create queue {_subscription.ChannelName.Value} because {ex.Message} waiting 60s to retry"; + s_logger.LogError(ex, "Could not create queue {ChannelName} because {ErrorMessage} waiting 60s to retry", _subscription.ChannelName.Value, ex.Message); + Thread.Sleep(TimeSpan.FromSeconds(30)); + throw new ChannelFailureException(error, ex); + } + catch (AmazonSQSException ex) + { + var error = $"Could not create queue {_queueUrl} subscribed to topic {_subscription.RoutingKey.Value} in region {AwsConnection.Region.DisplayName} because {ex.Message}"; + s_logger.LogError(ex, "Could not create queue {URL} subscribed to topic {Topic} in region {Region} because {ErrorMessage}", _queueUrl, _subscription.RoutingKey.Value, AwsConnection.Region.DisplayName, ex.Message); + throw new InvalidOperationException(error, ex); + } + catch (HttpErrorResponseException ex) + { + var error = $"Could not create queue {_queueUrl} subscribed to topic {_subscription.RoutingKey.Value} in region {AwsConnection.Region.DisplayName} because {ex.Message}"; + s_logger.LogError(ex, "Could not create queue {URL} subscribed to topic {Topic} in region {Region} because {ErrorMessage}", _queueUrl, _subscription.RoutingKey.Value, AwsConnection.Region.DisplayName, ex.Message); + throw new InvalidOperationException(error, ex); } + } + + private async Task CheckSubscriptionAsync(OnMissingChannel makeSubscriptions, AmazonSQSClient sqsClient, AmazonSimpleNotificationServiceClient snsClient) + { + if (makeSubscriptions == OnMissingChannel.Assume) + return; - private void SubscribeToTopic(AmazonSQSClient sqsClient, AmazonSimpleNotificationServiceClient snsClient) + if (!await SubscriptionExistsAsync(sqsClient, snsClient)) { - var subscription = snsClient.SubscribeQueueAsync(ChannelTopicArn, sqsClient, _queueUrl).Result; - if (!string.IsNullOrEmpty(subscription)) + if (makeSubscriptions == OnMissingChannel.Validate) { - //We need to support raw messages to allow the use of message attributes - var response = snsClient.SetSubscriptionAttributesAsync( - new SetSubscriptionAttributesRequest( - subscription, "RawMessageDelivery", _subscription.RawMessageDelivery.ToString()) - ) - .Result; - if (response.HttpStatusCode != HttpStatusCode.OK) - { - throw new InvalidOperationException("Unable to set subscription attribute for raw message delivery"); - } + throw new BrokerUnreachableException($"Subscription validation error: could not find subscription for {_queueUrl}"); } - else + else if (makeSubscriptions == OnMissingChannel.Create) { - throw new InvalidOperationException( - $"Could not subscribe to topic: {ChannelTopicArn} from queue: {_queueUrl} in region {_awsConnection.Region}"); + await SubscribeToTopicAsync(sqsClient, snsClient); } } + } - private (bool, string) QueueExists(AmazonSQSClient client, string channelName) + private async Task SubscribeToTopicAsync(AmazonSQSClient sqsClient, AmazonSimpleNotificationServiceClient snsClient) + { + var arn = await snsClient.SubscribeQueueAsync(ChannelTopicArn, sqsClient, _queueUrl); + if (!string.IsNullOrEmpty(arn)) { - bool exists = false; - string queueUrl = null; - try - { - var response = client.GetQueueUrlAsync(channelName).Result; - //If the queue does not exist yet then - if (!string.IsNullOrWhiteSpace(response.QueueUrl)) - { - queueUrl = response.QueueUrl; - exists = true; - } - } - catch (AggregateException ae) + var response = await snsClient.SetSubscriptionAttributesAsync( + new SetSubscriptionAttributesRequest(arn, "RawMessageDelivery", _subscription?.RawMessageDelivery.ToString()) + ); + if (response.HttpStatusCode != HttpStatusCode.OK) { - ae.Handle((e) => - { - if (e is QueueDoesNotExistException) - { - //handle this, because we expect a queue might be missing and will create - exists = false; - return true; - } - - //we didn't expect this - return false; - }); + throw new InvalidOperationException("Unable to set subscription attribute for raw message delivery"); } - - return (exists, queueUrl); } - - private bool SubscriptionExists(AmazonSQSClient sqsClient, AmazonSimpleNotificationServiceClient snsClient) + else { - string queueArn = GetQueueARNForChannel(sqsClient); - - if (queueArn == null) - throw new BrokerUnreachableException($"Could not find queue ARN for queue {_queueUrl}"); - - bool exists = false; - ListSubscriptionsByTopicResponse response; - do - { - response = snsClient.ListSubscriptionsByTopicAsync(new ListSubscriptionsByTopicRequest {TopicArn = ChannelTopicArn}).GetAwaiter().GetResult(); - exists = response.Subscriptions.Any(sub => (sub.Protocol.ToLower() == "sqs") && (sub.Endpoint == queueArn)); - } while (!exists && response.NextToken != null); - - return exists; + throw new InvalidOperationException($"Could not subscribe to topic: {ChannelTopicArn} from queue: {_queueUrl} in region {AwsConnection.Region}"); } + } - public void DeleteQueue() + private async Task<(bool exists, string? queueUrl)> QueueExistsAsync(AmazonSQSClient client, string? channelName) + { + if (string.IsNullOrEmpty(channelName)) + return (false, null); + + bool exists = false; + string? queueUrl = null; + try { - if (_subscription == null) - return; - - using var sqsClient = new AmazonSQSClient(_awsConnection.Credentials, _awsConnection.Region); - //Does the queue exist - this is an HTTP call, we should cache the results for a period of time - (bool exists, string name) queueExists = QueueExists(sqsClient, _subscription.ChannelName.ToValidSQSQueueName()); - - if (queueExists.exists) + var response = await client.GetQueueUrlAsync(channelName); + if (!string.IsNullOrWhiteSpace(response.QueueUrl)) { - try - { - sqsClient.DeleteQueueAsync(queueExists.name).Wait(); - } - catch (Exception) - { - //don't break on an exception here, if we can't delete, just exit - s_logger.LogError("Could not delete queue {ChannelName}", queueExists.name); - } + queueUrl = response.QueueUrl; + exists = true; } } - - public void DeleteTopic() + catch (AggregateException ae) { - if (_subscription == null) - return; - - using var snsClient = new AmazonSimpleNotificationServiceClient(_awsConnection.Credentials, _awsConnection.Region); - (bool exists, string topicArn) = new ValidateTopicByArn(snsClient).ValidateAsync(ChannelTopicArn).GetAwaiter().GetResult(); - if (exists) + ae.Handle((e) => { - try - { - UnsubscribeFromTopic(snsClient); - - DeleteTopic(snsClient); - } - catch (Exception) + if (e is QueueDoesNotExistException) { - //don't break on an exception here, if we can't delete, just exit - s_logger.LogError("Could not delete topic {TopicResourceName}", ChannelTopicArn); + exists = false; + return true; } - } + return false; + }); } - private void DeleteTopic(AmazonSimpleNotificationServiceClient snsClient) - { - snsClient.DeleteTopicAsync(ChannelTopicArn).GetAwaiter().GetResult(); - } + return (exists, queueUrl); + } + + private async Task SubscriptionExistsAsync(AmazonSQSClient sqsClient, AmazonSimpleNotificationServiceClient snsClient) + { + string? queueArn = await GetQueueArnForChannelAsync(sqsClient); + if (queueArn == null) + throw new BrokerUnreachableException($"Could not find queue ARN for queue {_queueUrl}"); - private string GetQueueARNForChannel(AmazonSQSClient sqsClient) + bool exists = false; + ListSubscriptionsByTopicResponse response; + do { - var result = sqsClient.GetQueueAttributesAsync( - new GetQueueAttributesRequest {QueueUrl = _queueUrl, AttributeNames = new List {"QueueArn"}} - ).GetAwaiter().GetResult(); + response = await snsClient.ListSubscriptionsByTopicAsync(new ListSubscriptionsByTopicRequest { TopicArn = ChannelTopicArn }); + exists = response.Subscriptions.Any(sub => (sub.Protocol.ToLower() == "sqs") && (sub.Endpoint == queueArn)); + } while (!exists && response.NextToken != null); - if (result.HttpStatusCode == HttpStatusCode.OK) - { - return result.QueueARN; - } + return exists; + } - return null; + /// + /// Gets the ARN of the queue for the channel. + /// Sync over async is used here; should be alright in context of channel creation. + /// + /// The SQS client. + /// The ARN of the queue. + private async Task GetQueueArnForChannelAsync(AmazonSQSClient sqsClient) + { + var result = await sqsClient.GetQueueAttributesAsync( + new GetQueueAttributesRequest { QueueUrl = _queueUrl, AttributeNames = new List { "QueueArn" } } + ); + + if (result.HttpStatusCode == HttpStatusCode.OK) + { + return result.QueueARN; } - private void UnsubscribeFromTopic(AmazonSimpleNotificationServiceClient snsClient) + return null; + } + + /// + /// Unsubscribes from the topic. + /// Sync over async is used here; should be alright in context of topic unsubscribe. + /// + /// The SNS client. + private async Task UnsubscribeFromTopicAsync(AmazonSimpleNotificationServiceClient snsClient) + { + ListSubscriptionsByTopicResponse response; + do { - ListSubscriptionsByTopicResponse response; - do + response = await snsClient.ListSubscriptionsByTopicAsync(new ListSubscriptionsByTopicRequest { TopicArn = ChannelTopicArn }); + foreach (var sub in response.Subscriptions) { - response = snsClient.ListSubscriptionsByTopicAsync(new ListSubscriptionsByTopicRequest {TopicArn = ChannelTopicArn}).GetAwaiter().GetResult(); - foreach (var sub in response.Subscriptions) + var unsubscribe = await snsClient.UnsubscribeAsync(new UnsubscribeRequest { SubscriptionArn = sub.SubscriptionArn }); + if (unsubscribe.HttpStatusCode != HttpStatusCode.OK) { - var unsubscribe = snsClient.UnsubscribeAsync(new UnsubscribeRequest {SubscriptionArn = sub.SubscriptionArn}).GetAwaiter().GetResult(); - if (unsubscribe.HttpStatusCode != HttpStatusCode.OK) - { - s_logger.LogError("Error unsubscribing from {TopicResourceName} for sub {ChannelResourceName}", ChannelTopicArn, sub.SubscriptionArn); - } + s_logger.LogError("Error unsubscribing from {TopicResourceName} for sub {ChannelResourceName}", ChannelTopicArn, sub.SubscriptionArn); } - } while (response.NextToken != null); - } + } + } while (response.NextToken != null); } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/HeaderResult.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/HeaderResult.cs index 5fada00a57..a6ff595de9 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/HeaderResult.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/HeaderResult.cs @@ -59,7 +59,7 @@ internal class HeaderResult /// /// The result. /// if set to true [success]. - public HeaderResult(TResult result, bool success) + public HeaderResult(TResult? result, bool success) { Success = success; Result = result; @@ -73,7 +73,7 @@ public HeaderResult(TResult result, bool success) /// HeaderResult<TNew>. public HeaderResult Map(Func> map) { - if (Success) + if (Success && Result is not null) return map(Result); return HeaderResult.Empty(); } @@ -87,7 +87,7 @@ public HeaderResult Map(Func> map) /// Gets the result. /// /// The result. - public TResult Result { get; } + public TResult? Result { get; } /// /// Empties this instance. diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ISqsMessageCreator.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ISqsMessageCreator.cs index eff39dad10..10a6982ecb 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ISqsMessageCreator.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ISqsMessageCreator.cs @@ -21,8 +21,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #endregion -using System; - namespace Paramore.Brighter.MessagingGateway.AWSSQS { internal interface ISqsMessageCreator @@ -41,11 +39,14 @@ protected HeaderResult ReadReceiptHandle(Amazon.SQS.Model.Message sqsMes return new HeaderResult(string.Empty, true); } - protected Message FailureMessage(HeaderResult topic, HeaderResult messageId) + protected Message FailureMessage(HeaderResult topic, HeaderResult messageId) { + var id = messageId.Success ? messageId.Result : string.Empty; + var routingKey = topic.Success ? topic.Result : RoutingKey.Empty; + var header = new MessageHeader( - messageId.Success ? messageId.Result : string.Empty, - topic.Success ? topic.Result : RoutingKey.Empty, + id!, + routingKey!, MessageType.MT_UNACCEPTABLE); var message = new Message(header, new MessageBody(string.Empty)); return message; diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/IValidateTopic.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/IValidateTopic.cs index 6105ba4491..3d39202a02 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/IValidateTopic.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/IValidateTopic.cs @@ -21,12 +21,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #endregion +using System.Threading; using System.Threading.Tasks; namespace Paramore.Brighter.MessagingGateway.AWSSQS { internal interface IValidateTopic { - Task<(bool, string TopicArn)> ValidateAsync(string topic); + Task<(bool, string? TopicArn)> ValidateAsync(string topic, CancellationToken cancellationToken = default); } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/Paramore.Brighter.MessagingGateway.AWSSQS.csproj b/src/Paramore.Brighter.MessagingGateway.AWSSQS/Paramore.Brighter.MessagingGateway.AWSSQS.csproj index f591d398c4..cb6d909add 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/Paramore.Brighter.MessagingGateway.AWSSQS.csproj +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/Paramore.Brighter.MessagingGateway.AWSSQS.csproj @@ -3,6 +3,7 @@ Provides an implementation of the messaging gateway for decoupled invocation in the Paramore.Brighter pipeline, using awssqs Deniz Kocak netstandard2.0;net8.0;net9.0 + enable awssqs;AMQP;Command;Event;Service Activator;Decoupled;Invocation;Messaging;Remote;Command Dispatcher;Command Processor;Request;Service;Task Queue;Work Queue;Retry;Circuit Breaker;Availability diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsAttributes.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsAttributes.cs index e48021c4a2..70c2454724 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsAttributes.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsAttributes.cs @@ -32,14 +32,14 @@ public class SnsAttributes /// The policy that defines how Amazon SNS retries failed deliveries to HTTP/S endpoints /// Ignored if TopicARN is set /// - public string DeliveryPolicy { get; set; } = null; + public string? DeliveryPolicy { get; set; } = null; /// /// The JSON serialization of the topic's access control policy. /// The policy that defines who can access your topic. By default, only the topic owner can publish or subscribe to the topic. /// Ignored if TopicARN is set /// - public string Policy { get; set; } = null; + public string? Policy { get; set; } = null; /// /// A list of resource tags to use when creating the publication diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageAttributeExtensions.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageAttributeExtensions.cs index 4433d4c449..6ef8056d9b 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageAttributeExtensions.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageAttributeExtensions.cs @@ -27,7 +27,7 @@ namespace Paramore.Brighter.MessagingGateway.AWSSQS { public static class SnsMessageAttributeExtensions { - public static string GetValueInString(this JsonElement jsonElement) + public static string? GetValueInString(this JsonElement jsonElement) { return jsonElement.TryGetProperty("Value", out var stringValue) ? stringValue.GetString() : string.Empty; } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageProducerFactory.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageProducerFactory.cs index a8646e9c08..65abc2ccd6 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageProducerFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageProducerFactory.cs @@ -22,35 +22,61 @@ THE SOFTWARE. */ #endregion using System.Collections.Generic; +using System.Threading.Tasks; +using Paramore.Brighter.Tasks; namespace Paramore.Brighter.MessagingGateway.AWSSQS { public class SnsMessageProducerFactory : IAmAMessageProducerFactory { private readonly AWSMessagingGatewayConnection _connection; - private readonly IEnumerable _snsPublications; + private readonly IEnumerable _publications; /// /// Creates a collection of SNS message producers from the SNS publication information /// /// The Connection to use to connect to AWS - /// The publications describing the SNS topics that we want to use + /// The publications describing the SNS topics that we want to use public SnsMessageProducerFactory( AWSMessagingGatewayConnection connection, - IEnumerable snsPublications) + IEnumerable publications) { _connection = connection; - _snsPublications = snsPublications; + _publications = publications; } /// - public Dictionary Create() + /// + /// Sync over async used here, alright in the context of producer creation + /// + public Dictionary Create() { var producers = new Dictionary(); - foreach (var p in _snsPublications) + foreach (var p in _publications) { + if (p.Topic is null) + throw new ConfigurationException($"Missing topic on Publication"); + var producer = new SqsMessageProducer(_connection, p); - if (producer.ConfirmTopicExistsAsync().GetAwaiter().GetResult()) + if (producer.ConfirmTopicExists()) + producers[p.Topic] = producer; + else + throw new ConfigurationException($"Missing SNS topic: {p.Topic}"); + } + + return producers; + } + + public async Task> CreateAsync() + { + var producers = new Dictionary(); + foreach (var p in _publications) + { + if (p.Topic is null) + throw new ConfigurationException($"Missing topic on Publication"); + + var producer = new SqsMessageProducer(_connection, p); + if (await producer.ConfirmTopicExistsAsync()) producers[p.Topic] = producer; else throw new ConfigurationException($"Missing SNS topic: {p.Topic}"); diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsProducerRegistryFactory.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsProducerRegistryFactory.cs index 37faad64c5..4b6cd38c75 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsProducerRegistryFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsProducerRegistryFactory.cs @@ -22,6 +22,8 @@ THE SOFTWARE. */ #endregion using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace Paramore.Brighter.MessagingGateway.AWSSQS { @@ -52,5 +54,11 @@ public IAmAProducerRegistry Create() var producerFactory = new SnsMessageProducerFactory(_connection, _snsPublications); return new ProducerRegistry(producerFactory.Create()); } + + public async Task CreateAsync(CancellationToken ct = default) + { + var producerFactory = new SnsMessageProducerFactory(_connection, _snsPublications); + return new ProducerRegistry(await producerFactory.CreateAsync()); + } } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsPublication.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsPublication.cs index 319512d061..0d9007d7a8 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsPublication.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsPublication.cs @@ -38,12 +38,12 @@ public class SnsPublication : Publication /// The attributes of the topic. If TopicARNs is set we will always assume that we do not /// need to create or validate the SNS Topic /// - public SnsAttributes SnsAttributes { get; set; } + public SnsAttributes? SnsAttributes { get; set; } /// /// If we want to use topic Arns and not topics you need to supply the Arn to use for any message that you send to us, /// as we use the topic from the header to dispatch to an Arn. /// - public string TopicArn { get; set; } + public string? TopicArn { get; set; } } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsInlineMessageCreator.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsInlineMessageCreator.cs index c591a02721..0246ca06f7 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsInlineMessageCreator.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsInlineMessageCreator.cs @@ -38,15 +38,15 @@ internal class SqsInlineMessageCreator : SqsMessageCreatorBase, ISqsMessageCreat public Message CreateMessage(Amazon.SQS.Model.Message sqsMessage) { var topic = HeaderResult.Empty(); - var messageId = HeaderResult.Empty(); - var contentType = HeaderResult.Empty(); - var correlationId = HeaderResult.Empty(); + var messageId = HeaderResult.Empty(); + var contentType = HeaderResult.Empty(); + var correlationId = HeaderResult.Empty(); var handledCount = HeaderResult.Empty(); var messageType = HeaderResult.Empty(); var timeStamp = HeaderResult.Empty(); var receiptHandle = HeaderResult.Empty(); - var replyTo = HeaderResult.Empty(); - var subject = HeaderResult.Empty(); + var replyTo = HeaderResult.Empty(); + var subject = HeaderResult.Empty(); Message message; try @@ -55,7 +55,7 @@ public Message CreateMessage(Amazon.SQS.Model.Message sqsMessage) _messageAttributes = ReadMessageAttributes(jsonDocument); topic = ReadTopic(); - messageId = ReadMessageId(); + messageId = ReadMessageId() ; contentType = ReadContentType(); correlationId = ReadCorrelationId(); handledCount = ReadHandledCount(); @@ -68,15 +68,15 @@ public Message CreateMessage(Amazon.SQS.Model.Message sqsMessage) //TODO:CLOUD_EVENTS parse from headers var messageHeader = new MessageHeader( - messageId: messageId.Result, - topic: topic.Result, + messageId: messageId.Result ?? string.Empty, + topic: topic.Result ?? RoutingKey.Empty, messageType: messageType.Result, source: null, type: "", timeStamp: timeStamp.Success ? timeStamp.Result : DateTime.UtcNow, - correlationId: correlationId.Success ? correlationId.Result : "", - replyTo: new RoutingKey(replyTo.Result), - contentType: contentType.Result, + correlationId: correlationId.Success ? correlationId.Result : string.Empty, + replyTo: replyTo.Result is not null ? new RoutingKey(replyTo.Result) : RoutingKey.Empty, + contentType: contentType.Result ?? "plain/text", handledCount: handledCount.Result, dataSchema: null, subject: subject.Result, @@ -122,17 +122,17 @@ private static Dictionary ReadMessageAttributes(JsonDocumen s_logger.LogWarning($"Failed while deserializing Sqs Message body, ex: {ex}"); } - return messageAttributes; + return messageAttributes ?? new Dictionary(); } - private HeaderResult ReadContentType() + private HeaderResult ReadContentType() { if (_messageAttributes.TryGetValue(HeaderNames.ContentType, out var contentType)) { - return new HeaderResult(contentType.GetValueInString(), true); + return new HeaderResult(contentType.GetValueInString(), true); } - return new HeaderResult(string.Empty, true); + return new HeaderResult(string.Empty, true); } private Dictionary ReadMessageBag() @@ -141,29 +141,33 @@ private Dictionary ReadMessageBag() { try { + var json = headerBag.GetValueInString(); + if (string.IsNullOrEmpty(json)) + return new Dictionary(); + var bag = JsonSerializer.Deserialize>( - headerBag.GetValueInString(), + json!, JsonSerialisationOptions.Options); - - return bag; + + return bag ?? new Dictionary(); } catch (Exception) { - + //suppress any errors in deserialization } } return new Dictionary(); } - private HeaderResult ReadReplyTo() + private HeaderResult ReadReplyTo() { if (_messageAttributes.TryGetValue(HeaderNames.ReplyTo, out var replyTo)) { - return new HeaderResult(replyTo.GetValueInString(), true); + return new HeaderResult(replyTo.GetValueInString(), true); } - return new HeaderResult(string.Empty, true); + return new HeaderResult(string.Empty, true); } private HeaderResult ReadTimestamp() @@ -205,24 +209,24 @@ private HeaderResult ReadHandledCount() return new HeaderResult(0, true); } - private HeaderResult ReadCorrelationId() + private HeaderResult ReadCorrelationId() { if (_messageAttributes.TryGetValue(HeaderNames.CorrelationId, out var correlationId)) { - return new HeaderResult(correlationId.GetValueInString(), true); + return new HeaderResult(correlationId.GetValueInString(), true); } - return new HeaderResult(string.Empty, true); + return new HeaderResult(string.Empty, true); } - private HeaderResult ReadMessageId() + private HeaderResult ReadMessageId() { if (_messageAttributes.TryGetValue(HeaderNames.Id, out var messageId)) { - return new HeaderResult(messageId.GetValueInString(), true); + return new HeaderResult(messageId.GetValueInString(), true); } - return new HeaderResult(string.Empty, true); + return new HeaderResult(string.Empty, true); } private HeaderResult ReadTopic() @@ -230,7 +234,11 @@ private HeaderResult ReadTopic() if (_messageAttributes.TryGetValue(HeaderNames.Topic, out var topicArn)) { //we have an arn, and we want the topic - var arnElements = topicArn.GetValueInString().Split(':'); + var s = topicArn.GetValueInString(); + if (string.IsNullOrEmpty(s)) + return new HeaderResult(RoutingKey.Empty, true); + + var arnElements = s!.Split(':'); var topic = arnElements[(int)ARNAmazonSNS.TopicName]; return new HeaderResult(new RoutingKey(topic), true); @@ -239,13 +247,13 @@ private HeaderResult ReadTopic() return new HeaderResult(RoutingKey.Empty, true); } - private static HeaderResult ReadMessageSubject(JsonDocument jsonDocument) + private static HeaderResult ReadMessageSubject(JsonDocument jsonDocument) { try { if (jsonDocument.RootElement.TryGetProperty("Subject", out var value)) { - return new HeaderResult(value.GetString(), true); + return new HeaderResult(value.GetString(), true); } } catch (Exception ex) @@ -253,7 +261,7 @@ private static HeaderResult ReadMessageSubject(JsonDocument jsonDocument s_logger.LogWarning($"Failed to parse Sqs Message Body to valid Json Document, ex: {ex}"); } - return new HeaderResult(null, true); + return new HeaderResult(null, true); } private static MessageBody ReadMessageBody(JsonDocument jsonDocument) diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessage.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessage.cs index d748856ff1..ca79ba0bf2 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessage.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessage.cs @@ -32,6 +32,6 @@ public class SqsMessage { public Guid MessageId { get; set; } - public string Message { get; set; } + public string? Message { get; set; } } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageConsumer.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageConsumer.cs index 48c744cd5d..dcd0d7dcd6 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageConsumer.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageConsumer.cs @@ -1,6 +1,6 @@ #region Licence /* The MIT License (MIT) -Copyright © 2022 Ian Cooper +Copyright © 2024 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 @@ -19,22 +19,25 @@ 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* +#endregion using System; using System.Collections.Generic; using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; using Amazon.SQS; using Amazon.SQS.Model; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; +using Paramore.Brighter.Tasks; namespace Paramore.Brighter.MessagingGateway.AWSSQS { /// /// Read messages from an SQS queue /// - public class SqsMessageConsumer : IAmAMessageConsumer + public class SqsMessageConsumer : IAmAMessageConsumerSync, IAmAMessageConsumerAsync { private static readonly ILogger s_logger= ApplicationLogging.CreateLogger(); @@ -50,97 +53,40 @@ public class SqsMessageConsumer : IAmAMessageConsumer /// /// The awsConnection details used to connect to the SQS queue. /// The name of the SQS Queue - /// the SNS Topic we subscribe to /// The maximum number of messages to consume per call to SQS /// Do we have a DLQ attached to this queue? /// Do we have Raw Message Delivery enabled? - public SqsMessageConsumer( - AWSMessagingGatewayConnection awsConnection, - string queueName, - RoutingKey routingKey, + public SqsMessageConsumer(AWSMessagingGatewayConnection awsConnection, + string? queueName, int batchSize = 1, bool hasDLQ = false, bool rawMessageDelivery = true) { + if (string.IsNullOrEmpty(queueName)) + throw new ConfigurationException("QueueName is mandatory"); + _clientFactory = new AWSClientFactory(awsConnection); - _queueName = queueName; + _queueName = queueName!; _batchSize = batchSize; _hasDlq = hasDLQ; _rawMessageDelivery = rawMessageDelivery; } /// - /// Receives the specified queue name. + /// Acknowledges the specified message. + /// Sync over Async /// - /// The timeout. AWS uses whole seconds. Anything greater than 0 uses long-polling. - public Message[] Receive(TimeSpan? timeOut = null) - { - AmazonSQSClient client = null; - Amazon.SQS.Model.Message[] sqsMessages; - try - { - client = _clientFactory.CreateSqsClient(); - var urlResponse = client.GetQueueUrlAsync(_queueName).GetAwaiter().GetResult(); - timeOut ??= TimeSpan.Zero; - - s_logger.LogDebug("SqsMessageConsumer: Preparing to retrieve next message from queue {URL}", - urlResponse.QueueUrl); - - var request = new ReceiveMessageRequest(urlResponse.QueueUrl) - { - MaxNumberOfMessages = _batchSize, - WaitTimeSeconds = timeOut.Value.Seconds, - MessageAttributeNames = new List {"All"}, - }; - - var receiveResponse = client.ReceiveMessageAsync(request).GetAwaiter().GetResult(); - - sqsMessages = receiveResponse.Messages.ToArray(); - } - catch (InvalidOperationException ioe) - { - s_logger.LogDebug("SqsMessageConsumer: Could not determine number of messages to retrieve"); - throw new ChannelFailureException("Error connecting to SQS, see inner exception for details", ioe); - } - catch (OperationCanceledException oce) - { - s_logger.LogDebug("SqsMessageConsumer: Could not find messages to retrieve"); - throw new ChannelFailureException("Error connecting to SQS, see inner exception for details", oce); - } - catch (Exception e) - { - s_logger.LogError(e, "SqsMessageConsumer: There was an error listening to queue {ChannelName} ", _queueName); - throw; - } - finally - { - client?.Dispose(); - } - - if (sqsMessages.Length == 0) - { - return new[] {_noopMessage}; - } - - var messages = new Message[sqsMessages.Length]; - for (int i = 0; i < sqsMessages.Length; i++) - { - var message = SqsMessageCreatorFactory.Create(_rawMessageDelivery).CreateMessage(sqsMessages[i]); - s_logger.LogInformation("SqsMessageConsumer: Received message from queue {ChannelName}, message: {1}{Request}", - _queueName, Environment.NewLine, JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)); - messages[i] = message; - } - - return messages; - } + /// The message. + public void Acknowledge(Message message) => BrighterSynchronizationHelper.Run(() => AcknowledgeAsync(message)); /// /// Acknowledges the specified message. /// /// The message. - public void Acknowledge(Message message) + /// Cancels the ackowledge operation + public async Task AcknowledgeAsync(Message message, CancellationToken cancellationToken = default(CancellationToken)) { - if (!message.Header.Bag.TryGetValue("ReceiptHandle", out object value)) + if (!message.Header.Bag.TryGetValue("ReceiptHandle", out object? value)) return; var receiptHandle = value.ToString(); @@ -148,8 +94,8 @@ public void Acknowledge(Message message) try { using var client = _clientFactory.CreateSqsClient(); - var urlResponse = client.GetQueueUrlAsync(_queueName).Result; - client.DeleteMessageAsync(new DeleteMessageRequest(urlResponse.QueueUrl, receiptHandle)).Wait(); + var urlResponse = await client.GetQueueUrlAsync(_queueName, cancellationToken); + await client.DeleteMessageAsync(new DeleteMessageRequest(urlResponse.QueueUrl, receiptHandle), cancellationToken); s_logger.LogInformation("SqsMessageConsumer: Deleted the message {Id} with receipt handle {ReceiptHandle} on the queue {URL}", message.Id, receiptHandle, urlResponse.QueueUrl); @@ -163,11 +109,19 @@ public void Acknowledge(Message message) /// /// Rejects the specified message. + /// Sync over async /// /// The message. - public void Reject(Message message) + public void Reject(Message message) => BrighterSynchronizationHelper.Run(() => RejectAsync(message)); + + /// + /// Rejects the specified message. + /// + /// The message. + /// Cancel the reject operation + public async Task RejectAsync(Message message, CancellationToken cancellationToken = default(CancellationToken)) { - if (!message.Header.Bag.TryGetValue("ReceiptHandle", out object value)) + if (!message.Header.Bag.TryGetValue("ReceiptHandle", out object? value)) return; var receiptHandle = value.ToString(); @@ -177,17 +131,20 @@ public void Reject(Message message) s_logger.LogInformation( "SqsMessageConsumer: Rejecting the message {Id} with receipt handle {ReceiptHandle} on the queue {ChannelName}", message.Id, receiptHandle, _queueName - ); + ); using var client = _clientFactory.CreateSqsClient(); - var urlResponse = client.GetQueueUrlAsync(_queueName).Result; + var urlResponse = await client.GetQueueUrlAsync(_queueName, cancellationToken); if (_hasDlq) { - client.ChangeMessageVisibilityAsync(new ChangeMessageVisibilityRequest(urlResponse.QueueUrl, receiptHandle, 0)).Wait(); + await client.ChangeMessageVisibilityAsync( + new ChangeMessageVisibilityRequest(urlResponse.QueueUrl, receiptHandle, 0), + cancellationToken + ); } else { - client.DeleteMessageAsync(urlResponse.QueueUrl, receiptHandle).Wait(); + await client.DeleteMessageAsync(urlResponse.QueueUrl, receiptHandle, cancellationToken); } } catch (Exception exception) @@ -199,16 +156,22 @@ public void Reject(Message message) /// /// Purges the specified queue name. + /// Sync over Async /// - public void Purge() + public void Purge() => BrighterSynchronizationHelper.Run(() => PurgeAsync()); + + /// + /// Purges the specified queue name. + /// + public async Task PurgeAsync(CancellationToken cancellationToken = default(CancellationToken)) { try { using var client = _clientFactory.CreateSqsClient(); s_logger.LogInformation("SqsMessageConsumer: Purging the queue {ChannelName}", _queueName); - var urlResponse = client.GetQueueUrlAsync(_queueName).Result; - client.PurgeQueueAsync(urlResponse.QueueUrl).Wait(); + var urlResponse = await client.GetQueueUrlAsync(_queueName, cancellationToken); + await client.PurgeQueueAsync(urlResponse.QueueUrl, cancellationToken); s_logger.LogInformation("SqsMessageConsumer: Purged the queue {ChannelName}", _queueName); } @@ -218,16 +181,93 @@ public void Purge() throw; } } + + /// + /// Receives the specified queue name. + /// Sync over async + /// + /// The timeout. AWS uses whole seconds. Anything greater than 0 uses long-polling. + public Message[] Receive(TimeSpan? timeOut = null) => BrighterSynchronizationHelper.Run(() => ReceiveAsync(timeOut)); + + /// + /// Receives the specified queue name. + /// + /// The timeout. AWS uses whole seconds. Anything greater than 0 uses long-polling. + /// Cancel the receive operation + public async Task ReceiveAsync(TimeSpan? timeOut = null, CancellationToken cancellationToken = default(CancellationToken)) + { + AmazonSQSClient? client = null; + Amazon.SQS.Model.Message[] sqsMessages; + try + { + client = _clientFactory.CreateSqsClient(); + var urlResponse = await client.GetQueueUrlAsync(_queueName, cancellationToken); + timeOut ??= TimeSpan.Zero; + + s_logger.LogDebug("SqsMessageConsumer: Preparing to retrieve next message from queue {URL}", urlResponse.QueueUrl); + + var request = new ReceiveMessageRequest(urlResponse.QueueUrl) + { + MaxNumberOfMessages = _batchSize, + WaitTimeSeconds = timeOut.Value.Seconds, + MessageAttributeNames = new List {"All"}, + }; + + var receiveResponse = await client.ReceiveMessageAsync(request, cancellationToken); + + sqsMessages = receiveResponse.Messages.ToArray(); + } + catch (InvalidOperationException ioe) + { + s_logger.LogDebug("SqsMessageConsumer: Could not determine number of messages to retrieve"); + throw new ChannelFailureException("Error connecting to SQS, see inner exception for details", ioe); + } + catch (OperationCanceledException oce) + { + s_logger.LogDebug("SqsMessageConsumer: Could not find messages to retrieve"); + throw new ChannelFailureException("Error connecting to SQS, see inner exception for details", oce); + } + catch (Exception e) + { + s_logger.LogError(e, "SqsMessageConsumer: There was an error listening to queue {ChannelName} ", _queueName); + throw; + } + finally + { + client?.Dispose(); + } + + if (sqsMessages.Length == 0) + { + return new[] {_noopMessage}; + } + + var messages = new Message[sqsMessages.Length]; + for (int i = 0; i < sqsMessages.Length; i++) + { + var message = SqsMessageCreatorFactory.Create(_rawMessageDelivery).CreateMessage(sqsMessages[i]); + s_logger.LogInformation("SqsMessageConsumer: Received message from queue {ChannelName}, message: {1}{Request}", + _queueName, Environment.NewLine, JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)); + messages[i] = message; + } + + return messages; + } + + + public bool Requeue(Message message, TimeSpan? delay = null) => BrighterSynchronizationHelper.Run(() => RequeueAsync(message, delay)); /// - /// Requeues the specified message. + /// Re-queues the specified message. /// /// The message. /// Time to delay delivery of the message. AWS uses seconds. 0s is immediate requeue. Default is 0ms + /// Cancels the requeue /// True if the message was requeued successfully - public bool Requeue(Message message, TimeSpan? delay = null) + public async Task RequeueAsync(Message message, TimeSpan? delay = null, + CancellationToken cancellationToken = default(CancellationToken)) { - if (!message.Header.Bag.TryGetValue("ReceiptHandle", out object value)) + if (!message.Header.Bag.TryGetValue("ReceiptHandle", out object? value)) return false; delay ??= TimeSpan.Zero; @@ -240,8 +280,11 @@ public bool Requeue(Message message, TimeSpan? delay = null) using (var client = _clientFactory.CreateSqsClient()) { - var urlResponse = client.GetQueueUrlAsync(_queueName).Result; - client.ChangeMessageVisibilityAsync(new ChangeMessageVisibilityRequest(urlResponse.QueueUrl, receiptHandle, delay.Value.Seconds)).Wait(); + var urlResponse = await client.GetQueueUrlAsync(_queueName, cancellationToken); + await client.ChangeMessageVisibilityAsync( + new ChangeMessageVisibilityRequest(urlResponse.QueueUrl, receiptHandle, delay.Value.Seconds), + cancellationToken + ); } s_logger.LogInformation("SqsMessageConsumer: re-queued the message {Id}", message.Id); @@ -255,20 +298,18 @@ public bool Requeue(Message message, TimeSpan? delay = null) } } - private string FindTopicArnByName(RoutingKey topicName) - { - using var snsClient = _clientFactory.CreateSnsClient(); - var topic = snsClient.FindTopicAsync(topicName.Value).GetAwaiter().GetResult(); - if (topic == null) - throw new BrokerUnreachableException($"Unable to find a Topic ARN for {topicName.Value}"); - return topic.TopicArn; - } - /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { + GC.SuppressFinalize(this); + } + + public ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + return new ValueTask(Task.CompletedTask); } } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageConsumerFactory.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageConsumerFactory.cs index 7d98a05042..08468f5df2 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageConsumerFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageConsumerFactory.cs @@ -42,20 +42,30 @@ public SqsMessageConsumerFactory(AWSMessagingGatewayConnection awsConnection) /// Creates a consumer for the specified queue. /// /// The queue to connect to - /// IAmAMessageConsumer. - public IAmAMessageConsumer Create(Subscription subscription) + /// IAmAMessageConsumerSync. + public IAmAMessageConsumerSync Create(Subscription subscription) { - SqsSubscription sqsSubscription = subscription as SqsSubscription; + return CreateImpl(subscription); + } + + public IAmAMessageConsumerAsync CreateAsync(Subscription subscription) + { + return CreateImpl(subscription); + } + + + private SqsMessageConsumer CreateImpl(Subscription subscription) + { + SqsSubscription? sqsSubscription = subscription as SqsSubscription; if (sqsSubscription == null) throw new ConfigurationException("We expect an SqsSubscription or SqsSubscription as a parameter"); return new SqsMessageConsumer( awsConnection: _awsConnection, - queueName:subscription.ChannelName.ToValidSQSQueueName(), - routingKey:subscription.RoutingKey, + queueName: subscription.ChannelName.ToValidSQSQueueName(), batchSize: subscription.BufferSize, hasDLQ: sqsSubscription.RedrivePolicy == null, rawMessageDelivery: sqsSubscription.RawMessageDelivery - ); + ); } } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageCreator.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageCreator.cs index 3fa51ad63a..576b51825b 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageCreator.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageCreator.cs @@ -51,7 +51,7 @@ internal class SqsMessageCreator : SqsMessageCreatorBase, ISqsMessageCreator public Message CreateMessage(Amazon.SQS.Model.Message sqsMessage) { var topic = HeaderResult.Empty(); - var messageId = HeaderResult.Empty(); + var messageId = HeaderResult.Empty(); var contentType = HeaderResult.Empty(); var correlationId = HeaderResult.Empty(); var handledCount = HeaderResult.Empty(); @@ -75,23 +75,25 @@ public Message CreateMessage(Amazon.SQS.Model.Message sqsMessage) replyTo = ReadReplyTo(sqsMessage); receiptHandle = ReadReceiptHandle(sqsMessage); + var bodyType = (contentType.Success ? contentType.Result : "plain/text"); + var messageHeader = new MessageHeader( - messageId: messageId.Result, - topic: topic.Result, + messageId: messageId.Result ?? string.Empty, + topic: topic.Result ?? RoutingKey.Empty, messageType.Result, source: null, - type: "", + type: string.Empty, timeStamp: timeStamp.Success ? timeStamp.Result : DateTime.UtcNow, - correlationId:correlationId.Success ? correlationId.Result : "", - replyTo: replyTo.Success ? new RoutingKey(replyTo.Result) : RoutingKey.Empty, - contentType: contentType.Success ? contentType.Result : "", + correlationId: correlationId.Success ? correlationId.Result : string.Empty, + replyTo: replyTo.Success ? new RoutingKey(replyTo.Result!) : RoutingKey.Empty, + contentType: bodyType!, handledCount: handledCount.Result, dataSchema: null, subject: null, delayed: TimeSpan.Zero ); - message = new Message(messageHeader, ReadMessageBody(sqsMessage, messageHeader.ContentType)); + message = new Message(messageHeader, ReadMessageBody(sqsMessage, bodyType!)); //deserialize the bag var bag = ReadMessageBag(sqsMessage); @@ -101,7 +103,7 @@ public Message CreateMessage(Amazon.SQS.Model.Message sqsMessage) } if(receiptHandle.Success) - message.Header.Bag.Add("ReceiptHandle", receiptHandle.Result); + message.Header.Bag.Add("ReceiptHandle", receiptHandle.Result!); } catch (Exception e) { @@ -109,13 +111,14 @@ public Message CreateMessage(Amazon.SQS.Model.Message sqsMessage) message = FailureMessage(topic, messageId); } - return message; } private static MessageBody ReadMessageBody(Amazon.SQS.Model.Message sqsMessage, string contentType) { - if(contentType == CompressPayloadTransformerAsync.GZIP || contentType == CompressPayloadTransformerAsync.DEFLATE || contentType == CompressPayloadTransformerAsync.BROTLI) + if(contentType == CompressPayloadTransformerAsync.GZIP + || contentType == CompressPayloadTransformerAsync.DEFLATE + || contentType == CompressPayloadTransformerAsync.BROTLI) return new MessageBody(sqsMessage.Body, contentType, CharacterEncoding.Base64); return new MessageBody(sqsMessage.Body, contentType); @@ -123,12 +126,13 @@ private static MessageBody ReadMessageBody(Amazon.SQS.Model.Message sqsMessage, private Dictionary ReadMessageBag(Amazon.SQS.Model.Message sqsMessage) { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Bag, out MessageAttributeValue value)) + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Bag, out MessageAttributeValue? value)) { try { var bag = JsonSerializer.Deserialize>(value.StringValue, JsonSerialisationOptions.Options); - return bag; + if (bag != null) + return bag; } catch (Exception) { @@ -140,7 +144,7 @@ private Dictionary ReadMessageBag(Amazon.SQS.Model.Message sqsMe private HeaderResult ReadReplyTo(Amazon.SQS.Model.Message sqsMessage) { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.ReplyTo, out MessageAttributeValue value)) + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.ReplyTo, out MessageAttributeValue? value)) { return new HeaderResult(value.StringValue, true); } @@ -149,7 +153,7 @@ private HeaderResult ReadReplyTo(Amazon.SQS.Model.Message sqsMessage) private HeaderResult ReadTimestamp(Amazon.SQS.Model.Message sqsMessage) { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Timestamp, out MessageAttributeValue value)) + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Timestamp, out MessageAttributeValue? value)) { if (DateTime.TryParse(value.StringValue, out DateTime timestamp)) { @@ -161,7 +165,7 @@ private HeaderResult ReadTimestamp(Amazon.SQS.Model.Message sqsMessage private HeaderResult ReadMessageType(Amazon.SQS.Model.Message sqsMessage) { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.MessageType, out MessageAttributeValue value)) + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.MessageType, out MessageAttributeValue? value)) { if (Enum.TryParse(value.StringValue, out MessageType messageType)) { @@ -173,7 +177,7 @@ private HeaderResult ReadMessageType(Amazon.SQS.Model.Message sqsMe private HeaderResult ReadHandledCount(Amazon.SQS.Model.Message sqsMessage) { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.HandledCount, out MessageAttributeValue value)) + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.HandledCount, out MessageAttributeValue? value)) { if (int.TryParse(value.StringValue, out int handledCount)) { @@ -185,7 +189,7 @@ private HeaderResult ReadHandledCount(Amazon.SQS.Model.Message sqsMessage) private HeaderResult ReadCorrelationid(Amazon.SQS.Model.Message sqsMessage) { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.CorrelationId, out MessageAttributeValue correlationId)) + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.CorrelationId, out MessageAttributeValue? correlationId)) { return new HeaderResult(correlationId.StringValue, true); } @@ -194,25 +198,25 @@ private HeaderResult ReadCorrelationid(Amazon.SQS.Model.Message sqsMessa private HeaderResult ReadContentType(Amazon.SQS.Model.Message sqsMessage) { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.ContentType, out MessageAttributeValue value)) + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.ContentType, out MessageAttributeValue? value)) { return new HeaderResult(value.StringValue, true); } - return new HeaderResult(String.Empty, true); + return new HeaderResult(string.Empty, true); } - private HeaderResult ReadMessageId(Amazon.SQS.Model.Message sqsMessage) + private HeaderResult ReadMessageId(Amazon.SQS.Model.Message sqsMessage) { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Id, out MessageAttributeValue value)) + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Id, out MessageAttributeValue? value)) { - return new HeaderResult(value.StringValue, true); + return new HeaderResult(value.StringValue, true); } - return new HeaderResult(string.Empty, true); + return new HeaderResult(string.Empty, true); } private HeaderResult ReadTopic(Amazon.SQS.Model.Message sqsMessage) { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Topic, out MessageAttributeValue value)) + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Topic, out MessageAttributeValue? value)) { //we have an arn, and we want the topic var arnElements = value.StringValue.Split(':'); diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageProducer.cs index 4f9a01a8e4..d5dbe58fde 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageProducer.cs @@ -23,8 +23,10 @@ THE SOFTWARE. */ using System; using System.Diagnostics; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Paramore.Brighter.Tasks; namespace Paramore.Brighter.MessagingGateway.AWSSQS { @@ -33,7 +35,6 @@ namespace Paramore.Brighter.MessagingGateway.AWSSQS /// public class SqsMessageProducer : AWSMessagingGateway, IAmAMessageProducerSync, IAmAMessageProducerAsync { - private readonly AWSMessagingGatewayConnection _connection; private readonly SnsPublication _publication; private readonly AWSClientFactory _clientFactory; @@ -45,7 +46,7 @@ public class SqsMessageProducer : AWSMessagingGateway, IAmAMessageProducerSync, /// /// The OTel Span we are writing Producer events too /// - public Activity Span { get; set; } + public Activity? Span { get; set; } /// /// Initializes a new instance of the class. /// @@ -54,7 +55,6 @@ public class SqsMessageProducer : AWSMessagingGateway, IAmAMessageProducerSync, public SqsMessageProducer(AWSMessagingGatewayConnection connection, SnsPublication publication) : base(connection) { - _connection = connection; _publication = publication; _clientFactory = new AWSClientFactory(connection); @@ -63,58 +63,79 @@ public SqsMessageProducer(AWSMessagingGatewayConnection connection, SnsPublicati } - public async Task ConfirmTopicExistsAsync(string topic = null) + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() { } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public ValueTask DisposeAsync() + { + return new ValueTask(Task.CompletedTask); + } + + public bool ConfirmTopicExists(string? topic = null) => BrighterSynchronizationHelper.Run(async () => await ConfirmTopicExistsAsync(topic)); + + public async Task ConfirmTopicExistsAsync(string? topic = null, CancellationToken cancellationToken = default) { //Only do this on first send for a topic for efficiency; won't auto-recreate when goes missing at runtime as a result - if (string.IsNullOrEmpty(ChannelTopicArn)) - { - await EnsureTopicAsync( - topic != null ? new RoutingKey(topic) : _publication.Topic, - _publication.SnsAttributes, - _publication.FindTopicBy, - _publication.MakeChannels); - } + if (!string.IsNullOrEmpty(ChannelTopicArn)) return !string.IsNullOrEmpty(ChannelTopicArn); + + RoutingKey? routingKey = null; + if (topic is null && _publication.Topic is not null) + routingKey = _publication.Topic; + else if (topic is not null) + routingKey = new RoutingKey(topic); + + if (routingKey is null) + throw new ConfigurationException("No topic specified for producer"); + + await EnsureTopicAsync( + routingKey, + _publication.FindTopicBy, + _publication.SnsAttributes, _publication.MakeChannels, cancellationToken); return !string.IsNullOrEmpty(ChannelTopicArn); } - + /// /// Sends the specified message. /// /// The message. - public async Task SendAsync(Message message) + /// Allows cancellation of the Send operation + public async Task SendAsync(Message message, CancellationToken cancellationToken = default) { s_logger.LogDebug("SQSMessageProducer: Publishing message with topic {Topic} and id {Id} and message: {Request}", message.Header.Topic, message.Id, message.Body); - await ConfirmTopicExistsAsync(message.Header.Topic); + await ConfirmTopicExistsAsync(message.Header.Topic, cancellationToken); + + if (string.IsNullOrEmpty(ChannelTopicArn)) + 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 SqsMessagePublisher(ChannelTopicArn, client); + var publisher = new SqsMessagePublisher(ChannelTopicArn!, client); var messageId = await publisher.PublishAsync(message); - if (messageId != null) - { - s_logger.LogDebug( - "SQSMessageProducer: Published message with topic {Topic}, Brighter messageId {MessageId} and SNS messageId {SNSMessageId}", - message.Header.Topic, message.Id, messageId); - return; - } - - throw new InvalidOperationException( - string.Format($"Failed to publish message with topic {message.Header.Topic} and id {message.Id} and message: {message.Body}")); + + 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( + "SQSMessageProducer: Published message with topic {Topic}, Brighter messageId {MessageId} and SNS messageId {SNSMessageId}", + message.Header.Topic, message.Id, messageId); } /// /// Sends the specified message. + /// Sync over Async /// /// The message. - public void Send(Message message) - { - SendAsync(message).Wait(); - } + public void Send(Message message) => BrighterSynchronizationHelper.Run(() => SendAsync(message)); /// - /// Sends the specified message. + /// Sends the specified message, with a delay. /// /// The message. /// The sending delay @@ -124,14 +145,18 @@ public void SendWithDelay(Message message, TimeSpan? delay= null) //TODO: Delay should set a visibility timeout Send(message); } - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// Sends the specified message, with a delay /// - public void Dispose() + /// The message + /// The sending delay + /// Cancels the send operation + /// + public async Task SendWithDelayAsync(Message message, TimeSpan? delay, CancellationToken cancellationToken = default) { - + //TODO: Delay should set the visibility timeout + await SendAsync(message, cancellationToken); } } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessagePublisher.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessagePublisher.cs index 96a6a6f742..a9f171727e 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessagePublisher.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessagePublisher.cs @@ -41,7 +41,7 @@ public SqsMessagePublisher(string topicArn, AmazonSimpleNotificationServiceClien _client = client; } - public async Task PublishAsync(Message message) + public async Task PublishAsync(Message message) { var messageString = message.Body.Value; var publishRequest = new PublishRequest(_topicArn, messageString, message.Header.Subject); @@ -63,7 +63,6 @@ public async Task PublishAsync(Message message) messageAttributes.Add(HeaderNames.Bag, new MessageAttributeValue{StringValue = Convert.ToString(bagJson), DataType = "String"}); publishRequest.MessageAttributes = messageAttributes; - var response = await _client.PublishAsync(publishRequest); if (response.HttpStatusCode == System.Net.HttpStatusCode.OK || response.HttpStatusCode == System.Net.HttpStatusCode.Created || response.HttpStatusCode == System.Net.HttpStatusCode.Accepted) { diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsSubscription.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsSubscription.cs index be40e01034..af0a30daf6 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsSubscription.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsSubscription.cs @@ -64,7 +64,7 @@ public class SqsSubscription : Subscription /// /// The JSON serialization of the queue's access control policy. /// - public string IAMPolicy { get; } + public string? IAMPolicy { get; } /// /// Indicate that the Raw Message Delivery setting is enabled or disabled @@ -74,18 +74,18 @@ public class SqsSubscription : Subscription /// /// The policy that controls when we send messages to a DLQ after too many requeue attempts /// - public RedrivePolicy RedrivePolicy { get; } + public RedrivePolicy? RedrivePolicy { get; } /// /// The attributes of the topic. If TopicARN is set we will always assume that we do not /// need to create or validate the SNS Topic /// - public SnsAttributes SnsAttributes { get; } + public SnsAttributes? SnsAttributes { get; } /// /// A list of resource tags to use when creating the queue /// - public Dictionary Tags { get; } + public Dictionary? Tags { get; } /// /// Initializes a new instance of the class. @@ -100,7 +100,7 @@ public class SqsSubscription : Subscription /// The number of times you want to requeue a message before dropping it. /// The number of milliseconds to delay the delivery of a requeue message for. /// The number of unacceptable messages to handle, before stopping reading from the channel. - /// Is this channel read asynchronously + /// Is this channel read asynchronously /// The channel factory to create channels for Consumer. /// What is the visibility timeout for the queue /// The length of time, in seconds, for which the delivery of all messages in the queue is delayed. @@ -114,33 +114,34 @@ public class SqsSubscription : Subscription /// The indication of Raw Message Delivery setting is enabled or disabled /// How long to pause when a channel is empty in milliseconds /// How long to pause when there is a channel failure in milliseconds - public SqsSubscription(Type dataType, - SubscriptionName name = null, - ChannelName channelName = null, - RoutingKey routingKey = null, + public SqsSubscription( + Type dataType, + SubscriptionName? name = null, + ChannelName? channelName = null, + RoutingKey? routingKey = null, int bufferSize = 1, int noOfPerformers = 1, TimeSpan? timeOut = null, int requeueCount = -1, TimeSpan? requeueDelay = null, int unacceptableMessageLimit = 0, - bool runAsync = false, - IAmAChannelFactory channelFactory = null, + MessagePumpType messagePumpType = MessagePumpType.Proactor, + IAmAChannelFactory? channelFactory = null, int lockTimeout = 10, int delaySeconds = 0, int messageRetentionPeriod = 345600, TopicFindBy findTopicBy = TopicFindBy.Name, - string iAmPolicy = null, - RedrivePolicy redrivePolicy = null, - SnsAttributes snsAttributes = null, - Dictionary tags = null, + string? iAmPolicy = null, + RedrivePolicy? redrivePolicy = null, + SnsAttributes? snsAttributes = null, + Dictionary? tags = null, OnMissingChannel makeChannels = OnMissingChannel.Create, bool rawMessageDelivery = true, - int emptyChannelDelay = 500, - int channelFailureDelay = 1000 + TimeSpan? emptyChannelDelay = null, + TimeSpan? channelFailureDelay = null ) : base(dataType, name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount, - requeueDelay, unacceptableMessageLimit, runAsync, channelFactory, makeChannels, emptyChannelDelay, channelFailureDelay) + requeueDelay, unacceptableMessageLimit, messagePumpType, channelFactory, makeChannels, emptyChannelDelay, channelFailureDelay) { LockTimeout = lockTimeout; DelaySeconds = delaySeconds; @@ -175,7 +176,7 @@ public class SqsSubscription : SqsSubscription where T : IRequest /// The number of times you want to requeue a message before dropping it. /// The number of milliseconds to delay the delivery of a requeue message for. /// The number of unacceptable messages to handle, before stopping reading from the channel. - /// Is this channel read asynchronously + /// Is this channel read asynchronously /// The channel factory to create channels for Consumer. /// What is the visibility timeout for the queue /// The length of time, in seconds, for which the delivery of all messages in the queue is delayed. @@ -189,32 +190,33 @@ public class SqsSubscription : SqsSubscription where T : IRequest /// The indication of Raw Message Delivery setting is enabled or disabled /// How long to pause when a channel is empty in milliseconds /// How long to pause when there is a channel failure in milliseconds - public SqsSubscription(SubscriptionName name = null, - ChannelName channelName = null, - RoutingKey routingKey = null, + public SqsSubscription( + SubscriptionName? name = null, + ChannelName? channelName = null, + RoutingKey? routingKey = null, int bufferSize = 1, int noOfPerformers = 1, TimeSpan? timeOut = null, int requeueCount = -1, TimeSpan? requeueDelay = null, int unacceptableMessageLimit = 0, - bool runAsync = false, - IAmAChannelFactory channelFactory = null, + MessagePumpType messagePumpType = MessagePumpType.Proactor, + IAmAChannelFactory? channelFactory = null, int lockTimeout = 10, int delaySeconds = 0, int messageRetentionPeriod = 345600, TopicFindBy findTopicBy = TopicFindBy.Name, - string iAmPolicy = null, - RedrivePolicy redrivePolicy = null, - SnsAttributes snsAttributes = null, - Dictionary tags = null, + string? iAmPolicy = null, + RedrivePolicy? redrivePolicy = null, + SnsAttributes? snsAttributes = null, + Dictionary? tags = null, OnMissingChannel makeChannels = OnMissingChannel.Create, bool rawMessageDelivery = true, - int emptyChannelDelay = 500, - int channelFailureDelay = 1000 + TimeSpan? emptyChannelDelay = null, + TimeSpan? channelFailureDelay = null ) : base(typeof(T), name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount, requeueDelay, - unacceptableMessageLimit, runAsync, channelFactory, lockTimeout, delaySeconds, messageRetentionPeriod,findTopicBy, + unacceptableMessageLimit, messagePumpType, channelFactory, lockTimeout, delaySeconds, messageRetentionPeriod,findTopicBy, iAmPolicy,redrivePolicy, snsAttributes, tags, makeChannels, rawMessageDelivery, emptyChannelDelay, channelFailureDelay) { } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByArn.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByArn.cs index f8f7081a99..d5ff5ccd48 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByArn.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByArn.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ using System; using System.Net; +using System.Threading; using System.Threading.Tasks; using Amazon; using Amazon.Runtime; @@ -31,34 +32,45 @@ THE SOFTWARE. */ namespace Paramore.Brighter.MessagingGateway.AWSSQS { - public class ValidateTopicByArn : IDisposable, IValidateTopic + public class ValidateTopicByArn : IDisposable, IValidateTopic { private AmazonSimpleNotificationServiceClient _snsClient; - public ValidateTopicByArn(AWSCredentials credentials, RegionEndpoint region, Action clientConfigAction = null) + /// + /// Initializes a new instance of the class. + /// + /// The AWS credentials. + /// The AWS region. + /// An optional action to configure the client. + public ValidateTopicByArn(AWSCredentials credentials, RegionEndpoint region, Action? clientConfigAction = null) { var clientFactory = new AWSClientFactory(credentials, region, clientConfigAction); _snsClient = clientFactory.CreateSnsClient(); } + /// + /// Initializes a new instance of the class. + /// + /// The SNS client. public ValidateTopicByArn(AmazonSimpleNotificationServiceClient snsClient) { _snsClient = snsClient; } - public virtual async Task<(bool, string TopicArn)> ValidateAsync(string topicArn) + /// + /// Validates the specified topic ARN asynchronously. + /// + /// The ARN of the topic to validate. + /// A cancellation token to observe while waiting for the task to complete. + /// A tuple indicating whether the topic is valid and its ARN. + public virtual async Task<(bool, string? TopicArn)> ValidateAsync(string topicArn, CancellationToken cancellationToken = default) { - //List topics does not work across accounts - GetTopicAttributesRequest works within the region - //List Topics is rate limited to 30 ListTopic transactions per second, and can be rate limited - //So where we can, we validate a topic using GetTopicAttributesRequest - bool exists = false; try { var topicAttributes = await _snsClient.GetTopicAttributesAsync( - new GetTopicAttributesRequest(topicArn) - ); + new GetTopicAttributesRequest(topicArn), cancellationToken); exists = ((topicAttributes.HttpStatusCode == HttpStatusCode.OK) && (topicAttributes.Attributes["TopicArn"] == topicArn)); } @@ -78,6 +90,9 @@ public ValidateTopicByArn(AmazonSimpleNotificationServiceClient snsClient) return (exists, topicArn); } + /// + /// Disposes the SNS client. + /// public void Dispose() { _snsClient?.Dispose(); diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByArnConvention.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByArnConvention.cs index 698d3a412c..6c5e86a35a 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByArnConvention.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByArnConvention.cs @@ -1,6 +1,6 @@ #region Licence /* The MIT License (MIT) -Copyright © 2022 Ian Cooper +Copyright © 2024 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 @@ -23,6 +23,7 @@ THE SOFTWARE. */ using System; using System.Net; +using System.Threading; using System.Threading.Tasks; using Amazon; using Amazon.Runtime; @@ -31,12 +32,21 @@ THE SOFTWARE. */ namespace Paramore.Brighter.MessagingGateway.AWSSQS { + /// + /// The class is responsible for validating an AWS SNS topic by its ARN convention. + /// public class ValidateTopicByArnConvention : ValidateTopicByArn, IValidateTopic { private readonly RegionEndpoint _region; - private AmazonSecurityTokenServiceClient _stsClient; + private readonly AmazonSecurityTokenServiceClient _stsClient; - public ValidateTopicByArnConvention(AWSCredentials credentials, RegionEndpoint region, Action clientConfigAction = null) + /// + /// Initializes a new instance of the class. + /// + /// The AWS credentials. + /// The AWS region. + /// An optional action to configure the client. + public ValidateTopicByArnConvention(AWSCredentials credentials, RegionEndpoint region, Action? clientConfigAction = null) : base(credentials, region, clientConfigAction) { _region = region; @@ -45,20 +55,31 @@ public ValidateTopicByArnConvention(AWSCredentials credentials, RegionEndpoint r _stsClient = clientFactory.CreateStsClient(); } - public override async Task<(bool, string TopicArn)> ValidateAsync(string topic) + /// + /// Validates the specified topic asynchronously. + /// + /// The topic to validate. + /// Cancel the validation + /// A tuple indicating whether the topic is valid and its ARN. + public override async Task<(bool, string? TopicArn)> ValidateAsync(string topic, CancellationToken cancellationToken = default) { - var topicArn = GetArnFromTopic(topic); + var topicArn = await GetArnFromTopic(topic); return await base.ValidateAsync(topicArn); } - private string GetArnFromTopic(string topicName) + /// + /// Gets the ARN from the topic name. + /// + /// The name of the topic. + /// The ARN of the topic. + /// Thrown when the AWS account identity cannot be found. + private async Task GetArnFromTopic(string topicName) { - var callerIdentityResponse = _stsClient.GetCallerIdentityAsync( - new GetCallerIdentityRequest() - ) - .GetAwaiter().GetResult(); + var callerIdentityResponse = await _stsClient.GetCallerIdentityAsync( + new GetCallerIdentityRequest() + ); - if (callerIdentityResponse.HttpStatusCode != HttpStatusCode.OK) throw new InvalidOperationException("Could not find identity of AWS account"); + if (callerIdentityResponse.HttpStatusCode != HttpStatusCode.OK) throw new InvalidOperationException("Could not find identity of AWS account"); return new Arn { diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByName.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByName.cs index 74c2b40b58..f7f1b4b892 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByName.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByName.cs @@ -22,6 +22,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Threading; using System.Threading.Tasks; using Amazon; using Amazon.Runtime; @@ -29,25 +30,46 @@ THE SOFTWARE. */ namespace Paramore.Brighter.MessagingGateway.AWSSQS { + /// + /// The class is responsible for validating an AWS SNS topic by its name. + /// internal class ValidateTopicByName : IValidateTopic { private readonly AmazonSimpleNotificationServiceClient _snsClient; - public ValidateTopicByName(AWSCredentials credentials, RegionEndpoint region, Action clientConfigAction = null) + /// + /// Initializes a new instance of the class. + /// + /// The AWS credentials. + /// The AWS region. + /// An optional action to configure the client. + public ValidateTopicByName(AWSCredentials credentials, RegionEndpoint region, Action? clientConfigAction = null) { var clientFactory = new AWSClientFactory(credentials, region, clientConfigAction); _snsClient = clientFactory.CreateSnsClient(); } - + + /// + /// Initializes a new instance of the class. + /// + /// The SNS client. public ValidateTopicByName(AmazonSimpleNotificationServiceClient snsClient) { _snsClient = snsClient; } - - //Note that we assume here that topic names are globally unique, if not provide the topic ARN directly in the SNSAttributes of the subscription - //This approach can have be rate throttled at scale. AWS limits to 30 ListTopics calls per second, so it you have a lot of clients starting - //you may run into issues - public async Task<(bool, string TopicArn)> ValidateAsync(string topicName) + + /// + /// Validates the specified topic name asynchronously. + /// + /// The name of the topic to validate. + /// A cancellation token to observe while waiting for the task to complete. + /// A tuple indicating whether the topic is valid and its ARN. + /// + /// Note that we assume here that topic names are globally unique, if not provide the topic ARN directly in the SNSAttributes of the subscription. + /// This approach can be rate throttled at scale. AWS limits to 30 ListTopics calls per second, so if you have a lot of clients starting, + /// you may run into issues. + /// + public async Task<(bool, string? TopicArn)> ValidateAsync(string topicName, CancellationToken cancellationToken = default) { var topic = await _snsClient.FindTopicAsync(topicName); return (topic != null, topic?.TopicArn); diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusChannelFactory.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusChannelFactory.cs index 3ae794d1d9..0bcd887c24 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusChannelFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusChannelFactory.cs @@ -1,49 +1,140 @@ using System; +using System.Threading; +using System.Threading.Tasks; -namespace Paramore.Brighter.MessagingGateway.AzureServiceBus +#region Licence +/* The MIT License (MIT) +Copyright © 2022 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 + +namespace Paramore.Brighter.MessagingGateway.AzureServiceBus; + +/// +/// Creates instances of channels using Azure Service Bus. +/// +public class AzureServiceBusChannelFactory : IAmAChannelFactory { + private readonly AzureServiceBusConsumerFactory _azureServiceBusConsumerFactory; + /// - /// Creates instances of channels using Azure Service Bus. + /// Initializes an Instance of /// - public class AzureServiceBusChannelFactory : IAmAChannelFactory + /// An Azure Service Bus Consumer Factory. + public AzureServiceBusChannelFactory(AzureServiceBusConsumerFactory azureServiceBusConsumerFactory) { - private readonly AzureServiceBusConsumerFactory _azureServiceBusConsumerFactory; + _azureServiceBusConsumerFactory = azureServiceBusConsumerFactory; + } - /// - /// Initializes an Instance of - /// - /// An Azure Service Bus Consumer Factory. - public AzureServiceBusChannelFactory(AzureServiceBusConsumerFactory azureServiceBusConsumerFactory) + /// + /// Creates the input channel. + /// + /// The parameters with which to create the channel for the transport + /// An instance of . + /// Thrown when the subscription is incorrect + public IAmAChannelSync CreateSyncChannel(Subscription subscription) + { + if (!(subscription is AzureServiceBusSubscription azureServiceBusSubscription)) { - _azureServiceBusConsumerFactory = azureServiceBusConsumerFactory; + throw new ConfigurationException( + "We expect an AzureServiceBusSubscription or AzureServiceBusSubscription as a parameter"); } - /// - /// Creates the input channel. - /// - /// The parameters with which to create the channel for the transport - /// IAmAnInputChannel. - public IAmAChannel CreateChannel(Subscription subscription) + if (subscription.TimeOut < TimeSpan.FromMilliseconds(400)) { - if (!(subscription is AzureServiceBusSubscription azureServiceBusSubscription)) - { - throw new ConfigurationException("We expect an AzureServiceBusSubscription or AzureServiceBusSubscription as a parameter"); - } - - if (subscription.TimeOut < TimeSpan.FromMilliseconds(400)) - { - throw new ArgumentException("The minimum allowed timeout is 400 milliseconds"); - } - - IAmAMessageConsumer messageConsumer = - _azureServiceBusConsumerFactory.Create(azureServiceBusSubscription); - - return new Channel( - channelName: subscription.ChannelName, - routingKey: subscription.RoutingKey, - messageConsumer: messageConsumer, - maxQueueLength: subscription.BufferSize - ); + throw new ArgumentException("The minimum allowed timeout is 400 milliseconds"); } + + IAmAMessageConsumerSync messageConsumer = + _azureServiceBusConsumerFactory.Create(azureServiceBusSubscription); + + return new Channel( + channelName: subscription.ChannelName, + routingKey: subscription.RoutingKey, + messageConsumer: messageConsumer, + maxQueueLength: subscription.BufferSize + ); + } + + /// + /// Creates the input channel. + /// + /// The parameters with which to create the channel for the transport + /// An instance of . + /// Thrown when the subscription is incorrect + public IAmAChannelAsync CreateAsyncChannel(Subscription subscription) + { + if (!(subscription is AzureServiceBusSubscription azureServiceBusSubscription)) + { + throw new ConfigurationException( + "We expect an AzureServiceBusSubscription or AzureServiceBusSubscription as a parameter"); + } + + if (subscription.TimeOut < TimeSpan.FromMilliseconds(400)) + { + throw new ArgumentException("The minimum allowed timeout is 400 milliseconds"); + } + + IAmAMessageConsumerAsync messageConsumer = + _azureServiceBusConsumerFactory.CreateAsync(azureServiceBusSubscription); + + return new ChannelAsync( + channelName: subscription.ChannelName, + routingKey: subscription.RoutingKey, + messageConsumer: messageConsumer, + maxQueueLength: subscription.BufferSize + ); + } + + /// + /// Creates the input channel. + /// + /// An SqsSubscription, the subscription parameter to create the channel with. + /// Cancel the ongoing operation + /// An instance of . + /// Thrown when the subscription is incorrect + public Task CreateAsyncChannelAsync(Subscription subscription, + CancellationToken ct = default) + { + if (!(subscription is AzureServiceBusSubscription azureServiceBusSubscription)) + { + throw new ConfigurationException( + "We expect an AzureServiceBusSubscription or AzureServiceBusSubscription as a parameter"); + } + + if (subscription.TimeOut < TimeSpan.FromMilliseconds(400)) + { + throw new ArgumentException("The minimum allowed timeout is 400 milliseconds"); + } + + IAmAMessageConsumerAsync messageConsumer = + _azureServiceBusConsumerFactory.CreateAsync(azureServiceBusSubscription); + + IAmAChannelAsync channel = new ChannelAsync( + channelName: subscription.ChannelName, + routingKey: subscription.RoutingKey, + messageConsumer: messageConsumer, + maxQueueLength: subscription.BufferSize + ); + + return Task.FromResult(channel); } } diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusConfiguration.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusConfiguration.cs index f45e067fc2..e0d16f19b1 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusConfiguration.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusConfiguration.cs @@ -1,36 +1,59 @@ -namespace Paramore.Brighter.MessagingGateway.AzureServiceBus +#region Licence +/* The MIT License (MIT) +Copyright © 2022 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 + +namespace Paramore.Brighter.MessagingGateway.AzureServiceBus; + +/// +/// Configuration Options for the Azure Service Bus Messaging Transport. +/// +public class AzureServiceBusConfiguration { /// - /// Configuration Options for the Azure Service Bus Messaging Transport. + /// Initializes an Instance of /// - public class AzureServiceBusConfiguration + /// The Connection String to connect to Azure Service Bus. + /// If True Messages and Read a Deleted, if False Messages are Peeked and Locked. + /// When sending more than one message using the MessageProducer, the max amount to send in a single transmission. + public AzureServiceBusConfiguration(string connectionString, bool ackOnRead = false, int bulkSendBatchSize = 10 ) { - /// - /// Initializes an Instance of - /// - /// The Connection String to connect to Azure Service Bus. - /// If True Messages and Read a Deleted, if False Messages are Peeked and Locked. - /// When sending more than one message using the MessageProducer, the max amount to send in a single transmission. - public AzureServiceBusConfiguration(string connectionString, bool ackOnRead = false, int bulkSendBatchSize = 10 ) - { - ConnectionString = connectionString; - AckOnRead = ackOnRead; - BulkSendBatchSize = bulkSendBatchSize; - } - - /// - /// The Connection String to connect to Azure Service Bus. - /// - public string ConnectionString { get; } - - /// - /// When set to true this will set the Channel to Read and Delete, when False Peek and Lock - /// - public bool AckOnRead{ get; } - - /// - /// When sending more than one message using the MessageProducer, the max amount to send in a single transmission. - /// - public int BulkSendBatchSize { get; } + ConnectionString = connectionString; + AckOnRead = ackOnRead; + BulkSendBatchSize = bulkSendBatchSize; } + + /// + /// The Connection String to connect to Azure Service Bus. + /// + public string ConnectionString { get; } + + /// + /// When set to true this will set the Channel to Read and Delete, when False Peek and Lock + /// + public bool AckOnRead{ get; } + + /// + /// When sending more than one message using the MessageProducer, the max amount to send in a single transmission. + /// + public int BulkSendBatchSize { get; } } diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusConsumer.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusConsumer.cs index 56d162fae9..86cba4d7c2 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusConsumer.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusConsumer.cs @@ -1,320 +1,410 @@ -using System; +#region Licence +/* The MIT License (MIT) +Copyright © 2022 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; +using System.Threading.Tasks; using Azure.Messaging.ServiceBus; using Microsoft.Extensions.Logging; using Paramore.Brighter.MessagingGateway.AzureServiceBus.AzureServiceBusWrappers; +using Paramore.Brighter.Tasks; -namespace Paramore.Brighter.MessagingGateway.AzureServiceBus +namespace Paramore.Brighter.MessagingGateway.AzureServiceBus; + +/// +/// Implementation of using Azure Service Bus for Transport. +/// +public abstract class AzureServiceBusConsumer : IAmAMessageConsumerSync, IAmAMessageConsumerAsync { + protected abstract string SubscriptionName { get; } + protected abstract ILogger Logger { get; } + + protected readonly AzureServiceBusSubscription Subscription; + protected readonly string Topic; + private readonly IAmAMessageProducer _messageProducer; + protected readonly IAdministrationClientWrapper AdministrationClientWrapper; + private readonly int _batchSize; + protected IServiceBusReceiverWrapper? ServiceBusReceiver; + protected readonly AzureServiceBusSubscriptionConfiguration SubscriptionConfiguration; + /// - /// Implementation of using Azure Service Bus for Transport. + /// Constructor for the Azure Service Bus Consumer /// - public abstract class AzureServiceBusConsumer : IAmAMessageConsumer + /// The ASB subscription details + /// The producer we want to send via + /// The admin client for ASB + /// Whether the consumer is async + protected AzureServiceBusConsumer( + AzureServiceBusSubscription subscription, + IAmAMessageProducer messageProducer, + IAdministrationClientWrapper administrationClientWrapper, + bool isAsync = false + ) { - protected abstract string SubscriptionName { get; } - protected abstract ILogger Logger { get; } - - protected readonly AzureServiceBusSubscription Subscription; - protected readonly string Topic; - private readonly IAmAMessageProducerSync _messageProducerSync; - protected readonly IAdministrationClientWrapper AdministrationClientWrapper; - private readonly int _batchSize; - protected IServiceBusReceiverWrapper? ServiceBusReceiver; - protected readonly AzureServiceBusSubscriptionConfiguration SubscriptionConfiguration; + Subscription = subscription; + Topic = subscription.RoutingKey; + _batchSize = subscription.BufferSize; + SubscriptionConfiguration = subscription.Configuration ?? new AzureServiceBusSubscriptionConfiguration(); + _messageProducer = messageProducer; + AdministrationClientWrapper = administrationClientWrapper; + } - protected AzureServiceBusConsumer(AzureServiceBusSubscription subscription, IAmAMessageProducerSync messageProducerSync, - IAdministrationClientWrapper administrationClientWrapper) + /// + /// Dispose of the Consumer. + /// + public void Dispose() + { + ServiceBusReceiver?.Close(); + GC.SuppressFinalize(this); + } + + public async ValueTask DisposeAsync() + { + if (ServiceBusReceiver is not null) await ServiceBusReceiver.CloseAsync(); + GC.SuppressFinalize(this); + } + + /// + /// Acknowledges the specified message. + /// + /// The message. + public void Acknowledge(Message message) => BrighterSynchronizationHelper.Run(async() => await AcknowledgeAsync(message)); + + /// + /// Acknowledges the specified message. + /// + /// The message. + /// Cancels the acknowledge operation + public async Task AcknowledgeAsync(Message message, CancellationToken cancellationToken = default(CancellationToken)) + { + try { - Subscription = subscription; - Topic = subscription.RoutingKey; - _batchSize = subscription.BufferSize; - SubscriptionConfiguration = subscription.Configuration ?? new AzureServiceBusSubscriptionConfiguration(); - _messageProducerSync = messageProducerSync; - AdministrationClientWrapper = administrationClientWrapper; - } + await EnsureChannelAsync(); + var lockToken = message.Header.Bag[ASBConstants.LockTokenHeaderBagKey].ToString(); - /// - /// Receives the specified queue name. - /// An abstraction over a third-party messaging library. Used to read messages from the broker and to acknowledge - /// the processing of those messages or requeue them. - /// Used by a to provide access to a third-party message queue. - /// - /// The timeout for a message being available. Defaults to 300ms. - /// Message. - public Message[] Receive(TimeSpan? timeOut = null) + if (string.IsNullOrEmpty(lockToken)) + throw new Exception($"LockToken for message with id {message.Id} is null or empty"); + Logger.LogDebug("Acknowledging Message with Id {Id} Lock Token : {LockToken}", message.Id, + lockToken); + + if(ServiceBusReceiver == null) + await GetMessageReceiverProviderAsync(); + + await ServiceBusReceiver!.CompleteAsync(lockToken); + + if (SubscriptionConfiguration.RequireSession) + if (ServiceBusReceiver is not null) await ServiceBusReceiver.CloseAsync(); + } + catch (AggregateException ex) + { + if (ex.InnerException is ServiceBusException asbException) + HandleAsbException(asbException, message.Id); + else + { + Logger.LogError(ex, "Error completing peak lock on message with id {Id}", message.Id); + throw; + } + } + catch (ServiceBusException ex) { - Logger.LogDebug( - "Preparing to retrieve next message(s) from topic {Topic} via subscription {ChannelName} with timeout {Timeout} and batch size {BatchSize}", - Topic, SubscriptionName, timeOut, _batchSize); + HandleAsbException(ex, message.Id); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error completing peak lock on message with id {Id}", message.Id); + throw; + } + } + + /// + /// Purges the specified queue name. + /// + public abstract void Purge(); + + /// + /// Purges the specified queue name. + /// + public abstract Task PurgeAsync(CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Receives the specified queue name. + /// An abstraction over a third-party messaging library. Used to read messages from the broker and to acknowledge + /// the processing of those messages or requeue them. + /// Used by a to provide access to a third-party message queue. + /// Sync over async + /// + /// The timeout for a message being available. Defaults to 300ms. + /// Message. + public Message[] Receive(TimeSpan? timeOut = null) => BrighterSynchronizationHelper.Run(() => ReceiveAsync(timeOut)); + + /// + /// Receives the specified queue name. + /// An abstraction over a third-party messaging library. Used to read messages from the broker and to acknowledge + /// the processing of those messages or requeue them. + /// Used by a to provide access to a third-party message queue. + /// + /// The timeout for a message being available. Defaults to 300ms. + /// Cancel the receive + /// Message. + public async Task ReceiveAsync(TimeSpan? timeOut = null, CancellationToken cancellationToken = default(CancellationToken)) + { + Logger.LogDebug( + "Preparing to retrieve next message(s) from topic {Topic} via subscription {ChannelName} with timeout {Timeout} and batch size {BatchSize}", + Topic, SubscriptionName, timeOut, _batchSize); - IEnumerable messages; - EnsureChannel(); + IEnumerable messages; + await EnsureChannelAsync(); - var messagesToReturn = new List(); + var messagesToReturn = new List(); - try + try + { + if (SubscriptionConfiguration.RequireSession || ServiceBusReceiver == null) { - if (SubscriptionConfiguration.RequireSession || ServiceBusReceiver == null) + await GetMessageReceiverProviderAsync(); + if (ServiceBusReceiver == null) { - GetMessageReceiverProvider(); - if (ServiceBusReceiver == null) - { - Logger.LogInformation("Message Gateway: Could not get a lock on a session for {TopicName}", - Topic); - return messagesToReturn.ToArray(); - } + Logger.LogInformation("Message Gateway: Could not get a lock on a session for {TopicName}", Topic); + return messagesToReturn.ToArray(); } - - timeOut ??= TimeSpan.FromMilliseconds(300); - - messages = ServiceBusReceiver.Receive(_batchSize, timeOut.Value) - .GetAwaiter().GetResult(); } - catch (Exception e) - { - if (ServiceBusReceiver is {IsClosedOrClosing: true} && !SubscriptionConfiguration.RequireSession) - { - Logger.LogDebug("Message Receiver is closing..."); - var message = new Message( - new MessageHeader(string.Empty, new RoutingKey(Topic), MessageType.MT_QUIT), - new MessageBody(string.Empty)); - messagesToReturn.Add(message); - return messagesToReturn.ToArray(); - } - - Logger.LogError(e, "Failing to receive messages"); - - //The connection to Azure Service bus may have failed so we re-establish the connection. - if(!SubscriptionConfiguration.RequireSession || ServiceBusReceiver == null) - GetMessageReceiverProvider(); - throw new ChannelFailureException("Failing to receive messages.", e); - } + timeOut ??= TimeSpan.FromMilliseconds(300); - foreach (IBrokeredMessageWrapper azureServiceBusMessage in messages) + messages = await ServiceBusReceiver.ReceiveAsync(_batchSize, timeOut.Value); + } + catch (Exception e) + { + if (ServiceBusReceiver is {IsClosedOrClosing: true} && !SubscriptionConfiguration.RequireSession) { - Message message = MapToBrighterMessage(azureServiceBusMessage); + Logger.LogDebug("Message Receiver is closing..."); + var message = new Message( + new MessageHeader(string.Empty, new RoutingKey(Topic), MessageType.MT_QUIT), + new MessageBody(string.Empty)); messagesToReturn.Add(message); + return messagesToReturn.ToArray(); } - return messagesToReturn.ToArray(); - } + Logger.LogError(e, "Failing to receive messages"); - /// - /// Requeues the specified message. - /// - /// - /// Delay to the delivery of the message. 0 is no delay. Defaults to 0. - /// True if the message should be acked, false otherwise - public bool Requeue(Message message, TimeSpan? delay = null) - { - var topic = message.Header.Topic; - delay ??= TimeSpan.Zero; + //The connection to Azure Service bus may have failed so we re-establish the connection. + if(!SubscriptionConfiguration.RequireSession || ServiceBusReceiver == null) + await GetMessageReceiverProviderAsync(); - Logger.LogInformation("Requeuing message with topic {Topic} and id {Id}", topic, message.Id); - - if (delay.Value > TimeSpan.Zero) - { - _messageProducerSync.SendWithDelay(message, delay.Value); - } - else - { - _messageProducerSync.Send(message); - } - Acknowledge(message); + throw new ChannelFailureException("Failing to receive messages.", e); + } - return true; + foreach (IBrokeredMessageWrapper azureServiceBusMessage in messages) + { + Message message = MapToBrighterMessage(azureServiceBusMessage); + messagesToReturn.Add(message); } - /// - /// Acknowledges the specified message. - /// - /// The message. - public void Acknowledge(Message message) + return messagesToReturn.ToArray(); + } + + /// + /// Rejects the specified message. + /// Sync over Async + /// + /// The message. + public void Reject(Message message) => BrighterSynchronizationHelper.Run(() => RejectAsync(message)); + + /// + /// Rejects the specified message. + /// + /// The message. + /// Cancel the rejection + public async Task RejectAsync(Message message, CancellationToken cancellationToken = default(CancellationToken)) + { + try { - try - { - EnsureChannel(); - var lockToken = message.Header.Bag[ASBConstants.LockTokenHeaderBagKey].ToString(); + await EnsureChannelAsync(); + var lockToken = message.Header.Bag[ASBConstants.LockTokenHeaderBagKey].ToString(); - if (string.IsNullOrEmpty(lockToken)) - throw new Exception($"LockToken for message with id {message.Id} is null or empty"); - Logger.LogDebug("Acknowledging Message with Id {Id} Lock Token : {LockToken}", message.Id, - lockToken); - - if(ServiceBusReceiver == null) - GetMessageReceiverProvider(); + if (string.IsNullOrEmpty(lockToken)) + throw new Exception($"LockToken for message with id {message.Id} is null or empty"); + Logger.LogDebug("Dead Lettering Message with Id {Id} Lock Token : {LockToken}", message.Id, lockToken); - ServiceBusReceiver?.Complete(lockToken).Wait(); - if (SubscriptionConfiguration.RequireSession) - ServiceBusReceiver?.Close(); - } - catch (AggregateException ex) - { - if (ex.InnerException is ServiceBusException asbException) - HandleAsbException(asbException, message.Id); - else - { - Logger.LogError(ex, "Error completing peak lock on message with id {Id}", message.Id); - throw; - } - } - catch (ServiceBusException ex) - { - HandleAsbException(ex, message.Id); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error completing peak lock on message with id {Id}", message.Id); - throw; - } - } + if(ServiceBusReceiver == null) + await GetMessageReceiverProviderAsync(); - /// - /// Rejects the specified message. - /// - /// The message. - public void Reject(Message message) + await ServiceBusReceiver!.DeadLetterAsync(lockToken); + if (SubscriptionConfiguration.RequireSession) + if (ServiceBusReceiver is not null) await ServiceBusReceiver.CloseAsync(); + } + catch (Exception ex) { - try - { - EnsureChannel(); - var lockToken = message.Header.Bag[ASBConstants.LockTokenHeaderBagKey].ToString(); + Logger.LogError(ex, "Error Dead Lettering message with id {Id}", message.Id); + throw; + } + } - if (string.IsNullOrEmpty(lockToken)) - throw new Exception($"LockToken for message with id {message.Id} is null or empty"); - Logger.LogDebug("Dead Lettering Message with Id {Id} Lock Token : {LockToken}", message.Id, lockToken); + /// + /// Requeues the specified message. + /// + /// + /// Delay to the delivery of the message. 0 is no delay. Defaults to 0. + /// True if the message should be acked, false otherwise + public bool Requeue(Message message, TimeSpan? delay = null) => BrighterSynchronizationHelper.Run(() => RequeueAsync(message, delay)); - if(ServiceBusReceiver == null) - GetMessageReceiverProvider(); - - ServiceBusReceiver?.DeadLetter(lockToken).Wait(); - if (SubscriptionConfiguration.RequireSession) - ServiceBusReceiver?.Close(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error Dead Lettering message with id {Id}", message.Id); - throw; - } - } + /// + /// Requeues the specified message. + /// + /// + /// Delay to the delivery of the message. 0 is no delay. Defaults to 0. + /// Cancel the requeue ioperation + /// True if the message should be acked, false otherwise + public async Task RequeueAsync(Message message, TimeSpan? delay = null, CancellationToken cancellationToken = default(CancellationToken)) + { + var topic = message.Header.Topic; + delay ??= TimeSpan.Zero; - /// - /// Purges the specified queue name. - /// - public abstract void Purge(); + Logger.LogInformation("Requeuing message with topic {Topic} and id {Id}", topic, message.Id); - /// - /// Dispose of the Consumer. - /// - public void Dispose() + var messageProducerAsync = _messageProducer as IAmAMessageProducerAsync; + + if (messageProducerAsync is null) + { + throw new ChannelFailureException("Message Producer is not of type IAmAMessageProducerSync"); + } + + if (delay.Value > TimeSpan.Zero) + { + await messageProducerAsync.SendWithDelayAsync(message, delay.Value, cancellationToken); + } + else { - Logger.LogInformation("Disposing the consumer..."); - ServiceBusReceiver?.Close(); - Logger.LogInformation("Consumer disposed"); + await messageProducerAsync.SendAsync(message, cancellationToken); } + + await AcknowledgeAsync(message, cancellationToken); - protected abstract void GetMessageReceiverProvider(); + return true; + } - private Message MapToBrighterMessage(IBrokeredMessageWrapper azureServiceBusMessage) + protected abstract Task GetMessageReceiverProviderAsync(); + + private Message MapToBrighterMessage(IBrokeredMessageWrapper azureServiceBusMessage) + { + if (azureServiceBusMessage.MessageBodyValue == null) { - if (azureServiceBusMessage.MessageBodyValue == null) - { - Logger.LogWarning( - "Null message body received from topic {Topic} via subscription {ChannelName}", - Topic, SubscriptionName); - } + Logger.LogWarning( + "Null message body received from topic {Topic} via subscription {ChannelName}", + Topic, SubscriptionName); + } - var messageBody = System.Text.Encoding.Default.GetString(azureServiceBusMessage.MessageBodyValue ?? Array.Empty()); - - Logger.LogDebug("Received message from topic {Topic} via subscription {ChannelName} with body {Request}", - Topic, SubscriptionName, messageBody); + var messageBody = System.Text.Encoding.Default.GetString(azureServiceBusMessage.MessageBodyValue ?? Array.Empty()); - MessageType messageType = GetMessageType(azureServiceBusMessage); - var replyAddress = GetReplyAddress(azureServiceBusMessage); - var handledCount = GetHandledCount(azureServiceBusMessage); + Logger.LogDebug("Received message from topic {Topic} via subscription {ChannelName} with body {Request}", + Topic, SubscriptionName, messageBody); - //TODO:CLOUD_EVENTS parse from headers + MessageType messageType = GetMessageType(azureServiceBusMessage); + var replyAddress = GetReplyAddress(azureServiceBusMessage); + var handledCount = GetHandledCount(azureServiceBusMessage); - var headers = new MessageHeader( - messageId: azureServiceBusMessage.Id, - topic: new RoutingKey(Topic), - messageType: messageType, - source: null, - type: "", - timeStamp: DateTime.UtcNow, - correlationId: azureServiceBusMessage.CorrelationId, - replyTo: new RoutingKey(replyAddress), - contentType: azureServiceBusMessage.ContentType, - handledCount:handledCount, - dataSchema: null, - subject: null, - delayed: TimeSpan.Zero - ); - - headers.Bag.Add(ASBConstants.LockTokenHeaderBagKey, azureServiceBusMessage.LockToken); + //TODO:CLOUD_EVENTS parse from headers - foreach (var property in azureServiceBusMessage.ApplicationProperties) - { - headers.Bag.Add(property.Key, property.Value); - } + var headers = new MessageHeader( + messageId: azureServiceBusMessage.Id, + topic: new RoutingKey(Topic), + messageType: messageType, + source: null, + type: "", + timeStamp: DateTime.UtcNow, + correlationId: azureServiceBusMessage.CorrelationId, + replyTo: new RoutingKey(replyAddress), + contentType: azureServiceBusMessage.ContentType, + handledCount:handledCount, + dataSchema: null, + subject: null, + delayed: TimeSpan.Zero + ); + + headers.Bag.Add(ASBConstants.LockTokenHeaderBagKey, azureServiceBusMessage.LockToken); - var message = new Message(headers, new MessageBody(messageBody)); - return message; + foreach (var property in azureServiceBusMessage.ApplicationProperties) + { + headers.Bag.Add(property.Key, property.Value); } + + var message = new Message(headers, new MessageBody(messageBody)); + return message; + } - private static MessageType GetMessageType(IBrokeredMessageWrapper azureServiceBusMessage) - { - if (azureServiceBusMessage.ApplicationProperties == null || - !azureServiceBusMessage.ApplicationProperties.TryGetValue(ASBConstants.MessageTypeHeaderBagKey, - out object? property)) - return MessageType.MT_EVENT; + private static MessageType GetMessageType(IBrokeredMessageWrapper azureServiceBusMessage) + { + if (azureServiceBusMessage.ApplicationProperties == null || + !azureServiceBusMessage.ApplicationProperties.TryGetValue(ASBConstants.MessageTypeHeaderBagKey, + out object? property)) + return MessageType.MT_EVENT; - if (Enum.TryParse(property.ToString(), true, out MessageType messageType)) - return messageType; + if (Enum.TryParse(property.ToString(), true, out MessageType messageType)) + return messageType; - return MessageType.MT_EVENT; - } + return MessageType.MT_EVENT; + } - private static string GetReplyAddress(IBrokeredMessageWrapper azureServiceBusMessage) + private static string GetReplyAddress(IBrokeredMessageWrapper azureServiceBusMessage) + { + if (azureServiceBusMessage.ApplicationProperties is null || + !azureServiceBusMessage.ApplicationProperties.TryGetValue(ASBConstants.ReplyToHeaderBagKey, + out object? property)) { - if (azureServiceBusMessage.ApplicationProperties is null || - !azureServiceBusMessage.ApplicationProperties.TryGetValue(ASBConstants.ReplyToHeaderBagKey, - out object? property)) - { - return string.Empty; - } + return string.Empty; + } - var replyAddress = property.ToString(); + var replyAddress = property.ToString(); - return replyAddress ?? string.Empty; - } + return replyAddress ?? string.Empty; + } - private static int GetHandledCount(IBrokeredMessageWrapper azureServiceBusMessage) + private static int GetHandledCount(IBrokeredMessageWrapper azureServiceBusMessage) + { + var count = 0; + if (azureServiceBusMessage.ApplicationProperties != null && + azureServiceBusMessage.ApplicationProperties.TryGetValue(ASBConstants.HandledCountHeaderBagKey, + out object? property)) { - var count = 0; - if (azureServiceBusMessage.ApplicationProperties != null && - azureServiceBusMessage.ApplicationProperties.TryGetValue(ASBConstants.HandledCountHeaderBagKey, - out object? property)) - { - int.TryParse(property.ToString(), out count); - } - - return count; + int.TryParse(property.ToString(), out count); } - protected abstract void EnsureChannel(); + return count; + } + + protected abstract Task EnsureChannelAsync(); - private void HandleAsbException(ServiceBusException ex, string messageId) + private void HandleAsbException(ServiceBusException ex, string messageId) + { + if (ex.Reason == ServiceBusFailureReason.MessageLockLost) + Logger.LogError(ex, "Error completing peak lock on message with id {Id}", messageId); + else { - if (ex.Reason == ServiceBusFailureReason.MessageLockLost) - Logger.LogError(ex, "Error completing peak lock on message with id {Id}", messageId); - else - { - Logger.LogError(ex, - "Error completing peak lock on message with id {Id} Reason {ErrorReason}", - messageId, ex.Reason); - } + Logger.LogError(ex, + "Error completing peak lock on message with id {Id} Reason {ErrorReason}", + messageId, ex.Reason); } } } diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusConsumerFactory.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusConsumerFactory.cs index 2688710526..96016a9965 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusConsumerFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusConsumerFactory.cs @@ -1,76 +1,111 @@ -using System; -using Azure.Messaging.ServiceBus; +#region Licence +/* The MIT License (MIT) +Copyright © 2022 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.MessagingGateway.AzureServiceBus.AzureServiceBusWrappers; using Paramore.Brighter.MessagingGateway.AzureServiceBus.ClientProvider; using IServiceBusClientProvider = Paramore.Brighter.MessagingGateway.AzureServiceBus.ClientProvider.IServiceBusClientProvider; -namespace Paramore.Brighter.MessagingGateway.AzureServiceBus +namespace Paramore.Brighter.MessagingGateway.AzureServiceBus; + +/// +/// Factory class for creating instances of +/// +public class AzureServiceBusConsumerFactory : IAmAMessageConsumerFactory { + private readonly IServiceBusClientProvider _clientProvider; + + /// + /// Factory to create an Azure Service Bus Consumer + /// + /// The configuration to connect to + public AzureServiceBusConsumerFactory(AzureServiceBusConfiguration configuration) + : this(new ServiceBusConnectionStringClientProvider(configuration.ConnectionString)) + { } + + /// + /// Factory to create an Azure Service Bus Consumer + /// + /// A client Provider to determine how to connect to ASB + public AzureServiceBusConsumerFactory(IServiceBusClientProvider clientProvider) + { + _clientProvider = clientProvider; + } + /// - /// Factory class for creating instances of + /// Creates a consumer for the specified queue. /// - public class AzureServiceBusConsumerFactory : IAmAMessageConsumerFactory + /// The queue to connect to + /// IAmAMessageConsumerSync + public IAmAMessageConsumerSync Create(Subscription subscription) { - private readonly IServiceBusClientProvider _clientProvider; - - /// - /// Factory to create an Azure Service Bus Consumer - /// - /// The configuration to connect to - public AzureServiceBusConsumerFactory(AzureServiceBusConfiguration configuration) - : this(new ServiceBusConnectionStringClientProvider(configuration.ConnectionString)) - { } - - /// - /// Factory to create an Azure Service Bus Consumer - /// - /// A client Provider to determine how to connect to ASB - public AzureServiceBusConsumerFactory(IServiceBusClientProvider clientProvider) + var nameSpaceManagerWrapper = new AdministrationClientWrapper(_clientProvider); + + if (!(subscription is AzureServiceBusSubscription sub)) + throw new ArgumentException("Subscription is not of type AzureServiceBusSubscription.", + nameof(subscription)); + + var receiverProvider = new ServiceBusReceiverProvider(_clientProvider); + + if (sub.Configuration.UseServiceBusQueue) { - _clientProvider = clientProvider; - } + var messageProducer = new AzureServiceBusQueueMessageProducer( + nameSpaceManagerWrapper, + new ServiceBusSenderProvider(_clientProvider), + new AzureServiceBusPublication { MakeChannels = subscription.MakeChannels }); - /// - /// Creates a consumer for the specified queue. - /// - /// The queue to connect to - /// IAmAMessageConsumer - public IAmAMessageConsumer Create(Subscription subscription) + return new AzureServiceBusQueueConsumer( + sub, + messageProducer, + nameSpaceManagerWrapper, + receiverProvider); + } + else { - var nameSpaceManagerWrapper = new AdministrationClientWrapper(_clientProvider); - - if (!(subscription is AzureServiceBusSubscription sub)) - throw new ArgumentException("Subscription is not of type AzureServiceBusSubscription.", - nameof(subscription)); - - var receiverProvider = new ServiceBusReceiverProvider(_clientProvider); - - if (sub.Configuration.UseServiceBusQueue) - { - var messageProducer = new AzureServiceBusQueueMessageProducer( - nameSpaceManagerWrapper, - new ServiceBusSenderProvider(_clientProvider), - new AzureServiceBusPublication { MakeChannels = subscription.MakeChannels }); - - return new AzureServiceBusQueueConsumer( - sub, - messageProducer, - nameSpaceManagerWrapper, - receiverProvider); - } - else - { - var messageProducer = new AzureServiceBusTopicMessageProducer( - nameSpaceManagerWrapper, - new ServiceBusSenderProvider(_clientProvider), - new AzureServiceBusPublication { MakeChannels = subscription.MakeChannels }); - - return new AzureServiceBusTopicConsumer( - sub, - messageProducer, - nameSpaceManagerWrapper, - receiverProvider); - } + var messageProducer = new AzureServiceBusTopicMessageProducer( + nameSpaceManagerWrapper, + new ServiceBusSenderProvider(_clientProvider), + new AzureServiceBusPublication { MakeChannels = subscription.MakeChannels }); + + return new AzureServiceBusTopicConsumer( + sub, + messageProducer, + nameSpaceManagerWrapper, + receiverProvider); } } + + /// + /// Creates a consumer for the specified queue. + /// + /// The queue to connect to + /// IAmAMessageConsumerSync + public IAmAMessageConsumerAsync CreateAsync(Subscription subscription) + { + var consumer = Create(subscription) as IAmAMessageConsumerAsync; + if (consumer == null) + throw new ChannelFailureException("AzureServiceBusConsumerFactory: Failed to create an async consumer"); + return consumer; + } } diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusMessageProducer.cs index cac1ea8e1c..962dc23c3f 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusMessageProducer.cs @@ -35,226 +35,237 @@ THE SOFTWARE. */ using Polly.Retry; using System.Threading.Tasks; using Azure.Messaging.ServiceBus; +using Paramore.Brighter.Tasks; -namespace Paramore.Brighter.MessagingGateway.AzureServiceBus +namespace Paramore.Brighter.MessagingGateway.AzureServiceBus; + + +/// +/// A Sync and Async Message Producer for Azure Service Bus. +/// +public abstract class AzureServiceBusMessageProducer : IAmAMessageProducerSync, IAmAMessageProducerAsync, IAmABulkMessageProducerAsync { + private readonly IServiceBusSenderProvider _serviceBusSenderProvider; + private readonly AzureServiceBusPublication _publication; + protected bool TopicCreated; + + private const int TopicConnectionSleepBetweenRetriesInMilliseconds = 100; + private const int TopicConnectionRetryCount = 5; + private readonly int _bulkSendBatchSize; + + protected abstract ILogger Logger { get; } + + /// + /// The publication configuration for this producer + /// + public Publication Publication { get { return _publication; } } + + /// + /// The OTel Span we are writing Producer events too + /// + public Activity? Span { get; set; } + /// - /// A Sync and Async Message Producer for Azure Service Bus. + /// An Azure Service Bus Message producer /// - public abstract class AzureServiceBusMessageProducer : IAmAMessageProducerSync, IAmAMessageProducerAsync, IAmABulkMessageProducerAsync + /// The provider to use when producing messages. + /// Configuration of a producer + /// When sending more than one message using the MessageProducer, the max amount to send in a single transmission. + protected AzureServiceBusMessageProducer( + IServiceBusSenderProvider serviceBusSenderProvider, + AzureServiceBusPublication publication, + int bulkSendBatchSize = 10 + ) { - private readonly IServiceBusSenderProvider _serviceBusSenderProvider; - private readonly AzureServiceBusPublication _publication; - protected bool TopicCreated; + _serviceBusSenderProvider = serviceBusSenderProvider; + _publication = publication; + _bulkSendBatchSize = bulkSendBatchSize; + } - private const int TopicConnectionSleepBetweenRetriesInMilliseconds = 100; - private const int TopicConnectionRetryCount = 5; - private readonly int _bulkSendBatchSize; + /// + /// Dispose of the producer + /// + public void Dispose() { } - protected abstract ILogger Logger { get; } + /// + /// Dispose of the producer + /// + /// + public ValueTask DisposeAsync() + { + return new ValueTask(Task.CompletedTask); + } - /// - /// The publication configuration for this producer - /// - public Publication Publication { get { return _publication; } } - - /// - /// The OTel Span we are writing Producer events too - /// - public Activity? Span { get; set; } - - /// - /// An Azure Service Bus Message producer - /// - /// The provider to use when producing messages. - /// Configuration of a producer - /// When sending more than one message using the MessageProducer, the max amount to send in a single transmission. - protected AzureServiceBusMessageProducer( - IServiceBusSenderProvider serviceBusSenderProvider, - AzureServiceBusPublication publication, - int bulkSendBatchSize = 10 - ) - { - _serviceBusSenderProvider = serviceBusSenderProvider; - _publication = publication; - _bulkSendBatchSize = bulkSendBatchSize; - } + /// + /// Sends the specified message. + /// + /// The message. + public void Send(Message message) + { + SendWithDelay(message); + } - /// - /// Sends the specified message. - /// - /// The message. - public void Send(Message message) - { - SendWithDelay(message); - } + /// + /// Sends the specified message. + /// + /// The message. + /// Cancel the in-flight send operation + public async Task SendAsync(Message message, CancellationToken cancellationToken = default) + { + await SendWithDelayAsync(message, cancellationToken: cancellationToken); + } - /// - /// Sends the specified message. - /// - /// The message. - public async Task SendAsync(Message message) + /// + /// Sends a Batch of Messages + /// + /// The messages to send. + /// The Cancellation Token. + /// List of Messages successfully sent. + /// + public async IAsyncEnumerable SendAsync( + IEnumerable messages, + [EnumeratorCancellation] CancellationToken cancellationToken + ) + { + var topics = messages.Select(m => m.Header.Topic).Distinct(); + if (topics.Count() != 1) { - await SendWithDelayAsync(message); + Logger.LogError("Cannot Bulk send for Multiple Topics, {NumberOfTopics} Topics Requested", topics.Count()); + throw new Exception($"Cannot Bulk send for Multiple Topics, {topics.Count()} Topics Requested"); } + var topic = topics.First()!; - /// - /// Sends a Batch of Messages - /// - /// The messages to send. - /// The Cancellation Token. - /// List of Messages successfully sent. - /// - public async IAsyncEnumerable SendAsync( - IEnumerable messages, - [EnumeratorCancellation] CancellationToken cancellationToken - ) - { - var topics = messages.Select(m => m.Header.Topic).Distinct(); - if (topics.Count() != 1) - { - Logger.LogError("Cannot Bulk send for Multiple Topics, {NumberOfTopics} Topics Requested", topics.Count()); - throw new Exception($"Cannot Bulk send for Multiple Topics, {topics.Count()} Topics Requested"); - } - var topic = topics.First()!; + var batches = Enumerable.Range(0, (int)Math.Ceiling(messages.Count() / (decimal)_bulkSendBatchSize)) + .Select(i => new List(messages + .Skip(i * _bulkSendBatchSize) + .Take(_bulkSendBatchSize) + .ToArray())); - var batches = Enumerable.Range(0, (int)Math.Ceiling(messages.Count() / (decimal)_bulkSendBatchSize)) - .Select(i => new List(messages - .Skip(i * _bulkSendBatchSize) - .Take(_bulkSendBatchSize) - .ToArray())); + var serviceBusSenderWrapper = await GetSenderAsync(topic); - var serviceBusSenderWrapper = GetSender(topic); - - Logger.LogInformation("Sending Messages for {TopicName} split into {NumberOfBatches} Batches of {BatchSize}", topic, batches.Count(), _bulkSendBatchSize); - try + Logger.LogInformation("Sending Messages for {TopicName} split into {NumberOfBatches} Batches of {BatchSize}", topic, batches.Count(), _bulkSendBatchSize); + try + { + foreach (var batch in batches) { - foreach (var batch in batches) - { - var asbMessages = batch.Select(ConvertToServiceBusMessage).ToArray(); + var asbMessages = batch.Select(ConvertToServiceBusMessage).ToArray(); - Logger.LogDebug("Publishing {NumberOfMessages} messages to topic {Topic}.", - asbMessages.Length, topic); + Logger.LogDebug("Publishing {NumberOfMessages} messages to topic {Topic}.", + asbMessages.Length, topic); - await serviceBusSenderWrapper.SendAsync(asbMessages, cancellationToken); - yield return batch.Select(m => m.Id).ToArray(); - } - } - finally - { - await serviceBusSenderWrapper.CloseAsync(); + await serviceBusSenderWrapper.SendAsync(asbMessages, cancellationToken); + yield return batch.Select(m => m.Id).ToArray(); } } - - /// - /// Send the specified message with specified delay - /// - /// The message. - /// Delay to delivery of the message. - public void SendWithDelay(Message message, TimeSpan? delay = null) + finally { - delay ??= TimeSpan.Zero; - SendWithDelayAsync(message, delay).Wait(); + await serviceBusSenderWrapper.CloseAsync(); } + } - /// - /// Send the specified message with specified delay - /// - /// The message. - /// Delay delivery of the message. - public async Task SendWithDelayAsync(Message message, TimeSpan? delay = null) - { - Logger.LogDebug("Preparing to send message on topic {Topic}", message.Header.Topic); + /// + /// Send the specified message with specified delay + /// Sync over Async + /// + /// The message. + /// Delay to delivery of the message. + public void SendWithDelay(Message message, TimeSpan? delay = null) => BrighterSynchronizationHelper.Run(async () => await SendWithDelayAsync(message, delay)); + + /// + /// Send the specified message with specified delay + /// + /// The message. + /// Delay delivery of the message. + /// Cancel the in-flight send operation + public async Task SendWithDelayAsync(Message message, TimeSpan? delay = null, CancellationToken cancellationToken = default) + { + Logger.LogDebug("Preparing to send message on topic {Topic}", message.Header.Topic); - delay ??= TimeSpan.Zero; + delay ??= TimeSpan.Zero; - if (message.Header.Topic is null) throw new ArgumentException("Topic not be null"); + if (message.Header.Topic is null) throw new ArgumentException("Topic not be null"); - var serviceBusSenderWrapper = GetSender(message.Header.Topic); + var serviceBusSenderWrapper = await GetSenderAsync(message.Header.Topic); - try - { - Logger.LogDebug( - "Publishing message to topic {Topic} with a delay of {Delay} and body {Request} and id {Id}", - message.Header.Topic, delay, message.Body.Value, message.Id); - - var azureServiceBusMessage = ConvertToServiceBusMessage(message); - if (delay == TimeSpan.Zero) - { - await serviceBusSenderWrapper.SendAsync(azureServiceBusMessage); - } - else - { - var dateTimeOffset = new DateTimeOffset(DateTime.UtcNow.Add(delay.Value)); - await serviceBusSenderWrapper.ScheduleMessageAsync(azureServiceBusMessage, dateTimeOffset); - } - - Logger.LogDebug( - "Published message to topic {Topic} with a delay of {Delay} and body {Request} and id {Id}", message.Header.Topic, delay, message.Body.Value, message.Id); - } - catch (Exception e) + try + { + Logger.LogDebug( + "Publishing message to topic {Topic} with a delay of {Delay} and body {Request} and id {Id}", + message.Header.Topic, delay, message.Body.Value, message.Id); + + var azureServiceBusMessage = ConvertToServiceBusMessage(message); + if (delay == TimeSpan.Zero) { - Logger.LogError(e, "Failed to publish message to topic {Topic} with id {Id}, message will not be retried", message.Header.Topic, message.Id); - throw new ChannelFailureException("Error talking to the broker, see inner exception for details", e); + await serviceBusSenderWrapper.SendAsync(azureServiceBusMessage, cancellationToken); } - finally + else { - await serviceBusSenderWrapper.CloseAsync(); + var dateTimeOffset = new DateTimeOffset(DateTime.UtcNow.Add(delay.Value)); + await serviceBusSenderWrapper.ScheduleMessageAsync(azureServiceBusMessage, dateTimeOffset, cancellationToken); } - } - public void Dispose() + Logger.LogDebug( + "Published message to topic {Topic} with a delay of {Delay} and body {Request} and id {Id}", message.Header.Topic, delay, message.Body.Value, message.Id); + } + catch (Exception e) { + Logger.LogError(e, "Failed to publish message to topic {Topic} with id {Id}, message will not be retried", message.Header.Topic, message.Id); + throw new ChannelFailureException("Error talking to the broker, see inner exception for details", e); } - - private IServiceBusSenderWrapper GetSender(string topic) + finally { - EnsureChannelExists(topic); - - try - { - RetryPolicy policy = Policy - .Handle() - .Retry(TopicConnectionRetryCount, (exception, retryNumber) => - { - Logger.LogError(exception, "Failed to connect to topic {Topic}, retrying...", - topic); - - Thread.Sleep(TimeSpan.FromMilliseconds(TopicConnectionSleepBetweenRetriesInMilliseconds)); - } - ); - - return policy.Execute(() => _serviceBusSenderProvider.Get(topic)); - } - catch (Exception e) - { - Logger.LogError(e, "Failed to connect to topic {Topic}, aborting.", topic); - throw; - } + await serviceBusSenderWrapper.CloseAsync(); } + } - private ServiceBusMessage ConvertToServiceBusMessage(Message message) - { - var azureServiceBusMessage = new ServiceBusMessage(message.Body.Bytes); - azureServiceBusMessage.ApplicationProperties.Add(ASBConstants.MessageTypeHeaderBagKey, message.Header.MessageType.ToString()); - azureServiceBusMessage.ApplicationProperties.Add(ASBConstants.HandledCountHeaderBagKey, message.Header.HandledCount); - azureServiceBusMessage.ApplicationProperties.Add(ASBConstants.ReplyToHeaderBagKey, message.Header.ReplyTo); + private async Task GetSenderAsync(string topic) + { + await EnsureChannelExistsAsync(topic); - foreach (var header in message.Header.Bag.Where(h => !ASBConstants.ReservedHeaders.Contains(h.Key))) - { - azureServiceBusMessage.ApplicationProperties.Add(header.Key, header.Value); - } - - if(message.Header.CorrelationId is not null) - azureServiceBusMessage.CorrelationId = message.Header.CorrelationId; - azureServiceBusMessage.ContentType = message.Header.ContentType; - azureServiceBusMessage.MessageId = message.Header.MessageId; - if (message.Header.Bag.TryGetValue(ASBConstants.SessionIdKey, out object? value)) - azureServiceBusMessage.SessionId = value.ToString(); - - return azureServiceBusMessage; + try + { + RetryPolicy policy = Policy + .Handle() + .Retry(TopicConnectionRetryCount, (exception, retryNumber) => + { + Logger.LogError(exception, "Failed to connect to topic {Topic}, retrying...", + topic); + + Thread.Sleep(TimeSpan.FromMilliseconds(TopicConnectionSleepBetweenRetriesInMilliseconds)); + } + ); + + return policy.Execute(() => _serviceBusSenderProvider.Get(topic)); } + catch (Exception e) + { + Logger.LogError(e, "Failed to connect to topic {Topic}, aborting.", topic); + throw; + } + } + + private ServiceBusMessage ConvertToServiceBusMessage(Message message) + { + var azureServiceBusMessage = new ServiceBusMessage(message.Body.Bytes); + azureServiceBusMessage.ApplicationProperties.Add(ASBConstants.MessageTypeHeaderBagKey, message.Header.MessageType.ToString()); + azureServiceBusMessage.ApplicationProperties.Add(ASBConstants.HandledCountHeaderBagKey, message.Header.HandledCount); + azureServiceBusMessage.ApplicationProperties.Add(ASBConstants.ReplyToHeaderBagKey, message.Header.ReplyTo); - protected abstract void EnsureChannelExists(string channelName); + foreach (var header in message.Header.Bag.Where(h => !ASBConstants.ReservedHeaders.Contains(h.Key))) + { + azureServiceBusMessage.ApplicationProperties.Add(header.Key, header.Value); + } + + if(message.Header.CorrelationId is not null) + azureServiceBusMessage.CorrelationId = message.Header.CorrelationId; + azureServiceBusMessage.ContentType = message.Header.ContentType; + azureServiceBusMessage.MessageId = message.Header.MessageId; + if (message.Header.Bag.TryGetValue(ASBConstants.SessionIdKey, out object? value)) + azureServiceBusMessage.SessionId = value.ToString(); + + return azureServiceBusMessage; } + + protected abstract Task EnsureChannelExistsAsync(string channelName); + } diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusMessageProducerFactory.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusMessageProducerFactory.cs index dd0745e47a..5c8d9ab59a 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusMessageProducerFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusMessageProducerFactory.cs @@ -1,54 +1,85 @@ -using System; +#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.Collections.Generic; +using System.Threading.Tasks; using Paramore.Brighter.MessagingGateway.AzureServiceBus.AzureServiceBusWrappers; using Paramore.Brighter.MessagingGateway.AzureServiceBus.ClientProvider; -namespace Paramore.Brighter.MessagingGateway.AzureServiceBus +namespace Paramore.Brighter.MessagingGateway.AzureServiceBus; + +/// +/// Factory class for creating dictionary of instances of +/// indexed by topic name +/// +public class AzureServiceBusMessageProducerFactory : IAmAMessageProducerFactory { + private readonly IServiceBusClientProvider _clientProvider; + private readonly IEnumerable _publications; + private readonly int _bulkSendBatchSize; + /// - /// Factory class for creating dictionary of instances of - /// indexed by topic name + /// Factory to create a dictionary of Azure Service Bus Producers indexed by topic name /// - public class AzureServiceBusMessageProducerFactory : IAmAMessageProducerFactory + /// The connection to ASB + /// A set of publications - topics on the server - to configure + /// The maximum size to chunk messages when dispatching to ASB + public AzureServiceBusMessageProducerFactory( + IServiceBusClientProvider clientProvider, + IEnumerable publications, + int bulkSendBatchSize) { - private readonly IServiceBusClientProvider _clientProvider; - private readonly IEnumerable _publications; - private readonly int _bulkSendBatchSize; - - /// - /// Factory to create a dictionary of Azure Service Bus Producers indexed by topic name - /// - /// The connection to ASB - /// A set of publications - topics on the server - to configure - /// The maximum size to chunk messages when dispatching to ASB - public AzureServiceBusMessageProducerFactory( - IServiceBusClientProvider clientProvider, - IEnumerable publications, - int bulkSendBatchSize) + _clientProvider = clientProvider; + _publications = publications; + _bulkSendBatchSize = bulkSendBatchSize; + } + + /// + public Dictionary Create() + { + var nameSpaceManagerWrapper = new AdministrationClientWrapper(_clientProvider); + var topicClientProvider = new ServiceBusSenderProvider(_clientProvider); + + var producers = new Dictionary(); + foreach (var publication in _publications) { - _clientProvider = clientProvider; - _publications = publications; - _bulkSendBatchSize = bulkSendBatchSize; + if (publication.Topic is null) + throw new ArgumentException("Publication must have a Topic."); + if(publication.UseServiceBusQueue) + producers.Add(publication.Topic, new AzureServiceBusQueueMessageProducer(nameSpaceManagerWrapper, topicClientProvider, publication, _bulkSendBatchSize)); + else + producers.Add(publication.Topic, new AzureServiceBusTopicMessageProducer(nameSpaceManagerWrapper, topicClientProvider, publication, _bulkSendBatchSize)); } - /// - public Dictionary Create() - { - var nameSpaceManagerWrapper = new AdministrationClientWrapper(_clientProvider); - var topicClientProvider = new ServiceBusSenderProvider(_clientProvider); - - var producers = new Dictionary(); - foreach (var publication in _publications) - { - if (publication.Topic is null) - throw new ArgumentException("Publication must have a Topic."); - if(publication.UseServiceBusQueue) - producers.Add(publication.Topic, new AzureServiceBusQueueMessageProducer(nameSpaceManagerWrapper, topicClientProvider, publication, _bulkSendBatchSize)); - else - producers.Add(publication.Topic, new AzureServiceBusTopicMessageProducer(nameSpaceManagerWrapper, topicClientProvider, publication, _bulkSendBatchSize)); - } - - return producers; - } + return producers; + } + + public Task> CreateAsync() + { + return Task.FromResult(Create()); } } diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusProducerRegistryFactory.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusProducerRegistryFactory.cs index 2bb5b510c6..929b86713a 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusProducerRegistryFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusProducerRegistryFactory.cs @@ -1,53 +1,89 @@ -using System.Collections.Generic; +#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.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Paramore.Brighter.MessagingGateway.AzureServiceBus.ClientProvider; -namespace Paramore.Brighter.MessagingGateway.AzureServiceBus +namespace Paramore.Brighter.MessagingGateway.AzureServiceBus; + +public class AzureServiceBusProducerRegistryFactory : IAmAProducerRegistryFactory { - public class AzureServiceBusProducerRegistryFactory : IAmAProducerRegistryFactory + private readonly IServiceBusClientProvider _clientProvider; + private readonly IEnumerable _asbPublications; + private readonly int _bulkSendBatchSize; + + /// + /// Creates a producer registry initialized with producers for ASB derived from the publications + /// + /// The configuration of the connection to ASB + /// A set of publications - topics on the server - to configure + public AzureServiceBusProducerRegistryFactory( + AzureServiceBusConfiguration configuration, + IEnumerable asbPublications) + { + _clientProvider = new ServiceBusConnectionStringClientProvider(configuration.ConnectionString); + _asbPublications = asbPublications; + _bulkSendBatchSize = configuration.BulkSendBatchSize; + } + + /// + /// Creates a producer registry initialized with producers for ASB derived from the publications + /// + /// The connection to ASB + /// A set of publications - topics on the server - to configure + /// The maximum size to chunk messages when dispatching to ASB + public AzureServiceBusProducerRegistryFactory( + IServiceBusClientProvider clientProvider, + IEnumerable asbPublications, + int bulkSendBatchSize = 10) + { + _clientProvider = clientProvider; + _asbPublications = asbPublications; + _bulkSendBatchSize = bulkSendBatchSize; + } + + /// + /// Creates message producers. + /// + /// A has of middleware clients by topic, for sending messages to the middleware + public IAmAProducerRegistry Create() + { + var producerFactory = new AzureServiceBusMessageProducerFactory(_clientProvider, _asbPublications, _bulkSendBatchSize); + + return new ProducerRegistry(producerFactory.Create()); + } + + /// + /// Creates message producers. + /// + /// A has of middleware clients by topic, for sending messages to the middleware + public Task CreateAsync(CancellationToken ct = default) { - private readonly IServiceBusClientProvider _clientProvider; - private readonly IEnumerable _asbPublications; - private readonly int _bulkSendBatchSize; - - /// - /// Creates a producer registry initialized with producers for ASB derived from the publications - /// - /// The configuration of the connection to ASB - /// A set of publications - topics on the server - to configure - public AzureServiceBusProducerRegistryFactory( - AzureServiceBusConfiguration configuration, - IEnumerable asbPublications) - { - _clientProvider = new ServiceBusConnectionStringClientProvider(configuration.ConnectionString); - _asbPublications = asbPublications; - _bulkSendBatchSize = configuration.BulkSendBatchSize; - } - - /// - /// Creates a producer registry initialized with producers for ASB derived from the publications - /// - /// The connection to ASB - /// A set of publications - topics on the server - to configure - /// The maximum size to chunk messages when dispatching to ASB - public AzureServiceBusProducerRegistryFactory( - IServiceBusClientProvider clientProvider, - IEnumerable asbPublications, - int bulkSendBatchSize = 10) - { - _clientProvider = clientProvider; - _asbPublications = asbPublications; - _bulkSendBatchSize = bulkSendBatchSize; - } - - /// - /// Creates message producers. - /// - /// A has of middleware clients by topic, for sending messages to the middleware - public IAmAProducerRegistry Create() - { - var producerFactory = new AzureServiceBusMessageProducerFactory(_clientProvider, _asbPublications, _bulkSendBatchSize); - - return new ProducerRegistry(producerFactory.Create()); - } + return Task.FromResult(Create()); } } diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusPublication.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusPublication.cs index 6da5abf04d..c12c7ec729 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusPublication.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusPublication.cs @@ -1,12 +1,37 @@ -namespace Paramore.Brighter.MessagingGateway.AzureServiceBus +#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 + + +namespace Paramore.Brighter.MessagingGateway.AzureServiceBus; + +public class AzureServiceBusPublication : Publication { - public class AzureServiceBusPublication : Publication - { - //TODO: Placeholder for producer specific properties if required + //TODO: Placeholder for producer specific properties if required - /// - /// Use a Service Bus Queue instead of a Topic - /// - public bool UseServiceBusQueue = false; - } + /// + /// Use a Service Bus Queue instead of a Topic + /// + public bool UseServiceBusQueue = false; } diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusQueueConsumer.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusQueueConsumer.cs index a373cd4680..abb626d192 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusQueueConsumer.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusQueueConsumer.cs @@ -1,111 +1,140 @@ -using System; +#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; +using System.Threading.Tasks; using Azure.Messaging.ServiceBus; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; using Paramore.Brighter.MessagingGateway.AzureServiceBus.AzureServiceBusWrappers; +using Paramore.Brighter.Tasks; + +namespace Paramore.Brighter.MessagingGateway.AzureServiceBus; -namespace Paramore.Brighter.MessagingGateway.AzureServiceBus +/// +/// Implementation of using Azure Service Bus for Transport. +/// +public class AzureServiceBusQueueConsumer : AzureServiceBusConsumer { + protected override string SubscriptionName => "Queue"; + protected override ILogger Logger => s_logger; + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + private readonly IServiceBusReceiverProvider _serviceBusReceiverProvider; + + private bool _queueCreated = false; + /// - /// Implementation of using Azure Service Bus for Transport. + /// Initializes an Instance of for Service Bus Queus /// - public class AzureServiceBusQueueConsumer : AzureServiceBusConsumer + /// An Azure Service Bus Subscription. + /// An instance of the Messaging Producer used for Requeue. + /// An Instance of Administration Client Wrapper. + /// An Instance of . + public AzureServiceBusQueueConsumer(AzureServiceBusSubscription subscription, + IAmAMessageProducerSync messageProducer, + IAdministrationClientWrapper administrationClientWrapper, + IServiceBusReceiverProvider serviceBusReceiverProvider) : base(subscription, + messageProducer, administrationClientWrapper) { - protected override string SubscriptionName => "Queue"; - protected override ILogger Logger => s_logger; - private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - private readonly IServiceBusReceiverProvider _serviceBusReceiverProvider; + _serviceBusReceiverProvider = serviceBusReceiverProvider; + } - private bool _queueCreated = false; - - /// - /// Initializes an Instance of for Service Bus Queus - /// - /// An Azure Service Bus Subscription. - /// An instance of the Messaging Producer used for Requeue. - /// An Instance of Administration Client Wrapper. - /// An Instance of . - public AzureServiceBusQueueConsumer(AzureServiceBusSubscription subscription, - IAmAMessageProducerSync messageProducerSync, - IAdministrationClientWrapper administrationClientWrapper, - IServiceBusReceiverProvider serviceBusReceiverProvider) : base(subscription, - messageProducerSync, administrationClientWrapper) + protected override async Task GetMessageReceiverProviderAsync() + { + s_logger.LogInformation( + "Getting message receiver provider for queue {Queue}...", + Topic); + try { - _serviceBusReceiverProvider = serviceBusReceiverProvider; + ServiceBusReceiver = await _serviceBusReceiverProvider.GetAsync(Topic, SubscriptionConfiguration.RequireSession); } + catch (Exception e) + { + s_logger.LogError(e, "Failed to get message receiver provider for queue {Queue}", Topic); + } + } + + /// + /// Purges the specified queue name. + /// + public override void Purge() => BrighterSynchronizationHelper.Run(async () => await PurgeAsync()); + + /// + /// Purges the specified queue name. + /// + public override async Task PurgeAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + Logger.LogInformation("Purging messages from Queue {Queue}", Topic); + + await AdministrationClientWrapper.DeleteQueueAsync(Topic); + await EnsureChannelAsync(); + } + + protected override async Task EnsureChannelAsync() + { + if (_queueCreated || Subscription.MakeChannels.Equals(OnMissingChannel.Assume)) + return; - protected override void GetMessageReceiverProvider() + try { - s_logger.LogInformation( - "Getting message receiver provider for queue {Queue}...", - Topic); - try + if (await AdministrationClientWrapper.QueueExistsAsync(Topic)) { - ServiceBusReceiver = _serviceBusReceiverProvider.Get(Topic, - SubscriptionConfiguration.RequireSession); + _queueCreated = true; + return; } - catch (Exception e) + + if (Subscription.MakeChannels.Equals(OnMissingChannel.Validate)) { - s_logger.LogError(e, "Failed to get message receiver provider for queue {Queue}", Topic); + throw new ChannelFailureException($"Queue {Topic} does not exist and missing channel mode set to Validate."); } - } - - /// - /// Purges the specified queue name. - /// - public override void Purge() - { - Logger.LogInformation("Purging messages from Queue {Queue}", - Topic); - AdministrationClientWrapper.DeleteQueueAsync(Topic); - EnsureChannel(); + await AdministrationClientWrapper.CreateQueueAsync(Topic, SubscriptionConfiguration.QueueIdleBeforeDelete); + _queueCreated = true; } - - protected override void EnsureChannel() + catch (ServiceBusException ex) { - if (_queueCreated || Subscription.MakeChannels.Equals(OnMissingChannel.Assume)) - return; - - try + if (ex.Reason == ServiceBusFailureReason.MessagingEntityAlreadyExists) { - if (AdministrationClientWrapper.QueueExists(Topic)) - { - _queueCreated = true; - return; - } - - if (Subscription.MakeChannels.Equals(OnMissingChannel.Validate)) - { - throw new ChannelFailureException( - $"Queue {Topic} does not exist and missing channel mode set to Validate."); - } - - AdministrationClientWrapper.CreateQueue(Topic, SubscriptionConfiguration.QueueIdleBeforeDelete); + s_logger.LogWarning( + "Message entity already exists with queue {Queue}", Topic); _queueCreated = true; } - catch (ServiceBusException ex) + else { - if (ex.Reason == ServiceBusFailureReason.MessagingEntityAlreadyExists) - { - s_logger.LogWarning( - "Message entity already exists with queue {Queue}", Topic); - _queueCreated = true; - } - else - { - throw new ChannelFailureException("Failing to check or create subscription", ex); - } + throw new ChannelFailureException("Failing to check or create subscription", ex); } - catch (Exception e) - { - s_logger.LogError(e, "Failing to check or create subscription"); + } + catch (Exception e) + { + s_logger.LogError(e, "Failing to check or create subscription"); - //The connection to Azure Service bus may have failed so we re-establish the connection. - AdministrationClientWrapper.Reset(); + //The connection to Azure Service bus may have failed so we re-establish the connection. + AdministrationClientWrapper.Reset(); - throw new ChannelFailureException("Failing to check or create subscription", e); - } + throw new ChannelFailureException("Failing to check or create subscription", e); } } } diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusQueueMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusQueueMessageProducer.cs index 4eadde9ad0..714f3a550a 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusQueueMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusQueueMessageProducer.cs @@ -24,6 +24,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; using Paramore.Brighter.MessagingGateway.AzureServiceBus.AzureServiceBusWrappers; @@ -58,14 +59,14 @@ public AzureServiceBusQueueMessageProducer( _administrationClientWrapper = administrationClientWrapper; } - protected override void EnsureChannelExists(string channelName) + protected override async Task EnsureChannelExistsAsync(string channelName) { if (TopicCreated || Publication.MakeChannels.Equals(OnMissingChannel.Assume)) return; try { - if (_administrationClientWrapper.QueueExists(channelName)) + if (await _administrationClientWrapper.QueueExistsAsync(channelName)) { TopicCreated = true; return; @@ -76,7 +77,7 @@ protected override void EnsureChannelExists(string channelName) throw new ChannelFailureException($"Queue {channelName} does not exist and missing channel mode set to Validate."); } - _administrationClientWrapper.CreateQueue(channelName); + await _administrationClientWrapper.CreateQueueAsync(channelName); TopicCreated = true; } catch (Exception e) diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusSubscription.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusSubscription.cs index e3d920d92f..d3522b3ae0 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusSubscription.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusSubscription.cs @@ -1,102 +1,127 @@ -using System; +#region Licence -namespace Paramore.Brighter.MessagingGateway.AzureServiceBus +/* 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; + +namespace Paramore.Brighter.MessagingGateway.AzureServiceBus; + +/// +/// A with Specific option for Azure Service Bus. +/// +public class AzureServiceBusSubscription : Subscription { + public AzureServiceBusSubscriptionConfiguration Configuration { get; } + /// - /// A with Specific option for Azure Service Bus. + /// Initializes an Instance of /// - public class AzureServiceBusSubscription : Subscription + /// The type for this Subscription. + /// The name. Defaults to the data type's full name. + /// The channel name. Defaults to the data type's full name. + /// The routing key. Defaults to the data type's full name. + /// The number of messages to buffer on the channel + /// The no of performers. + /// The timeout to wait. Defaults to 300ms + /// The number of times you want to requeue a message before dropping it. + /// The number of milliseconds to delay the delivery of a requeue message for. + /// The number of unacceptable messages to handle, before stopping reading from the channel. + /// + /// The channel factory to create channels for Consumer. + /// Should we make channels if they don't exist, defaults to creating + /// The configuration options for the subscriptions. + /// How long to pause when a channel is empty in milliseconds + /// How long to pause when there is a channel failure in milliseconds + public AzureServiceBusSubscription( + Type dataType, + SubscriptionName? name = null, + ChannelName? channelName = null, + RoutingKey? routingKey = null, + int bufferSize = 1, + int noOfPerformers = 1, + TimeSpan? timeOut = null, + int requeueCount = -1, + TimeSpan? requeueDelay = null, + int unacceptableMessageLimit = 0, + MessagePumpType messagePumpType = MessagePumpType.Proactor, + IAmAChannelFactory? channelFactory = null, + OnMissingChannel makeChannels = OnMissingChannel.Create, + AzureServiceBusSubscriptionConfiguration? subscriptionConfiguration = null, + TimeSpan? emptyChannelDelay = null, + TimeSpan? channelFailureDelay = null) + : base(dataType, name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount, + requeueDelay, unacceptableMessageLimit, messagePumpType, channelFactory, makeChannels, emptyChannelDelay, + channelFailureDelay) { - public AzureServiceBusSubscriptionConfiguration Configuration { get; } - - /// - /// Initializes an Instance of - /// - /// The type for this Subscription. - /// The name. Defaults to the data type's full name. - /// The channel name. Defaults to the data type's full name. - /// The routing key. Defaults to the data type's full name. - /// The number of messages to buffer on the channel - /// The no of performers. - /// The timeout to wait. Defaults to 300ms - /// The number of times you want to requeue a message before dropping it. - /// The number of milliseconds to delay the delivery of a requeue message for. - /// The number of unacceptable messages to handle, before stopping reading from the channel. - /// - /// The channel factory to create channels for Consumer. - /// Should we make channels if they don't exist, defaults to creating - /// The configuration options for the subscriptions. - /// How long to pause when a channel is empty in milliseconds - /// How long to pause when there is a channel failure in milliseconds - public AzureServiceBusSubscription( - Type dataType, - SubscriptionName? name = null, - ChannelName? channelName = null, - RoutingKey? routingKey = null, - int bufferSize = 1, - int noOfPerformers = 1, - TimeSpan? timeOut = null, - int requeueCount = -1, - TimeSpan? requeueDelay = null, - int unacceptableMessageLimit = 0, - bool isAsync = false, - IAmAChannelFactory? channelFactory = null, - OnMissingChannel makeChannels = OnMissingChannel.Create, - AzureServiceBusSubscriptionConfiguration? subscriptionConfiguration = null, - int emptyChannelDelay = 500, - int channelFailureDelay = 1000) - : base(dataType, name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount, - requeueDelay, unacceptableMessageLimit, isAsync, channelFactory, makeChannels, emptyChannelDelay, - channelFailureDelay) - { - Configuration = subscriptionConfiguration ?? new AzureServiceBusSubscriptionConfiguration(); - } + Configuration = subscriptionConfiguration ?? new AzureServiceBusSubscriptionConfiguration(); } +} +/// +/// Initializes an Instance of +/// +/// The type of Subscription. +public class AzureServiceBusSubscription : AzureServiceBusSubscription where T : IRequest +{ /// /// Initializes an Instance of /// - /// The type of Subscription. - public class AzureServiceBusSubscription : AzureServiceBusSubscription where T : IRequest + /// The name. Defaults to the data type's full name. + /// The channel name. Defaults to the data type's full name. + /// The routing key. Defaults to the data type's full name. + /// The number of messages to buffer on the channel + /// The no of performers. + /// The timeout to wait for messages; defaults to 300ms + /// The number of times you want to requeue a message before dropping it. + /// The delay the delivery of a requeue message. 0 is no delay. Defaults to 0 + /// The number of unacceptable messages to handle, before stopping reading from the channel. + /// + /// The channel factory to create channels for Consumer. + /// Should we make channels if they don't exist, defaults to creating + /// The configuration options for the subscriptions. + /// How long to pause when a channel is empty in milliseconds + /// How long to pause when there is a channel failure in milliseconds + public AzureServiceBusSubscription( + SubscriptionName? name = null, + ChannelName? channelName = null, + RoutingKey? routingKey = null, + int bufferSize = 1, + int noOfPerformers = 1, + TimeSpan? timeOut = null, + int requeueCount = -1, + TimeSpan? requeueDelay = null, + int unacceptableMessageLimit = 0, + MessagePumpType messagePumpType = MessagePumpType.Proactor, + IAmAChannelFactory? channelFactory = null, + OnMissingChannel makeChannels = OnMissingChannel.Create, + AzureServiceBusSubscriptionConfiguration? subscriptionConfiguration = null, + TimeSpan? emptyChannelDelay = null, + TimeSpan? channelFailureDelay = null) + : base(typeof(T), name, channelName, routingKey, bufferSize, noOfPerformers, + timeOut, requeueCount, requeueDelay, unacceptableMessageLimit, messagePumpType, channelFactory, makeChannels, + subscriptionConfiguration, emptyChannelDelay, channelFailureDelay) { - /// - /// Initializes an Instance of - /// - /// The name. Defaults to the data type's full name. - /// The channel name. Defaults to the data type's full name. - /// The routing key. Defaults to the data type's full name. - /// The number of messages to buffer on the channel - /// The no of performers. - /// The timeout to wait for messages; defaults to 300ms - /// The number of times you want to requeue a message before dropping it. - /// The delay the delivery of a requeue message. 0 is no delay. Defaults to 0 - /// The number of unacceptable messages to handle, before stopping reading from the channel. - /// - /// The channel factory to create channels for Consumer. - /// Should we make channels if they don't exist, defaults to creating - /// The configuration options for the subscriptions. - /// How long to pause when a channel is empty in milliseconds - /// How long to pause when there is a channel failure in milliseconds - public AzureServiceBusSubscription( - SubscriptionName? name = null, - ChannelName? channelName = null, - RoutingKey? routingKey = null, - int bufferSize = 1, - int noOfPerformers = 1, - TimeSpan? timeOut = null, - int requeueCount = -1, - TimeSpan? requeueDelay = null, - int unacceptableMessageLimit = 0, - bool isAsync = false, - IAmAChannelFactory? channelFactory = null, - OnMissingChannel makeChannels = OnMissingChannel.Create, - AzureServiceBusSubscriptionConfiguration? subscriptionConfiguration = null, - int emptyChannelDelay = 500, - int channelFailureDelay = 1000) - : base(typeof(T), name, channelName, routingKey, bufferSize, noOfPerformers, - timeOut, requeueCount, requeueDelay, unacceptableMessageLimit, isAsync, channelFactory, makeChannels, - subscriptionConfiguration, emptyChannelDelay, channelFailureDelay) - { - } } } diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusSubscriptionConfiguration.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusSubscriptionConfiguration.cs index fcbeec261c..90fceb265a 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusSubscriptionConfiguration.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusSubscriptionConfiguration.cs @@ -1,48 +1,73 @@ -using System; +#region Licence -namespace Paramore.Brighter.MessagingGateway.AzureServiceBus +/* 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; + +namespace Paramore.Brighter.MessagingGateway.AzureServiceBus; + +public class AzureServiceBusSubscriptionConfiguration { - public class AzureServiceBusSubscriptionConfiguration - { - /// - /// The Maximum amount of times that a Message can be delivered before it is dead Lettered - /// - public int MaxDeliveryCount { get; set; } = 5; + /// + /// The Maximum amount of times that a Message can be delivered before it is dead Lettered + /// + public int MaxDeliveryCount { get; set; } = 5; - /// - /// Dead letter a message when it expires - /// - public bool DeadLetteringOnMessageExpiration { get; set; } = true; + /// + /// Dead letter a message when it expires + /// + public bool DeadLetteringOnMessageExpiration { get; set; } = true; - /// - /// How long message locks are held for - /// - public TimeSpan LockDuration { get; set; } = TimeSpan.FromMinutes(1); + /// + /// How long message locks are held for + /// + public TimeSpan LockDuration { get; set; } = TimeSpan.FromMinutes(1); - /// - /// How long messages sit in the queue before they expire - /// - public TimeSpan DefaultMessageTimeToLive { get; set; } = TimeSpan.FromDays(3); - - /// - /// How long a queue is idle for before being deleted. - /// Default is TimeSpan.MaxValue. - /// - public TimeSpan QueueIdleBeforeDelete { get; set; } = TimeSpan.MaxValue; - - /// - /// Subscription is Session Enabled - /// - public bool RequireSession { get; set; } = false; - - /// - /// A Sql Filter to apply to the subscription - /// - public string SqlFilter = String.Empty; - - /// - /// Use a Service Bus Queue instead of a Topic - /// - public bool UseServiceBusQueue = false; - } + /// + /// How long messages sit in the queue before they expire + /// + public TimeSpan DefaultMessageTimeToLive { get; set; } = TimeSpan.FromDays(3); + + /// + /// How long a queue is idle for before being deleted. + /// Default is TimeSpan.MaxValue. + /// + public TimeSpan QueueIdleBeforeDelete { get; set; } = TimeSpan.MaxValue; + + /// + /// Subscription is Session Enabled + /// + public bool RequireSession { get; set; } = false; + + /// + /// A Sql Filter to apply to the subscription + /// + public string SqlFilter = String.Empty; + + /// + /// Use a Service Bus Queue instead of a Topic + /// + public bool UseServiceBusQueue = false; } diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusTopicConsumer.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusTopicConsumer.cs index b80258b704..4ea8ec77c2 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusTopicConsumer.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusTopicConsumer.cs @@ -1,117 +1,149 @@ -using System; +#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; +using System.Threading.Tasks; using Azure.Messaging.ServiceBus; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; using Paramore.Brighter.MessagingGateway.AzureServiceBus.AzureServiceBusWrappers; +using Paramore.Brighter.Tasks; -namespace Paramore.Brighter.MessagingGateway.AzureServiceBus +namespace Paramore.Brighter.MessagingGateway.AzureServiceBus; + +/// +/// Implementation of using Azure Service Bus for Transport. +/// +public class AzureServiceBusTopicConsumer : AzureServiceBusConsumer { + protected override ILogger Logger => s_logger; + + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + private bool _subscriptionCreated; + private readonly string _subscriptionName; + private readonly IServiceBusReceiverProvider _serviceBusReceiverProvider; + protected override string SubscriptionName => _subscriptionName; + /// - /// Implementation of using Azure Service Bus for Transport. + /// Initializes an Instance of for Service Bus Topics /// - public class AzureServiceBusTopicConsumer : AzureServiceBusConsumer + /// An Azure Service Bus Subscription. + /// An instance of the Messaging Producer used for Requeue. + /// An Instance of Administration Client Wrapper. + /// An Instance of . + public AzureServiceBusTopicConsumer( + AzureServiceBusSubscription subscription, + IAmAMessageProducer messageProducer, + IAdministrationClientWrapper administrationClientWrapper, + IServiceBusReceiverProvider serviceBusReceiverProvider) + : base(subscription, messageProducer, administrationClientWrapper) { - protected override ILogger Logger => s_logger; - - private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - private bool _subscriptionCreated; - private readonly string _subscriptionName; - private readonly IServiceBusReceiverProvider _serviceBusReceiverProvider; - protected override string SubscriptionName => _subscriptionName; - - /// - /// Initializes an Instance of for Service Bus Topics - /// - /// An Azure Service Bus Subscription. - /// An instance of the Messaging Producer used for Requeue. - /// An Instance of Administration Client Wrapper. - /// An Instance of . - public AzureServiceBusTopicConsumer(AzureServiceBusSubscription subscription, - IAmAMessageProducerSync messageProducerSync, - IAdministrationClientWrapper administrationClientWrapper, - IServiceBusReceiverProvider serviceBusReceiverProvider) : base(subscription, - messageProducerSync, administrationClientWrapper) - { - _subscriptionName = subscription.ChannelName.Value; - _serviceBusReceiverProvider = serviceBusReceiverProvider; - } + _subscriptionName = subscription.ChannelName.Value; + _serviceBusReceiverProvider = serviceBusReceiverProvider; + } + + /// + /// Purges the specified queue name. + /// + public override void Purge() => BrighterSynchronizationHelper.Run(async () => await PurgeAsync()); + + /// + /// Purges the specified queue name. + /// + public override async Task PurgeAsync(CancellationToken ct = default) + { + Logger.LogInformation("Purging messages from {Subscription} Subscription on Topic {Topic}", + SubscriptionName, Topic); - protected override void GetMessageReceiverProvider() + await AdministrationClientWrapper.DeleteTopicAsync(Topic); + await EnsureChannelAsync(); + } + + protected override async Task EnsureChannelAsync() + { + if (_subscriptionCreated || Subscription.MakeChannels.Equals(OnMissingChannel.Assume)) + return; + + try { - s_logger.LogInformation( - "Getting message receiver provider for topic {Topic} and subscription {ChannelName}...", - Topic, _subscriptionName); - try + if (await AdministrationClientWrapper.SubscriptionExistsAsync(Topic, _subscriptionName)) { - ServiceBusReceiver = _serviceBusReceiverProvider.Get(Topic, _subscriptionName, - SubscriptionConfiguration.RequireSession); + _subscriptionCreated = true; + return; } - catch (Exception e) + + if (Subscription.MakeChannels.Equals(OnMissingChannel.Validate)) { - s_logger.LogError(e, - "Failed to get message receiver provider for topic {Topic} and subscription {ChannelName}", - Topic, _subscriptionName); + throw new ChannelFailureException( + $"Subscription {_subscriptionName} does not exist on topic {Topic} and missing channel mode set to Validate."); } - } - - /// - /// Purges the specified queue name. - /// - public override void Purge() - { - Logger.LogInformation("Purging messages from {Subscription} Subscription on Topic {Topic}", - SubscriptionName, Topic); - AdministrationClientWrapper.DeleteTopicAsync(Topic); - EnsureChannel(); + await AdministrationClientWrapper.CreateSubscriptionAsync(Topic, _subscriptionName, SubscriptionConfiguration); + _subscriptionCreated = true; } - - protected override void EnsureChannel() + catch (ServiceBusException ex) { - if (_subscriptionCreated || Subscription.MakeChannels.Equals(OnMissingChannel.Assume)) - return; - - try + if (ex.Reason == ServiceBusFailureReason.MessagingEntityAlreadyExists) { - if (AdministrationClientWrapper.SubscriptionExists(Topic, _subscriptionName)) - { - _subscriptionCreated = true; - return; - } - - if (Subscription.MakeChannels.Equals(OnMissingChannel.Validate)) - { - throw new ChannelFailureException( - $"Subscription {_subscriptionName} does not exist on topic {Topic} and missing channel mode set to Validate."); - } - - AdministrationClientWrapper.CreateSubscription(Topic, _subscriptionName, - SubscriptionConfiguration); + s_logger.LogWarning( + "Message entity already exists with topic {Topic} and subscription {ChannelName}", Topic, + _subscriptionName); _subscriptionCreated = true; } - catch (ServiceBusException ex) + else { - if (ex.Reason == ServiceBusFailureReason.MessagingEntityAlreadyExists) - { - s_logger.LogWarning( - "Message entity already exists with topic {Topic} and subscription {ChannelName}", Topic, - _subscriptionName); - _subscriptionCreated = true; - } - else - { - throw new ChannelFailureException("Failing to check or create subscription", ex); - } + throw new ChannelFailureException("Failing to check or create subscription", ex); } - catch (Exception e) - { - s_logger.LogError(e, "Failing to check or create subscription"); + } + catch (Exception e) + { + s_logger.LogError(e, "Failing to check or create subscription"); - //The connection to Azure Service bus may have failed so we re-establish the connection. - AdministrationClientWrapper.Reset(); + //The connection to Azure Service bus may have failed so we re-establish the connection. + AdministrationClientWrapper.Reset(); - throw new ChannelFailureException("Failing to check or create subscription", e); - } + throw new ChannelFailureException("Failing to check or create subscription", e); + } + } + + protected override async Task GetMessageReceiverProviderAsync() + { + s_logger.LogInformation( + "Getting message receiver provider for topic {Topic} and subscription {ChannelName}...", + Topic, _subscriptionName); + try + { + ServiceBusReceiver = await _serviceBusReceiverProvider.GetAsync(Topic, _subscriptionName, + SubscriptionConfiguration.RequireSession); + } + catch (Exception e) + { + s_logger.LogError(e, + "Failed to get message receiver provider for topic {Topic} and subscription {ChannelName}", + Topic, _subscriptionName); } } } diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusTopicMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusTopicMessageProducer.cs index 383b3b3106..6984ff9ccf 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusTopicMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusTopicMessageProducer.cs @@ -24,68 +24,68 @@ THE SOFTWARE. */ #endregion using System; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; using Paramore.Brighter.MessagingGateway.AzureServiceBus.AzureServiceBusWrappers; -namespace Paramore.Brighter.MessagingGateway.AzureServiceBus +namespace Paramore.Brighter.MessagingGateway.AzureServiceBus; + +/// +/// A Sync and Async Message Producer for Azure Service Bus. +/// +public class AzureServiceBusTopicMessageProducer : AzureServiceBusMessageProducer { + protected override ILogger Logger => s_logger; + + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + + private readonly IAdministrationClientWrapper _administrationClientWrapper; + /// - /// A Sync and Async Message Producer for Azure Service Bus. + /// An Azure Service Bus Message producer /// - public class AzureServiceBusTopicMessageProducer : AzureServiceBusMessageProducer + /// The administrative client. + /// The provider to use when producing messages. + /// Configuration of a producer + /// When sending more than one message using the MessageProducer, the max amount to send in a single transmission. + public AzureServiceBusTopicMessageProducer( + IAdministrationClientWrapper administrationClientWrapper, + IServiceBusSenderProvider serviceBusSenderProvider, + AzureServiceBusPublication publication, + int bulkSendBatchSize = 10 + ) : base(serviceBusSenderProvider, publication, bulkSendBatchSize) { - protected override ILogger Logger => s_logger; - - private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - - private readonly IAdministrationClientWrapper _administrationClientWrapper; + _administrationClientWrapper = administrationClientWrapper; + } - /// - /// An Azure Service Bus Message producer - /// - /// The administrative client. - /// The provider to use when producing messages. - /// Configuration of a producer - /// When sending more than one message using the MessageProducer, the max amount to send in a single transmission. - public AzureServiceBusTopicMessageProducer( - IAdministrationClientWrapper administrationClientWrapper, - IServiceBusSenderProvider serviceBusSenderProvider, - AzureServiceBusPublication publication, - int bulkSendBatchSize = 10 - ) : base(serviceBusSenderProvider, publication, bulkSendBatchSize) - { - _administrationClientWrapper = administrationClientWrapper; - } + protected override async Task EnsureChannelExistsAsync(string channelName) + { + if (TopicCreated || Publication.MakeChannels.Equals(OnMissingChannel.Assume)) + return; - protected override void EnsureChannelExists(string channelName) + try { - if (TopicCreated || Publication.MakeChannels.Equals(OnMissingChannel.Assume)) - return; - - try + if (await _administrationClientWrapper.TopicExistsAsync(channelName)) { - if (_administrationClientWrapper.TopicExists(channelName)) - { - TopicCreated = true; - return; - } - - if (Publication.MakeChannels.Equals(OnMissingChannel.Validate)) - { - throw new ChannelFailureException($"Topic {channelName} does not exist and missing channel mode set to Validate."); - } - - _administrationClientWrapper.CreateTopic(channelName); TopicCreated = true; + return; } - catch (Exception e) + + if (Publication.MakeChannels.Equals(OnMissingChannel.Validate)) { - //The connection to Azure Service bus may have failed so we re-establish the connection. - _administrationClientWrapper.Reset(); - s_logger.LogError(e, "Failing to check or create topic"); - throw; + throw new ChannelFailureException($"Topic {channelName} does not exist and missing channel mode set to Validate."); } + + await _administrationClientWrapper.CreateTopicAsync(channelName); + TopicCreated = true; + } + catch (Exception e) + { + //The connection to Azure Service bus may have failed so we re-establish the connection. + _administrationClientWrapper.Reset(); + s_logger.LogError(e, "Failing to check or create topic"); + throw; } } } diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/AdministrationClientWrapper.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/AdministrationClientWrapper.cs index 9f7646e260..88bd65a1a5 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/AdministrationClientWrapper.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/AdministrationClientWrapper.cs @@ -1,10 +1,34 @@ -using System; +#region Licence +/* The MIT License (MIT) +Copyright © 2024 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; using System.Threading.Tasks; using Azure.Messaging.ServiceBus.Administration; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; using Paramore.Brighter.MessagingGateway.AzureServiceBus.ClientProvider; +using Paramore.Brighter.Tasks; namespace Paramore.Brighter.MessagingGateway.AzureServiceBus.AzureServiceBusWrappers { @@ -35,114 +59,92 @@ public void Reset() s_logger.LogWarning("Resetting management client wrapper..."); Initialise(); } - + /// - /// Check if a Topic exists + /// Create a Queue + /// Sync over async but alright in the context of creating a queue /// - /// The name of the Topic. - /// True if the Topic exists. - public bool TopicExists(string topicName) + /// The name of the Queue + /// Number of minutes before an ideal queue will be deleted + public async Task CreateQueueAsync(string queueName, TimeSpan? autoDeleteOnIdle = null) { - s_logger.LogDebug("Checking if topic {Topic} exists...", topicName); - - bool result; + s_logger.LogInformation("Creating topic {Topic}...", queueName); try { - result = _administrationClient.TopicExistsAsync(topicName).GetAwaiter().GetResult(); + await _administrationClient.CreateQueueAsync(new CreateQueueOptions(queueName) + { + AutoDeleteOnIdle = autoDeleteOnIdle ?? TimeSpan.MaxValue + }); } catch (Exception e) { - s_logger.LogError(e,"Failed to check if topic {Topic} exists", topicName); + s_logger.LogError(e, "Failed to create queue {Queue}.", queueName); throw; } - if (result) - { - s_logger.LogDebug("Topic {Topic} exists", topicName); - } - else - { - s_logger.LogWarning("Topic {Topic} does not exist", topicName); - } - - return result; + s_logger.LogInformation("Queue {Queue} created.", queueName); } - + /// - /// Check if a Queue exists + /// Create a Subscription. + /// Sync over Async but alright in the context of creating a subscription /// - /// The name of the Queue. - /// True if the Queue exists. - public bool QueueExists(string queueName) + /// The name of the Topic. + /// The name of the Subscription. + /// The configuration options for the subscriptions. + public async Task CreateSubscriptionAsync(string topicName, string subscriptionName, AzureServiceBusSubscriptionConfiguration subscriptionConfiguration) { - s_logger.LogDebug("Checking if queue {Queue} exists...", queueName); - - bool result; + s_logger.LogInformation("Creating subscription {ChannelName} for topic {Topic}...", subscriptionName, topicName); - try + if (!await TopicExistsAsync(topicName)) { - result = _administrationClient.QueueExistsAsync(queueName).GetAwaiter().GetResult(); - } - catch (Exception e) - { - s_logger.LogError(e,"Failed to check if queue {Queue} exists", queueName); - throw; + await CreateTopicAsync(topicName, subscriptionConfiguration.QueueIdleBeforeDelete); } - if (result) - { - s_logger.LogDebug("Queue {Queue} exists", queueName); - } - else + var subscriptionOptions = new CreateSubscriptionOptions(topicName, subscriptionName) { - s_logger.LogWarning("Queue {Queue} does not exist", queueName); - } - - return result; - } + MaxDeliveryCount = subscriptionConfiguration.MaxDeliveryCount, + DeadLetteringOnMessageExpiration = subscriptionConfiguration.DeadLetteringOnMessageExpiration, + LockDuration = subscriptionConfiguration.LockDuration, + DefaultMessageTimeToLive = subscriptionConfiguration.DefaultMessageTimeToLive, + AutoDeleteOnIdle = subscriptionConfiguration.QueueIdleBeforeDelete, + RequiresSession = subscriptionConfiguration.RequireSession + }; - /// - /// Create a Queue - /// - /// The name of the Queue - /// Number of minutes before an ideal queue will be deleted - public void CreateQueue(string queueName, TimeSpan? autoDeleteOnIdle = null) - { - s_logger.LogInformation("Creating topic {Topic}...", queueName); + var ruleOptions = string.IsNullOrEmpty(subscriptionConfiguration.SqlFilter) + ? new CreateRuleOptions() : new CreateRuleOptions("sqlFilter",new SqlRuleFilter(subscriptionConfiguration.SqlFilter)); try { - _administrationClient - .CreateQueueAsync(new CreateQueueOptions(queueName) - { - AutoDeleteOnIdle = autoDeleteOnIdle ?? TimeSpan.MaxValue - }).GetAwaiter().GetResult(); + await _administrationClient.CreateSubscriptionAsync(subscriptionOptions, ruleOptions); } catch (Exception e) { - s_logger.LogError(e, "Failed to create queue {Queue}.", queueName); + s_logger.LogError(e, "Failed to create subscription {ChannelName} for topic {Topic}.", subscriptionName, topicName); throw; } - s_logger.LogInformation("Queue {Queue} created.", queueName); + s_logger.LogInformation("Subscription {ChannelName} for topic {Topic} created.", subscriptionName, topicName); } + /// /// Create a Topic + /// Sync over async but runs in the context of creating a topic /// /// The name of the Topic /// Number of minutes before an ideal queue will be deleted - public void CreateTopic(string topicName, TimeSpan? autoDeleteOnIdle = null) + public async Task CreateTopicAsync(string topicName, TimeSpan? autoDeleteOnIdle = null) { s_logger.LogInformation("Creating topic {Topic}...", topicName); try { - _administrationClient.CreateTopicAsync(new CreateTopicOptions(topicName) + await _administrationClient.CreateTopicAsync(new CreateTopicOptions(topicName) { AutoDeleteOnIdle = autoDeleteOnIdle ?? TimeSpan.MaxValue - }).GetAwaiter().GetResult(); + }); } catch (Exception e) { @@ -153,6 +155,7 @@ public void CreateTopic(string topicName, TimeSpan? autoDeleteOnIdle = null) s_logger.LogInformation("Topic {Topic} created.", topicName); } + /// /// Delete a Queue /// @@ -188,6 +191,52 @@ public async Task DeleteTopicAsync(string topicName) s_logger.LogError(e, "Failed to delete Topic {Topic}", topicName); } } + + /// + /// GetAsync a Subscription. + /// + /// The name of the Topic. + /// The name of the Subscription. + /// The Cancellation Token. + public async Task GetSubscriptionAsync(string topicName, string subscriptionName, + CancellationToken cancellationToken = default) + { + return await _administrationClient.GetSubscriptionAsync(topicName, subscriptionName, cancellationToken); + } + + /// + /// Check if a Queue exists + /// Sync over async but runs in the context of checking queue existence + /// + /// The name of the Queue. + /// True if the Queue exists. + public async Task QueueExistsAsync(string queueName) + { + s_logger.LogDebug("Checking if queue {Queue} exists...", queueName); + + bool result; + + try + { + result = await _administrationClient.QueueExistsAsync(queueName); + } + catch (Exception e) + { + s_logger.LogError(e,"Failed to check if queue {Queue} exists", queueName); + throw; + } + + if (result) + { + s_logger.LogDebug("Queue {Queue} exists", queueName); + } + else + { + s_logger.LogWarning("Queue {Queue} does not exist", queueName); + } + + return result; + } /// /// Check if a Subscription Exists for a Topic. @@ -195,7 +244,7 @@ public async Task DeleteTopicAsync(string topicName) /// The name of the Topic. /// The name of the Subscription /// True if the subscription exists on the specified Topic. - public bool SubscriptionExists(string topicName, string subscriptionName) + public async Task SubscriptionExistsAsync(string topicName, string subscriptionName) { s_logger.LogDebug("Checking if subscription {ChannelName} for topic {Topic} exists...", subscriptionName, topicName); @@ -203,7 +252,7 @@ public bool SubscriptionExists(string topicName, string subscriptionName) try { - result =_administrationClient.SubscriptionExistsAsync(topicName, subscriptionName).Result; + result = await _administrationClient.SubscriptionExistsAsync(topicName, subscriptionName); } catch (Exception e) { @@ -224,78 +273,54 @@ public bool SubscriptionExists(string topicName, string subscriptionName) } /// - /// Create a Subscription. - /// - /// The name of the Topic. - /// The name of the Subscription. - /// The configuration options for the subscriptions. - public void CreateSubscription(string topicName, string subscriptionName, AzureServiceBusSubscriptionConfiguration subscriptionConfiguration) - { - CreateSubscriptionAsync(topicName, subscriptionName, subscriptionConfiguration).Wait(); - } - - /// - /// Get a Subscription. + /// Check if a Topic exists + /// Sync over async but alright in the context of checking topic existence /// /// The name of the Topic. - /// The name of the Subscription. - /// The Cancellation Token. - public async Task GetSubscriptionAsync(string topicName, string subscriptionName, - CancellationToken cancellationToken = default) + /// True if the Topic exists. + public async Task TopicExistsAsync(string topicName) { - return await _administrationClient.GetSubscriptionAsync(topicName, subscriptionName, cancellationToken); - } + s_logger.LogDebug("Checking if topic {Topic} exists...", topicName); - private void Initialise() - { - s_logger.LogDebug("Initialising new management client wrapper..."); + bool result; try { - _administrationClient = _clientProvider.GetServiceBusAdministrationClient(); + result = await _administrationClient.TopicExistsAsync(topicName); } catch (Exception e) { - s_logger.LogError(e,"Failed to initialise new management client wrapper."); + s_logger.LogError(e,"Failed to check if topic {Topic} exists", topicName); throw; } - s_logger.LogDebug("New management client wrapper initialised."); - } - - private async Task CreateSubscriptionAsync(string topicName, string subscriptionName, AzureServiceBusSubscriptionConfiguration subscriptionConfiguration) - { - s_logger.LogInformation("Creating subscription {ChannelName} for topic {Topic}...", subscriptionName, topicName); - - if (!TopicExists(topicName)) + if (result) { - CreateTopic(topicName, subscriptionConfiguration.QueueIdleBeforeDelete); + s_logger.LogDebug("Topic {Topic} exists", topicName); } - - var subscriptionOptions = new CreateSubscriptionOptions(topicName, subscriptionName) + else { - MaxDeliveryCount = subscriptionConfiguration.MaxDeliveryCount, - DeadLetteringOnMessageExpiration = subscriptionConfiguration.DeadLetteringOnMessageExpiration, - LockDuration = subscriptionConfiguration.LockDuration, - DefaultMessageTimeToLive = subscriptionConfiguration.DefaultMessageTimeToLive, - AutoDeleteOnIdle = subscriptionConfiguration.QueueIdleBeforeDelete, - RequiresSession = subscriptionConfiguration.RequireSession - }; + s_logger.LogWarning("Topic {Topic} does not exist", topicName); + } - var ruleOptions = string.IsNullOrEmpty(subscriptionConfiguration.SqlFilter) - ? new CreateRuleOptions() : new CreateRuleOptions("sqlFilter",new SqlRuleFilter(subscriptionConfiguration.SqlFilter)); + return result; + } + + private void Initialise() + { + s_logger.LogDebug("Initialising new management client wrapper..."); try { - await _administrationClient.CreateSubscriptionAsync(subscriptionOptions, ruleOptions); + _administrationClient = _clientProvider.GetServiceBusAdministrationClient(); } catch (Exception e) { - s_logger.LogError(e, "Failed to create subscription {ChannelName} for topic {Topic}.", subscriptionName, topicName); + s_logger.LogError(e,"Failed to initialise new management client wrapper."); throw; } - s_logger.LogInformation("Subscription {ChannelName} for topic {Topic} created.", subscriptionName, topicName); + s_logger.LogDebug("New management client wrapper initialised."); } } } diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/IAdministrationClientWrapper.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/IAdministrationClientWrapper.cs index 2849bf3d3f..5a2efcd93f 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/IAdministrationClientWrapper.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/IAdministrationClientWrapper.cs @@ -15,28 +15,28 @@ public interface IAdministrationClientWrapper /// /// The name of the Topic. /// True if the Topic exists. - bool TopicExists(string topicName); + Task TopicExistsAsync(string topicName); /// /// Check if a Queue exists /// /// The name of the Queue. /// True if the Queue exists. - bool QueueExists(string queueName); + Task QueueExistsAsync(string queueName); /// /// Create a Queue /// /// The name of the Queue /// Number of minutes before an ideal queue will be deleted - void CreateQueue(string queueName, TimeSpan? autoDeleteOnIdle = null); + Task CreateQueueAsync(string queueName, TimeSpan? autoDeleteOnIdle = null); /// /// Create a Topic /// /// The name of the Topic /// Number of minutes before an ideal queue will be deleted - void CreateTopic(string topicName, TimeSpan? autoDeleteOnIdle = null); + Task CreateTopicAsync(string topicName, TimeSpan? autoDeleteOnIdle = null); /// /// Delete a Queue @@ -56,7 +56,7 @@ public interface IAdministrationClientWrapper /// The name of the Topic. /// The name of the Subscription /// True if the subscription exists on the specified Topic. - bool SubscriptionExists(string topicName, string subscriptionName); + Task SubscriptionExistsAsync(string topicName, string subscriptionName); /// /// Create a Subscription. @@ -64,7 +64,7 @@ public interface IAdministrationClientWrapper /// The name of the Topic. /// The name of the Subscription. /// The configuration options for the subscriptions. - void CreateSubscription(string topicName, string subscriptionName, AzureServiceBusSubscriptionConfiguration subscriptionConfiguration); + Task CreateSubscriptionAsync(string topicName, string subscriptionName, AzureServiceBusSubscriptionConfiguration subscriptionConfiguration); /// /// Reset the Connection. @@ -72,7 +72,7 @@ public interface IAdministrationClientWrapper void Reset(); /// - /// Get a Subscription. + /// GetAsync a Subscription. /// /// The name of the Topic. /// The name of the Subscription. diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/IServiceBusReceiverProvider.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/IServiceBusReceiverProvider.cs index 0595fa85c4..89dbf3bcf5 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/IServiceBusReceiverProvider.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/IServiceBusReceiverProvider.cs @@ -1,4 +1,6 @@ -namespace Paramore.Brighter.MessagingGateway.AzureServiceBus.AzureServiceBusWrappers +using System.Threading.Tasks; + +namespace Paramore.Brighter.MessagingGateway.AzureServiceBus.AzureServiceBusWrappers { /// /// Interface for a Provider to provide @@ -11,8 +13,8 @@ public interface IServiceBusReceiverProvider /// The name of the Topic. /// Use Sessions for Processing /// A ServiceBusReceiverWrapper. - IServiceBusReceiverWrapper? Get(string queueName,bool sessionEnabled); - + Task GetAsync(string queueName, bool sessionEnabled); + /// /// Gets a for a Service Bus Topic /// @@ -20,6 +22,6 @@ public interface IServiceBusReceiverProvider /// The name of the Subscription on the Topic. /// Use Sessions for Processing /// A ServiceBusReceiverWrapper. - IServiceBusReceiverWrapper? Get(string topicName, string subscriptionName, bool sessionEnabled); + Task GetAsync(string topicName, string subscriptionName, bool sessionEnabled); } } diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/IServiceBusReceiverWrapper.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/IServiceBusReceiverWrapper.cs index 39fa9e3da8..cabe0b0955 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/IServiceBusReceiverWrapper.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/IServiceBusReceiverWrapper.cs @@ -10,31 +10,37 @@ namespace Paramore.Brighter.MessagingGateway.AzureServiceBus.AzureServiceBusWrap public interface IServiceBusReceiverWrapper { /// - /// Receive a batch of messages. + /// ReceiveAsync a batch of messages. /// /// The size of the batch to receive. /// Time to wait. /// - Task> Receive(int batchSize, TimeSpan serverWaitTime); + Task> ReceiveAsync(int batchSize, TimeSpan serverWaitTime); /// - /// Complete (Acknowledge) a Message. + /// CompleteAsync (Acknowledge) a Message. /// /// The Lock Token the message was provider with. - Task Complete(string lockToken); + Task CompleteAsync(string lockToken); /// /// Send a message to the Dead Letter Queue. /// /// The Lock Token the message was provider with. /// - Task DeadLetter(string lockToken); + Task DeadLetterAsync(string lockToken); /// /// Close the connection. /// void Close(); + /// + /// Closes the connection asynchronously. + /// + /// + Task CloseAsync(); + /// /// Is the connection currently closed. /// diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/IServiceBusSenderProvider.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/IServiceBusSenderProvider.cs index cd7d945c30..5e6d87e4b2 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/IServiceBusSenderProvider.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/IServiceBusSenderProvider.cs @@ -6,7 +6,7 @@ public interface IServiceBusSenderProvider { /// - /// Get a ServiceBusSenderWrapper for a Topic. + /// GetAsync a ServiceBusSenderWrapper for a Topic. /// /// The name of the Topic. /// A ServiceBusSenderWrapper for the given Topic. diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/ServiceBusReceiverProvider.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/ServiceBusReceiverProvider.cs index 543d070579..0c2f982faf 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/ServiceBusReceiverProvider.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/ServiceBusReceiverProvider.cs @@ -1,31 +1,51 @@ -using Azure.Messaging.ServiceBus; +#region Licence +/* The MIT License (MIT) +Copyright © 2024 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.Threading.Tasks; +using Azure.Messaging.ServiceBus; using Paramore.Brighter.MessagingGateway.AzureServiceBus.ClientProvider; namespace Paramore.Brighter.MessagingGateway.AzureServiceBus.AzureServiceBusWrappers { - internal class ServiceBusReceiverProvider : IServiceBusReceiverProvider + internal class ServiceBusReceiverProvider(IServiceBusClientProvider clientProvider) : IServiceBusReceiverProvider { - private readonly ServiceBusClient _client; + private readonly ServiceBusClient _client = clientProvider.GetServiceBusClient(); - public ServiceBusReceiverProvider(IServiceBusClientProvider clientProvider) - { - _client = clientProvider.GetServiceBusClient(); - } - /// /// Gets a for a Service Bus Queue + /// Sync over async used here, alright in the context of receiver creation /// /// The name of the Topic. /// Use Sessions for Processing /// A ServiceBusReceiverWrapper. - public IServiceBusReceiverWrapper? Get(string queueName, bool sessionEnabled) + public async Task GetAsync(string queueName, bool sessionEnabled) { if (sessionEnabled) { try { - return new ServiceBusReceiverWrapper(_client.AcceptNextSessionAsync(queueName, - new ServiceBusSessionReceiverOptions() {ReceiveMode = ServiceBusReceiveMode.PeekLock}).GetAwaiter().GetResult()); + return new ServiceBusReceiverWrapper(await _client.AcceptNextSessionAsync(queueName, + new ServiceBusSessionReceiverOptions() {ReceiveMode = ServiceBusReceiveMode.PeekLock})); } catch (ServiceBusException e) { @@ -47,19 +67,20 @@ public ServiceBusReceiverProvider(IServiceBusClientProvider clientProvider) /// /// Gets a for a Service Bus Topic + /// Sync over async used here, alright in the context of receiver creation /// /// The name of the Topic. /// The name of the Subscription on the Topic. /// Use Sessions for Processing /// A ServiceBusReceiverWrapper. - public IServiceBusReceiverWrapper? Get(string topicName, string subscriptionName, bool sessionEnabled) + public async Task GetAsync(string topicName, string subscriptionName, bool sessionEnabled) { if (sessionEnabled) { try { - return new ServiceBusReceiverWrapper(_client.AcceptNextSessionAsync(topicName, subscriptionName, - new ServiceBusSessionReceiverOptions() {ReceiveMode = ServiceBusReceiveMode.PeekLock}).GetAwaiter().GetResult()); + return new ServiceBusReceiverWrapper(await _client.AcceptNextSessionAsync(topicName, subscriptionName, + new ServiceBusSessionReceiverOptions() {ReceiveMode = ServiceBusReceiveMode.PeekLock})); } catch (ServiceBusException e) { diff --git a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/ServiceBusReceiverWrapper.cs b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/ServiceBusReceiverWrapper.cs index 36dd3f6330..e875099bb7 100644 --- a/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/ServiceBusReceiverWrapper.cs +++ b/src/Paramore.Brighter.MessagingGateway.AzureServiceBus/AzureServiceBusWrappers/ServiceBusReceiverWrapper.cs @@ -1,4 +1,27 @@ -using System; +#region Licence +/* The MIT License (MIT) +Copyright © 2024 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; @@ -8,17 +31,30 @@ namespace Paramore.Brighter.MessagingGateway.AzureServiceBus.AzureServiceBusWrappers { + /// + /// Wraps the to provide additional functionality. + /// internal class ServiceBusReceiverWrapper : IServiceBusReceiverWrapper { private readonly ServiceBusReceiver _messageReceiver; private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + /// + /// Initializes a new instance of the class. + /// + /// The to wrap. public ServiceBusReceiverWrapper(ServiceBusReceiver messageReceiver) { _messageReceiver = messageReceiver; } - public async Task> Receive(int batchSize, TimeSpan serverWaitTime) + /// + /// Receives a batch of messages from the Service Bus. + /// + /// The number of messages to receive. + /// The maximum time to wait for the messages. + /// A task that represents the asynchronous receive operation. The task result contains the received messages. + public async Task> ReceiveAsync(int batchSize, TimeSpan serverWaitTime) { var messages = await _messageReceiver.ReceiveMessagesAsync(batchSize, serverWaitTime).ConfigureAwait(false); @@ -29,26 +65,53 @@ public async Task> Receive(int batchSize, T return messages.Select(x => new BrokeredMessageWrapper(x)); } + /// + /// Closes the message receiver connection. + /// public void Close() { s_logger.LogWarning("Closing the MessageReceiver connection"); _messageReceiver.CloseAsync().GetAwaiter().GetResult(); s_logger.LogWarning("MessageReceiver connection stopped"); } + + public async Task CloseAsync() + { + s_logger.LogWarning("Closing the MessageReceiver connection"); + await _messageReceiver.CloseAsync().ConfigureAwait(false); + s_logger.LogWarning("MessageReceiver connection stopped"); + } - public Task Complete(string lockToken) + /// + /// Completes the message processing. + /// + /// The lock token of the message to complete. + /// A task that represents the asynchronous complete operation. + public Task CompleteAsync(string lockToken) { return _messageReceiver.CompleteMessageAsync(CreateMessageShiv(lockToken)); } - public Task DeadLetter(string lockToken) + /// + /// Deadletters the message. + /// + /// The lock token of the message to deadletter. + /// A task that represents the asynchronous deadletter operation. + public Task DeadLetterAsync(string lockToken) { return _messageReceiver.DeadLetterMessageAsync(CreateMessageShiv(lockToken)); } + /// + /// Gets a value indicating whether the message receiver is closed or closing. + /// public bool IsClosedOrClosing => _messageReceiver.IsClosed; - + /// + /// Creates a with the specified lock token. + /// + /// The lock token of the message. + /// A with the specified lock token. private ServiceBusReceivedMessage CreateMessageShiv(string lockToken) { return ServiceBusModelFactory.ServiceBusReceivedMessage(lockTokenGuid: Guid.Parse(lockToken)); diff --git a/src/Paramore.Brighter.MessagingGateway.Kafka/ChannelFactory.cs b/src/Paramore.Brighter.MessagingGateway.Kafka/ChannelFactory.cs index 299950fd36..444de83bc0 100644 --- a/src/Paramore.Brighter.MessagingGateway.Kafka/ChannelFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.Kafka/ChannelFactory.cs @@ -21,40 +21,84 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #endregion -namespace Paramore.Brighter.MessagingGateway.Kafka +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter.MessagingGateway.Kafka; + +/// +/// Factory class for creating Kafka channels. +/// +public class ChannelFactory : IAmAChannelFactory { + private readonly KafkaMessageConsumerFactory _kafkaMessageConsumerFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The factory for creating Kafka message consumers. + public ChannelFactory(KafkaMessageConsumerFactory kafkaMessageConsumerFactory) + { + _kafkaMessageConsumerFactory = kafkaMessageConsumerFactory; + } + + /// + /// Creates a synchronous Kafka channel. + /// + /// The subscription details for the channel. + /// A synchronous Kafka channel instance. + /// Thrown when the subscription is not a KafkaSubscription. + public IAmAChannelSync CreateSyncChannel(Subscription subscription) + { + KafkaSubscription rmqSubscription = subscription as KafkaSubscription; + if (rmqSubscription == null) + throw new ConfigurationException("We expect a KafkaSubscription or KafkaSubscription as a parameter"); + + return new Channel( + subscription.ChannelName, + subscription.RoutingKey, + _kafkaMessageConsumerFactory.Create(subscription), + subscription.BufferSize); + } + /// - /// Abstracts a Kafka channel. A channel is a logically addressable pipe. + /// Creates an asynchronous Kafka channel. /// - public class ChannelFactory : IAmAChannelFactory + /// The subscription details for the channel. + /// An asynchronous Kafka channel instance. + /// Thrown when the subscription is not a KafkaSubscription. + public IAmAChannelAsync CreateAsyncChannel(Subscription subscription) { - private readonly KafkaMessageConsumerFactory _kafkaMessageConsumerFactory; - - /// - /// Initializes a new instance of the class. - /// - /// The messageConsumerFactory. - public ChannelFactory(KafkaMessageConsumerFactory kafkaMessageConsumerFactory) - { - _kafkaMessageConsumerFactory = kafkaMessageConsumerFactory; - } - - /// - /// Creates the input channel - /// - /// The subscription parameters with which to create the channel - /// - public IAmAChannel CreateChannel(Subscription subscription) - { - KafkaSubscription rmqSubscription = subscription as KafkaSubscription; - if (rmqSubscription == null) - throw new ConfigurationException("We expect an KafkaSubscription or KafkaSubscription as a parameter"); - - return new Channel( - subscription.ChannelName, - subscription.RoutingKey, - _kafkaMessageConsumerFactory.Create(subscription), - subscription.BufferSize); - } + KafkaSubscription rmqSubscription = subscription as KafkaSubscription; + if (rmqSubscription == null) + throw new ConfigurationException("We expect a KafkaSubscription or KafkaSubscription as a parameter"); + + return new ChannelAsync( + subscription.ChannelName, + subscription.RoutingKey, + _kafkaMessageConsumerFactory.CreateAsync(subscription), + subscription.BufferSize); + } + + /// + /// Asynchronously creates an asynchronous Kafka channel. + /// + /// The subscription details for the channel. + /// A token to cancel the operation. + /// A task representing the asynchronous operation, with an asynchronous Kafka channel instance as the result. + /// Thrown when the subscription is not a KafkaSubscription. + public Task CreateAsyncChannelAsync(Subscription subscription, CancellationToken ct = default) + { + KafkaSubscription rmqSubscription = subscription as KafkaSubscription; + if (rmqSubscription == null) + throw new ConfigurationException("We expect a KafkaSubscription or KafkaSubscription as a parameter"); + + IAmAChannelAsync channel = new ChannelAsync( + subscription.ChannelName, + subscription.RoutingKey, + _kafkaMessageConsumerFactory.CreateAsync(subscription), + subscription.BufferSize); + + return Task.FromResult(channel); } } diff --git a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageConsumer.cs b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageConsumer.cs index d4a0233592..6c156b441e 100644 --- a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageConsumer.cs +++ b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageConsumer.cs @@ -33,9 +33,9 @@ THE SOFTWARE. */ namespace Paramore.Brighter.MessagingGateway.Kafka { - /// + /// /// - /// Class KafkaMessageConsumer is an implementation of + /// Class KafkaMessageConsumer is an implementation of /// and provides the facilities to consume messages from a Kafka broker for a topic /// in a consumer group. /// A Kafka Message Consumer can create topics, depending on the options chosen. @@ -44,7 +44,7 @@ namespace Paramore.Brighter.MessagingGateway.Kafka /// This dual strategy prevents low traffic topics having batches that are 'pending' for long periods, causing a risk that the consumer /// will end before committing its offsets. /// - public class KafkaMessageConsumer : KafkaMessagingGateway, IAmAMessageConsumer + public class KafkaMessageConsumer : KafkaMessagingGateway, IAmAMessageConsumerSync, IAmAMessageConsumerAsync { private IConsumer _consumer; private readonly KafkaMessageCreator _creator; @@ -56,7 +56,6 @@ public class KafkaMessageConsumer : KafkaMessagingGateway, IAmAMessageConsumer private DateTime _lastFlushAt = DateTime.UtcNow; private readonly TimeSpan _sweepUncommittedInterval; private readonly SemaphoreSlim _flushToken = new(1, 1); - private bool _disposedValue; private bool _hasFatalError; /// @@ -222,12 +221,14 @@ public KafkaMessageConsumer( /// /// Acknowledges the specified message. + /// + /// /// We do not have autocommit on and this stores the message that has just been processed. /// We use the header bag to store the partition offset of the message when reading it from Kafka. This enables us to get hold of it when /// we acknowledge the message via Brighter. We store the offset via the consumer, and keep an in-memory list of offsets. If we have hit the /// batch size we commit the offsets. if not, we trigger the sweeper, which will commit the offset once the specified time interval has passed if /// a batch has not done so. - /// + /// /// The message. public void Acknowledge(Message message) { @@ -237,7 +238,12 @@ public void Acknowledge(Message message) try { var topicPartitionOffset = bagData as TopicPartitionOffset; - + if (topicPartitionOffset == null) + { + s_logger.LogInformation("Cannot acknowledge message {MessageId} as no offset data", message.Id); + return; + } + var offset = new TopicPartitionOffset(topicPartitionOffset.TopicPartition, new Offset(topicPartitionOffset.Offset + 1)); s_logger.LogInformation("Storing offset {Offset} to topic {Topic} for partition {ChannelName}", @@ -261,10 +267,32 @@ public void Acknowledge(Message message) } } - /// - /// There is no 'queue' to purge in Kafka, so we treat this as moving past to the offset to tne end of any assigned partitions, - /// thus skipping over anything that exists at that point. + /// + /// Acknowledges the specified message. /// + /// + /// We do not have autocommit on and this stores the message that has just been processed. + /// We use the header bag to store the partition offset of the message when reading it from Kafka. This enables us to get hold of it when + /// we acknowledge the message via Brighter. We store the offset via the consumer, and keep an in-memory list of offsets. If we have hit the + /// batch size we commit the offsets. if not, we trigger the sweeper, which will commit the offset once the specified time interval has passed if + /// a batch has not done so. + /// This just calls the sync method, which does not block + /// + /// The message. + /// A cancellation token - not used as calls the sync method which does not block + public Task AcknowledgeAsync(Message message, CancellationToken cancellationToken = default(CancellationToken)) + { + Acknowledge(message); + return Task.CompletedTask; + } + + /// + /// Purges the specified queue name. + /// + /// + /// There is no 'queue' to purge in Kafka, so we treat this as moving past to the offset to tne end of any assigned partitions, + /// thus skipping over anything that exists at that point. + /// public void Purge() { if (!_consumer.Assignment.Any()) @@ -275,12 +303,30 @@ public void Purge() _consumer.Seek(new TopicPartitionOffset(topicPartition, Offset.End)); } } + + /// + /// Purges the specified queue name. + /// + /// + /// There is no 'queue' to purge in Kafka, so we treat this as moving past to the offset to tne end of any assigned partitions, + /// thus skipping over anything that exists at that point. + /// As the Confluent library does not support async, this is sync over async and would block the main performer thread + /// so we use a new thread pool thread to run this and await that. This could lead to thread pool exhaustion but Purge is rarely used + /// in production code + /// + /// + public Task PurgeAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.Run(this.Purge, cancellationToken); + } /// /// Receives from the specified topic. Used by a to provide access to the stream. + /// + /// /// We consume the next offset from the stream, and turn it into a Brighter message; we store the offset in the partition into the Brighter message /// headers for use in storing and committing offsets. If the stream is EOF or we are not allocated partitions, returns an empty message. - /// + /// /// The timeout for receiving a message. Defaults to 300ms /// A Brighter message wrapping the payload from the Kafka stream /// We catch Kafka consumer errors and rethrow as a ChannelFailureException @@ -346,16 +392,52 @@ public Message[] Receive(TimeSpan? timeOut = null) } } + /// + /// Receives from the specified topic. Used by a to provide access to the stream. + /// + /// + /// We consume the next offset from the stream, and turn it into a Brighter message; we store the offset in the partition into the Brighter message + /// headers for use in storing and committing offsets. If the stream is EOF or we are not allocated partitions, returns an empty message. + /// Kafka does not support an async consumer, and probably never will. See Confluent Kafka + /// As a result we use Task.Run to encapsulate the call. This will cost a thread, and be slower than the sync version. However, given our pump characeristics this would not result + /// in thread pool exhaustion. + /// + /// The timeout for receiving a message. Defaults to 300ms + /// The cancellation token - not used as this is async over sync + /// A Brighter message wrapping the payload from the Kafka stream + /// We catch Kafka consumer errors and rethrow as a ChannelFailureException + public async Task ReceiveAsync(TimeSpan? timeOut = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return await Task.Run(() => Receive(timeOut), cancellationToken); + } /// - /// Rejects the specified message. This is just a commit of the offset to move past the record without processing it + /// Rejects the specified message. /// + /// + /// This is just a commit of the offset to move past the record without processing it + /// /// The message. public void Reject(Message message) { Acknowledge(message); } + /// + /// Rejects the specified message. + /// + /// + /// This is just a commit of the offset to move past the record without processing it + /// Calls the underlying sync method, which is non-blocking + /// + /// The message. + /// Cancels the reject; not used as non-blocking + public Task RejectAsync(Message message, CancellationToken cancellationToken = default(CancellationToken)) + { + Reject(message); + return Task.CompletedTask; + } + /// /// Requeues the specified message. A no-op on Kafka as the stream is immutable /// @@ -366,6 +448,18 @@ public bool Requeue(Message message, TimeSpan? delay = null) { return false; } + + /// + /// Requeues the specified message. A no-op on Kafka as the stream is immutable + /// + /// + /// Number of seconds to delay delivery of the message. + /// Cancellation token - not used as not implemented for Kafka + /// False as no requeue support on Kafka + public Task RequeueAsync(Message message, TimeSpan? delay = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(false); + } private void CheckHasPartitions() { @@ -602,26 +696,19 @@ private void Close() } } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { - Close(); - - if (!_disposedValue) + if (disposing) { - if (disposing) - { - _consumer.Dispose(); - _consumer = null; - } - - _disposedValue = true; + _consumer?.Dispose(); + _flushToken?.Dispose(); } } ~KafkaMessageConsumer() { - Dispose(false); + Dispose(false); } public void Dispose() @@ -630,5 +717,11 @@ public void Dispose() GC.SuppressFinalize(this); } - } + public ValueTask DisposeAsync() + { + Dispose(true); + GC.SuppressFinalize(this); + return new ValueTask(Task.CompletedTask); + } + } } diff --git a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageConsumerFactory.cs b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageConsumerFactory.cs index 2ae42020be..5f79e52827 100644 --- a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageConsumerFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageConsumerFactory.cs @@ -27,6 +27,7 @@ namespace Paramore.Brighter.MessagingGateway.Kafka /// /// A factory for creating a Kafka message consumer from a > /// + public class KafkaMessageConsumerFactory : IAmAMessageConsumerFactory { private readonly KafkaMessagingGatewayConfiguration _configuration; @@ -47,7 +48,7 @@ KafkaMessagingGatewayConfiguration configuration /// /// The to read /// A consumer that can be used to read from the stream - public IAmAMessageConsumer Create(Subscription subscription) + public IAmAMessageConsumerSync Create(Subscription subscription) { KafkaSubscription kafkaSubscription = subscription as KafkaSubscription; if (kafkaSubscription == null) @@ -71,5 +72,30 @@ public IAmAMessageConsumer Create(Subscription subscription) makeChannels: kafkaSubscription.MakeChannels ); } + + public IAmAMessageConsumerAsync CreateAsync(Subscription subscription) + { + KafkaSubscription kafkaSubscription = subscription as KafkaSubscription; + if (kafkaSubscription == null) + throw new ConfigurationException("We expect an SQSConnection or SQSConnection as a parameter"); + + return new KafkaMessageConsumer( + configuration: _configuration, + routingKey:kafkaSubscription.RoutingKey, //topic + groupId: kafkaSubscription.GroupId, + offsetDefault: kafkaSubscription.OffsetDefault, + sessionTimeout: kafkaSubscription.SessionTimeout, + maxPollInterval: kafkaSubscription.MaxPollInterval, + isolationLevel: kafkaSubscription.IsolationLevel, + commitBatchSize: kafkaSubscription.CommitBatchSize, + sweepUncommittedOffsetsInterval: kafkaSubscription.SweepUncommittedOffsetsInterval, + readCommittedOffsetsTimeout: kafkaSubscription.ReadCommittedOffsetsTimeOut, + numPartitions: kafkaSubscription.NumPartitions, + partitionAssignmentStrategy: kafkaSubscription.PartitionAssignmentStrategy, + replicationFactor: kafkaSubscription.ReplicationFactor, + topicFindTimeout: kafkaSubscription.TopicFindTimeout, + makeChannels: kafkaSubscription.MakeChannels + ); + } } } diff --git a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageProducer.cs index d624f0f440..af1b8c14e4 100644 --- a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageProducer.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ using System; using System.Diagnostics; +using System.Threading; using System.Threading.Tasks; using Confluent.Kafka; using Microsoft.Extensions.Logging; @@ -52,7 +53,6 @@ internal class KafkaMessageProducer : KafkaMessagingGateway, IAmAMessageProducer private readonly ProducerConfig _producerConfig; private KafkaMessagePublisher _publisher; private bool _hasFatalProducerError; - private bool _disposedValue; public KafkaMessageProducer( KafkaMessagingGatewayConfiguration configuration, @@ -118,6 +118,28 @@ public KafkaMessageProducer( TopicFindTimeout = TimeSpan.FromMilliseconds(publication.TopicFindTimeoutMs); _headerBuilder = publication.MessageHeaderBuilder; } + + /// + /// Dispose of the producer + /// + /// Are we disposing or being called by the GC + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + + /// + /// Dispose of the producer + /// + /// Are we disposing or being called by the GC + public ValueTask DisposeAsync() + { + Dispose(true); + GC.SuppressFinalize(this); + return new ValueTask(Task.CompletedTask); + } /// /// There are a **lot** of properties that we can set to configure Kafka. We expose only those of high importance @@ -152,8 +174,13 @@ public void Init() EnsureTopic(); } - - + + /// + /// Sends the specified message. + /// + /// The message. + /// The message was missing + /// The Kafka client has entered an unrecoverable state public void Send(Message message) { if (message == null) @@ -214,14 +241,19 @@ public void Send(Message message) } } - public void SendWithDelay(Message message, TimeSpan? delay = null) - { - //TODO: No delay support implemented - Send(message); - } - - - public async Task SendAsync(Message message) + + + /// + /// 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) { if (message == null) throw new ArgumentNullException(nameof(message)); @@ -237,7 +269,7 @@ public async Task SendAsync(Message message) message.Body.Value ); - await _publisher.PublishMessageAsync(message, result => PublishResults(result.Status, result.Headers) ); + await _publisher.PublishMessageAsync(message, result => PublishResults(result.Status, result.Headers), cancellationToken); } catch (ProduceException pe) @@ -270,34 +302,42 @@ public async Task SendAsync(Message message) } } - - protected virtual void Dispose(bool disposing) + + /// + /// 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) { - if (!_disposedValue) - { - if (disposing) - { - if (_producer != null) - { - _producer.Flush(TimeSpan.FromMilliseconds(_producerConfig.MessageTimeoutMs.Value + 5000)); - _producer.Dispose(); - _producer = null; - } - } - - _disposedValue = true; - } + //TODO: No delay support implemented + Send(message); } - ~KafkaMessageProducer() + /// + /// 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) { - Dispose(false); + //TODO: No delay support implemented + await SendAsync(message); } - public void Dispose() + private void Dispose(bool disposing) { - Dispose(true); - GC.SuppressFinalize(this); + if (disposing) + { + _producer?.Dispose(); + } } private void PublishResults(PersistenceStatus status, Headers headers) diff --git a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageProducerFactory.cs b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageProducerFactory.cs index b52736b021..a6c5674d46 100644 --- a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageProducerFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageProducerFactory.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Confluent.Kafka; namespace Paramore.Brighter.MessagingGateway.Kafka @@ -54,7 +55,10 @@ public KafkaMessageProducerFactory( _configHook = null; } - /// + /// + /// Creates a message producer registry. + /// + /// A registry of middleware clients by topic, for sending messages to the middleware public Dictionary Create() { var publicationsByTopic = new Dictionary(); @@ -71,6 +75,19 @@ public Dictionary Create() return publicationsByTopic; } + /// + /// Creates a message producer registry. + /// + /// + /// Mainly useful where the producer creation is asynchronous, such as when connecting to a remote service to create or validate infrastructure + /// + /// A cancellation token to cancel the operation + /// A registry of middleware clients by topic, for sending messages to the middleware + public Task> CreateAsync() + { + return Task.FromResult(Create()); + } + /// /// Set a configuration hook to set properties not exposed by KafkaMessagingGatewayConfiguration or KafkaPublication /// Intended as 'get out of gaol free' this couples us to the Confluent .NET Kafka client. Bear in mind that a future release diff --git a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessagePublisher.cs b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessagePublisher.cs index 6579a27ded..f7a1c2e93d 100644 --- a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessagePublisher.cs +++ b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessagePublisher.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Threading; using System.Threading.Tasks; using Confluent.Kafka; @@ -48,11 +49,11 @@ public void PublishMessage(Message message, Action> deliveryReport) + public async Task PublishMessageAsync(Message message, Action> deliveryReport, CancellationToken cancellationToken = default) { var kafkaMessage = BuildMessage(message); - var deliveryResult = await _producer.ProduceAsync(message.Header.Topic, kafkaMessage); + var deliveryResult = await _producer.ProduceAsync(message.Header.Topic, kafkaMessage, cancellationToken); deliveryReport(deliveryResult); } diff --git a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessagingGateway.cs b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessagingGateway.cs index ba0e2aedbb..5b2a8e9446 100644 --- a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessagingGateway.cs +++ b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessagingGateway.cs @@ -30,6 +30,7 @@ THE SOFTWARE. */ using Confluent.Kafka.Admin; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; +using Paramore.Brighter.Tasks; namespace Paramore.Brighter.MessagingGateway.Kafka { @@ -48,6 +49,11 @@ public class KafkaMessagingGateway protected short ReplicationFactor; protected TimeSpan TopicFindTimeout; + /// + /// Ensure that the topic exists, behaviour based on the MakeChannels flag of the publication + /// Sync over async, but alright as we in topic creation + /// + /// protected void EnsureTopic() { if (MakeChannels == OnMissingChannel.Assume) @@ -61,7 +67,7 @@ protected void EnsureTopic() throw new ChannelFailureException($"Topic: {Topic.Value} does not exist"); if (!exists && MakeChannels == OnMissingChannel.Create) - MakeTopic().GetAwaiter().GetResult(); + BrighterSynchronizationHelper.Run(async () => await MakeTopic()); } } diff --git a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaProducerRegistryFactory.cs b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaProducerRegistryFactory.cs index b56a302b64..26c0175218 100644 --- a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaProducerRegistryFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaProducerRegistryFactory.cs @@ -23,6 +23,8 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Confluent.Kafka; namespace Paramore.Brighter.MessagingGateway.Kafka @@ -66,6 +68,16 @@ public IAmAProducerRegistry Create() return new ProducerRegistry(producerFactory.Create()); } + /// + /// Create a producer registry from the and instances supplied + /// to the constructor + /// + /// An that represents a collection of Kafka Message Producers + public Task CreateAsync(CancellationToken ct = default) + { + return Task.FromResult(Create()); + } + /// /// Set a configuration hook to set properties not exposed by KafkaMessagingGatewayConfiguration or KafkaPublication /// Intended as 'get out of gaol free' this couples us to the Confluent .NET Kafka client. Bear in mind that a future release diff --git a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaSubscription.cs b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaSubscription.cs index fd30f9626b..c21c42c408 100644 --- a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaSubscription.cs +++ b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaSubscription.cs @@ -29,14 +29,13 @@ namespace Paramore.Brighter.MessagingGateway.Kafka { public class KafkaSubscription : Subscription { - /// /// /// We commit processed work (marked as acked or rejected) when a batch size worth of work has been completed /// If the batch size is 1, then there is a low risk of offsets not being committed and therefore duplicates appearing /// in the stream, but the latency per request and load on the broker increases. As the batch size rises the risk of /// a crashing worker process failing to commit a batch that is then represented rises. /// - public long CommitBatchSize { get; set; } = 10; + public long CommitBatchSize { get; set; } /// /// Only one consumer in a group can read from a partition at any one time; this preserves ordering @@ -47,7 +46,7 @@ public class KafkaSubscription : Subscription /// /// Default to read only committed messages, change if you want to read uncommited messages. May cause duplicates. /// - public IsolationLevel IsolationLevel { get; set; } = IsolationLevel.ReadCommitted; + public IsolationLevel IsolationLevel { get; set; } /// /// How often the consumer needs to poll for new messages to be considered alive, polling greater than this interval triggers a rebalance @@ -59,7 +58,7 @@ public class KafkaSubscription : Subscription /// /// How many partitions on this topic? /// - public int NumPartitions { get; set; } = 1; + public int NumPartitions { get; set; } /// /// What do we do if there is no offset stored in ZooKeeper for this consumer @@ -67,7 +66,7 @@ public class KafkaSubscription : Subscription /// AutoOffsetReset.Latest - Start from now i.e. only consume messages after we start /// AutoOffsetReset.Error - Consider it an error to be lacking a reset /// - public AutoOffsetReset OffsetDefault { get; set; } = AutoOffsetReset.Earliest; + public AutoOffsetReset OffsetDefault { get; set; } /// /// How should we assign partitions to consumers in the group? @@ -85,7 +84,7 @@ public class KafkaSubscription : Subscription /// /// What is the replication factor? How many nodes is the topic copied to on the broker? /// - public short ReplicationFactor { get; set; } = 1; + public short ReplicationFactor { get; set; } /// /// If Kafka does not receive a heartbeat from the consumer within this time window, trigger a re-balance @@ -124,7 +123,7 @@ public class KafkaSubscription : Subscription /// How often does the consumer poll for a message to be considered alive, after which Kafka will assume dead and rebalance. Defaults to 30000ms /// How often do we commit offsets that have yet to be saved; defaults to 30000 /// Should we read messages that are not on all replicas? May cause duplicates. - /// Is this channel read asynchronously + /// Is this channel read asynchronously /// How many partitions should this topic have - used if we create the topic /// How many copies of each partition should we have across our broker's nodes - used if we create the topic /// The channel factory to create channels for Consumer. /// Should we make channels if they don't exist, defaults to creating @@ -149,16 +148,16 @@ public KafkaSubscription ( TimeSpan? maxPollInterval = null, TimeSpan? sweepUncommittedOffsetsInterval = null, IsolationLevel isolationLevel = IsolationLevel.ReadCommitted, - bool runAsync = false, + MessagePumpType messagePumpType = MessagePumpType.Proactor, int numOfPartitions = 1, short replicationFactor = 1, IAmAChannelFactory channelFactory = null, OnMissingChannel makeChannels = OnMissingChannel.Create, - int emptyChannelDelay = 500, - int channelFailureDelay = 1000, + TimeSpan? emptyChannelDelay = null, + TimeSpan? channelFailureDelay = null, PartitionAssignmentStrategy partitionAssignmentStrategy = PartitionAssignmentStrategy.RoundRobin) : base(dataType, name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount, - requeueDelay, unacceptableMessageLimit, runAsync, channelFactory, makeChannels, emptyChannelDelay, channelFailureDelay) + requeueDelay, unacceptableMessageLimit, messagePumpType, channelFactory, makeChannels, emptyChannelDelay, channelFailureDelay) { CommitBatchSize = commitBatchSize; GroupId = groupId; @@ -198,7 +197,7 @@ public class KafkaSubscription : KafkaSubscription where T : IRequest /// How often does the consumer poll for a message to be considered alive, after which Kafka will assume dead and rebalance; defaults to 30000ms /// How often do we commit offsets that have yet to be saved; defaults to 30000ms /// Should we read messages that are not on all replicas? May cause duplicates. - /// Is this channel read asynchronously + /// Is this channel read asynchronously /// How many partitions should this topic have - used if we create the topic /// How many copies of each partition should we have across our broker's nodes - used if we create the topic /// Should we make channels if they don't exist, defaults to creating @@ -223,17 +222,17 @@ public KafkaSubscription( TimeSpan? maxPollInterval = null, TimeSpan? sweepUncommittedOffsetsInterval = null, IsolationLevel isolationLevel = IsolationLevel.ReadCommitted, - bool runAsync = false, + MessagePumpType messagePumpType = MessagePumpType.Reactor, int numOfPartitions = 1, short replicationFactor = 1, IAmAChannelFactory channelFactory = null, OnMissingChannel makeChannels = OnMissingChannel.Create, - int emptyChannelDelay = 500, - int channelFailureDelay = 1000, + TimeSpan? emptyChannelDelay = null, + TimeSpan? channelFailureDelay = null, PartitionAssignmentStrategy partitionAssignmentStrategy = PartitionAssignmentStrategy.RoundRobin) : base(typeof(T), name, channelName, routingKey, groupId, bufferSize, noOfPerformers, timeOut, requeueCount, requeueDelay, unacceptableMessageLimit, offsetDefault, commitBatchSize, - sessionTimeout, maxPollInterval, sweepUncommittedOffsetsInterval, isolationLevel, runAsync, + sessionTimeout, maxPollInterval, sweepUncommittedOffsetsInterval, isolationLevel, messagePumpType, numOfPartitions, replicationFactor, channelFactory, makeChannels, emptyChannelDelay, channelFailureDelay, partitionAssignmentStrategy) { diff --git a/src/Paramore.Brighter.MessagingGateway.MQTT/MQTTMessageConsumer.cs b/src/Paramore.Brighter.MessagingGateway.MQTT/MQTTMessageConsumer.cs index fc86f69b40..84b9e2524e 100644 --- a/src/Paramore.Brighter.MessagingGateway.MQTT/MQTTMessageConsumer.cs +++ b/src/Paramore.Brighter.MessagingGateway.MQTT/MQTTMessageConsumer.cs @@ -19,7 +19,7 @@ namespace Paramore.Brighter.MessagingGateway.MQTT /// The is used on the server to receive messages from the broker. It abstracts away the details of /// inter-process communication tasks from the server. It handles subscription establishment, request reception and dispatching. /// - public class MQTTMessageConsumer : IAmAMessageConsumer + public class MQTTMessageConsumer : IAmAMessageConsumerSync, IAmAMessageConsumerAsync { private readonly string _topic; private readonly Queue _messageQueue = new Queue(); @@ -30,6 +30,7 @@ public class MQTTMessageConsumer : IAmAMessageConsumer /// /// Initializes a new instance of the class. + /// Sync over Async within constructor /// /// public MQTTMessageConsumer(MQTTMessagingGatewayConsumerConfiguration configuration) @@ -62,7 +63,9 @@ public MQTTMessageConsumer(MQTTMessagingGatewayConsumerConfiguration configurati }); Task connectTask = Connect(configuration.ConnectionAttempts); - connectTask.Wait(); + connectTask + .GetAwaiter() + .GetResult(); } /// @@ -72,11 +75,28 @@ public MQTTMessageConsumer(MQTTMessagingGatewayConsumerConfiguration configurati public void Acknowledge(Message message) { } + + /// + /// Not implemented Acknowledge Method. + /// + /// + public Task AcknowledgeAsync(Message message, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.CompletedTask; + } + public void Dispose() { _mqttClient.Dispose(); } + + + public ValueTask DisposeAsync() + { + _mqttClient.Dispose(); + return new ValueTask(Task.CompletedTask); + } /// /// Clears the internal Queue buffer. @@ -85,9 +105,19 @@ public void Purge() { _messageQueue.Clear(); } + + /// + /// Clears the internal Queue buffer. + /// + /// Allows cancellation of the purge task + public Task PurgeAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + Purge(); + return Task.CompletedTask; + } /// - /// Retrieves the current recieved messages from the internal buffer. + /// Retrieves the current received messages from the internal buffer. /// /// The time to delay retrieval. Defaults to 300ms public Message[] Receive(TimeSpan? timeOut = null) @@ -117,6 +147,11 @@ public Message[] Receive(TimeSpan? timeOut = null) return messages.ToArray(); } + + public Task ReceiveAsync(TimeSpan? timeOut = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(Receive(timeOut)); + } /// /// Not implemented Reject Method. @@ -126,6 +161,17 @@ public void Reject(Message message) { } + /// + /// Not implemented Reject Method. + /// + /// + /// + public Task RejectAsync(Message message, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.CompletedTask; + } + + /// /// Not implemented Requeue Method. /// @@ -135,6 +181,12 @@ public bool Requeue(Message message, TimeSpan? delay = null) { return false; } + + public Task RequeueAsync(Message message, TimeSpan? delay = null, + CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(false); + } private async Task Connect(int connectionAttempts) { diff --git a/src/Paramore.Brighter.MessagingGateway.MQTT/MQTTMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.MQTT/MQTTMessageProducer.cs index 2adb1aed5e..f9ae1a9a79 100644 --- a/src/Paramore.Brighter.MessagingGateway.MQTT/MQTTMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.MQTT/MQTTMessageProducer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Threading; using System.Threading.Tasks; namespace Paramore.Brighter.MessagingGateway.MQTT @@ -31,9 +32,18 @@ public MQTTMessageProducer(MQTTMessagePublisher mqttMessagePublisher) _mqttMessagePublisher = mqttMessagePublisher; } - public void Dispose() + /// + /// Disposes of the producer + /// + public void Dispose(){ } + + /// + /// Disposes of the producer + /// + /// + public ValueTask DisposeAsync() { - _mqttMessagePublisher = null; + return new ValueTask(Task.CompletedTask); } /// @@ -52,28 +62,39 @@ public void Send(Message message) /// Sends the specified message asynchronously. /// /// The message. + /// Allows cancellation of the send operation /// Task. - public async Task SendAsync(Message message) + public async Task SendAsync(Message message, CancellationToken cancellationToken = default) { if (message == null) throw new ArgumentNullException(nameof(message)); - await _mqttMessagePublisher.PublishMessageAsync(message); + await _mqttMessagePublisher.PublishMessageAsync(message, cancellationToken); } - - + /// /// Sends the specified message. /// /// The message. - /// Delay to delivery of the message. + /// Delay is not natively supported - don't block with Task.Delay public void SendWithDelay(Message message, TimeSpan? delay = null) { - delay ??= TimeSpan.Zero; - - //TODO: This is a blocking call, we should replace with a Time call - Task.Delay(delay.Value); + // delay is not natively supported Send(message); } + + /// + /// Sens the specified message. + /// + /// The message. + /// Delay is not natively supported - don't block with Task.Delay + /// 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); + } + + } } diff --git a/src/Paramore.Brighter.MessagingGateway.MQTT/MQTTMessagePublisher.cs b/src/Paramore.Brighter.MessagingGateway.MQTT/MQTTMessagePublisher.cs index 83201e7d69..6f6e63a61b 100644 --- a/src/Paramore.Brighter.MessagingGateway.MQTT/MQTTMessagePublisher.cs +++ b/src/Paramore.Brighter.MessagingGateway.MQTT/MQTTMessagePublisher.cs @@ -7,6 +7,7 @@ using MQTTnet.Client; using MQTTnet.Client.Options; using Paramore.Brighter.Logging; +using Paramore.Brighter.Tasks; namespace Paramore.Brighter.MessagingGateway.MQTT { @@ -22,6 +23,7 @@ public class MQTTMessagePublisher /// /// Initializes a new instance of the class. + /// Sync over async, but necessary as we are in the ctor /// /// The Publisher configuration. public MQTTMessagePublisher(MQTTMessagingGatewayConfiguration config) @@ -46,16 +48,16 @@ public MQTTMessagePublisher(MQTTMessagingGatewayConfiguration config) _mqttClientOptions = mqttClientOptionsBuilder.Build(); - Connect(); + ConnectAsync().GetAwaiter().GetResult(); } - private void Connect() + private async Task ConnectAsync() { for (int i = 0; i < _config.ConnectionAttempts; i++) { try { - _mqttClient.ConnectAsync(_mqttClientOptions, CancellationToken.None).GetAwaiter().GetResult(); + await _mqttClient.ConnectAsync(_mqttClientOptions, CancellationToken.None); s_logger.LogInformation($"Connected to {_config.Hostname}"); return; } @@ -68,25 +70,24 @@ private void Connect() /// /// Sends the specified message. + /// Sync over async /// /// The message. - public void PublishMessage(Message message) - { - PublishMessageAsync(message).GetAwaiter().GetResult(); - } + public void PublishMessage(Message message) => BrighterSynchronizationHelper.Run(() => PublishMessageAsync(message)); /// /// Sends the specified message asynchronously. /// /// The message. + /// Allows cancellation of the operation /// Task. - public async Task PublishMessageAsync(Message message) + public async Task PublishMessageAsync(Message message, CancellationToken cancellationToken = default) { - MqttApplicationMessage mqttMessage = createMQTTMessage(message); - await _mqttClient.PublishAsync(mqttMessage, CancellationToken.None); + MqttApplicationMessage mqttMessage = CreateMqttMessage(message); + await _mqttClient.PublishAsync(mqttMessage, cancellationToken); } - private MqttApplicationMessage createMQTTMessage(Message message) + private MqttApplicationMessage CreateMqttMessage(Message message) { string payload = JsonSerializer.Serialize(message); MqttApplicationMessageBuilder outMessage = new MqttApplicationMessageBuilder() diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/ChannelFactory.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/ChannelFactory.cs index ada4a3cee9..f4cb3be85d 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/ChannelFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/ChannelFactory.cs @@ -1,41 +1,91 @@ using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; -namespace Paramore.Brighter.MessagingGateway.MsSql +namespace Paramore.Brighter.MessagingGateway.MsSql; + +/// +/// Factory class for creating MS SQL channels. +/// +public class ChannelFactory : IAmAChannelFactory { - public class ChannelFactory : IAmAChannelFactory + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + private readonly MsSqlMessageConsumerFactory _msSqlMessageConsumerFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The factory for creating MS SQL message consumers. + /// Thrown when the msSqlMessageConsumerFactory is null. + public ChannelFactory(MsSqlMessageConsumerFactory msSqlMessageConsumerFactory) + { + _msSqlMessageConsumerFactory = msSqlMessageConsumerFactory ?? + throw new ArgumentNullException(nameof(msSqlMessageConsumerFactory)); + } + + /// + /// Creates a synchronous MS SQL channel. + /// + /// The subscription details for the channel. + /// A synchronous MS SQL channel instance. + /// Thrown when the subscription is not an MsSqlSubscription. + public IAmAChannelSync CreateSyncChannel(Subscription subscription) + { + MsSqlSubscription? rmqSubscription = subscription as MsSqlSubscription; + if (rmqSubscription == null) + throw new ConfigurationException("MS SQL ChannelFactory We expect an MsSqlSubscription or MsSqlSubscription as a parameter"); + + s_logger.LogDebug("MsSqlInputChannelFactory: create input channel {ChannelName} for topic {Topic}", subscription.ChannelName, subscription.RoutingKey); + return new Channel( + subscription.ChannelName, + subscription.RoutingKey, + _msSqlMessageConsumerFactory.Create(subscription), + subscription.BufferSize); + } + + /// + /// Creates an asynchronous MS SQL channel. + /// + /// The subscription details for the channel. + /// An asynchronous MS SQL channel instance. + /// Thrown when the subscription is not an MsSqlSubscription. + public IAmAChannelAsync CreateAsyncChannel(Subscription subscription) + { + MsSqlSubscription? rmqSubscription = subscription as MsSqlSubscription; + if (rmqSubscription == null) + throw new ConfigurationException("MS SQL ChannelFactory We expect an MsSqlSubscription or MsSqlSubscription as a parameter"); + + s_logger.LogDebug("MsSqlInputChannelFactory: create input channel {ChannelName} for topic {Topic}", subscription.ChannelName, subscription.RoutingKey); + return new ChannelAsync( + subscription.ChannelName, + subscription.RoutingKey, + _msSqlMessageConsumerFactory.CreateAsync(subscription), + subscription.BufferSize); + + } + + /// + /// Asynchronously creates an asynchronous MS SQL channel. + /// + /// The subscription details for the channel. + /// A token to cancel the operation. + /// A task representing the asynchronous operation, with an asynchronous MS SQL channel instance as the result. + /// Thrown when the subscription is not an MsSqlSubscription. + public async Task CreateAsyncChannelAsync(Subscription subscription, CancellationToken ct = default) { - private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - private readonly MsSqlMessageConsumerFactory _msSqlMessageConsumerFactory; - - /// - /// Initializes a new instance of the class. - /// - /// - public ChannelFactory(MsSqlMessageConsumerFactory msSqlMessageConsumerFactory) - { - _msSqlMessageConsumerFactory = msSqlMessageConsumerFactory ?? - throw new ArgumentNullException(nameof(msSqlMessageConsumerFactory)); - } - - /// - /// Creates the input channel - /// - /// The subscription parameters with which to create the channel - /// - public IAmAChannel CreateChannel(Subscription subscription) - { - MsSqlSubscription rmqSubscription = subscription as MsSqlSubscription; - if (rmqSubscription == null) - throw new ConfigurationException("We expect an MsSqlSubscription or MsSqlSubscription as a parameter"); - - s_logger.LogDebug("MsSqlInputChannelFactory: create input channel {ChannelName} for topic {Topic}", subscription.ChannelName, subscription.RoutingKey); - return new Channel( - subscription.ChannelName, - subscription.RoutingKey, - _msSqlMessageConsumerFactory.Create(subscription), - subscription.BufferSize); - } + MsSqlSubscription? rmqSubscription = subscription as MsSqlSubscription; + if (rmqSubscription == null) + throw new ConfigurationException("MS SQL ChannelFactory We expect an MsSqlSubscription or MsSqlSubscription as a parameter"); + + s_logger.LogDebug("MsSqlInputChannelFactory: create input channel {ChannelName} for topic {Topic}", subscription.ChannelName, subscription.RoutingKey); + var channel = new ChannelAsync( + subscription.ChannelName, + subscription.RoutingKey, + _msSqlMessageConsumerFactory.CreateAsync(subscription), + subscription.BufferSize); + + return await Task.FromResult(channel); } } diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs index e885f84a98..b5b9398975 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; @@ -7,7 +8,7 @@ namespace Paramore.Brighter.MessagingGateway.MsSql { - public class MsSqlMessageConsumer : IAmAMessageConsumer + public class MsSqlMessageConsumer : IAmAMessageConsumerSync, IAmAMessageConsumerAsync { private readonly string _topic; private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); @@ -23,73 +24,115 @@ RelationalDbConnectionProvider connectionProvider _sqlMessageQueue = new MsSqlMessageQueue(msSqlConfiguration, connectionProvider); } - public MsSqlMessageConsumer( - RelationalDatabaseConfiguration msSqlConfiguration, - string topic) :this(msSqlConfiguration, topic, new MsSqlConnectionProvider(msSqlConfiguration)) + public MsSqlMessageConsumer(RelationalDatabaseConfiguration msSqlConfiguration, string topic) + : this(msSqlConfiguration, topic, new MsSqlConnectionProvider(msSqlConfiguration)) + {} + + /// + /// Acknowledges the specified message. + /// + /// + /// No implementation required because of atomic 'read-and-delete' + /// + /// The message. + public void Acknowledge(Message message) {} + + public Task AcknowledgeAsync(Message message, CancellationToken cancellationToken = default(CancellationToken)) { + return Task.CompletedTask; } - + + /// + /// Purges the specified queue name. + /// + public void Purge() + { + s_logger.LogDebug("MsSqlMessagingConsumer: purging queue"); + _sqlMessageQueue.Purge(); + } + + public async Task PurgeAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + s_logger.LogDebug("MsSqlMessagingConsumer: purging queue"); + await Task.Run( () => _sqlMessageQueue.Purge(), cancellationToken); + } + /// /// Receives the specified queue name. /// An abstraction over a third-party messaging library. Used to read messages from the broker and to acknowledge the processing of those messages or requeue them. /// Used by a to provide access to a third-party message queue. /// /// How long to wait on a recieve. Default is 300ms - /// Message. + /// Message public Message[] Receive(TimeSpan? timeOut = null) { timeOut ??= TimeSpan.FromMilliseconds(300); var rc = _sqlMessageQueue.TryReceive(_topic, timeOut.Value); var message = !rc.IsDataValid ? new Message() : rc.Message; - return new Message[]{message}; + return [message!]; } /// - /// Acknowledges the specified message. + /// Receives the specified queue name. + /// An abstraction over a third-party messaging library. Used to read messages from the broker and to acknowledge the processing of those messages or requeue them. + /// Used by a to provide access to a third-party message queue. /// - /// The message. - public void Acknowledge(Message message) + /// How long to wait on a recieve. Default is 300ms + /// The for the operation + /// Message + public async Task ReceiveAsync(TimeSpan? timeOut = null, CancellationToken cancellationToken = default(CancellationToken)) { - // Not required because of atomic 'read-and-delete' + cancellationToken.ThrowIfCancellationRequested(); + + var ct = new CancellationTokenSource(); + ct.CancelAfter(timeOut ?? TimeSpan.FromMilliseconds(300) ); + var operationCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, ct.Token).Token; + + var rc = await _sqlMessageQueue.TryReceiveAsync(_topic, operationCancellationToken); + var message = !rc.IsDataValid ? new Message() : rc.Message; + return [message!]; } - /// + /// /// Rejects the specified message. /// + /// + /// Not implemented for the MSSQL message consumer + /// /// The message. public void Reject(Message message) { s_logger.LogInformation( "MsSqlMessagingConsumer: rejecting message with topic {Topic} and id {Id}, NOT IMPLEMENTED", - message.Header.Topic, message.Id.ToString()); + message.Header.Topic, message.Id); } /// - /// Purges the specified queue name. + /// Rejects the specified message. /// - public void Purge() - { - s_logger.LogDebug("MsSqlMessagingConsumer: purging queue"); - _sqlMessageQueue.Purge(); - } + /// + /// Not implemented for the MSSQL message consumer + /// + /// The message. + /// A to cancel the reject + public Task RejectAsync(Message message, CancellationToken cancellationToken = default(CancellationToken)) + { + Reject(message); + return Task.CompletedTask; + } /// /// Requeues the specified message. /// /// - /// Delay to delivery of the message. 0 for immediate requeue. Default to 0 + /// Delay is not natively supported - don't block with Task.Delay /// True when message is requeued public bool Requeue(Message message, TimeSpan? delay = null) { delay ??= TimeSpan.Zero; - - //TODO: This blocks, use a time evern instead to requeue after an interval - if (delay.Value > TimeSpan.Zero) - { - Task.Delay(delay.Value).Wait(); - } - + + // delay is not natively supported - don't block with Task.Delay var topic = message.Header.Topic; s_logger.LogDebug("MsSqlMessagingConsumer: re-queuing message with topic {Topic} and id {Id}", topic, @@ -99,8 +142,41 @@ public bool Requeue(Message message, TimeSpan? delay = null) return true; } + /// + /// Requeues the specified message. + /// + /// + /// Delay is not natively supported - don't block with Task.Delay + /// True when message is requeued + public async Task RequeueAsync(Message message, TimeSpan? delay = null, CancellationToken cancellationToken = default(CancellationToken)) + { + delay ??= TimeSpan.Zero; + + // delay is not natively supported - don't block with Task.Delay + var topic = message.Header.Topic; + + s_logger.LogDebug("MsSqlMessagingConsumer: re-queuing message with topic {Topic} and id {Id}", topic, + message.Id.ToString()); + + await _sqlMessageQueue.SendAsync(message, topic, null, cancellationToken: cancellationToken); + return true; + } + + /// + /// Dispose of the consumer + /// + /// + /// Nothing to do here + /// public void Dispose() { + GC.SuppressFinalize(this); + } + + public ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + return new ValueTask(Task.CompletedTask); } } } diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumerFactory.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumerFactory.cs index 09100f5910..b11a60b437 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumerFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumerFactory.cs @@ -5,28 +5,30 @@ namespace Paramore.Brighter.MessagingGateway.MsSql { - public class MsSqlMessageConsumerFactory : IAmAMessageConsumerFactory + public class MsSqlMessageConsumerFactory(RelationalDatabaseConfiguration msSqlConfiguration) : IAmAMessageConsumerFactory { private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - private readonly RelationalDatabaseConfiguration _msSqlConfiguration; - - public MsSqlMessageConsumerFactory(RelationalDatabaseConfiguration msSqlConfiguration) - { - _msSqlConfiguration = msSqlConfiguration ?? - throw new ArgumentNullException( - nameof(msSqlConfiguration)); - } + private readonly RelationalDatabaseConfiguration _msSqlConfiguration = msSqlConfiguration ?? throw new ArgumentNullException(nameof(msSqlConfiguration)); /// /// Creates a consumer for the specified queue. /// /// The queue to connect to - /// IAmAMessageConsumer - public IAmAMessageConsumer Create(Subscription subscription) + /// IAmAMessageConsumerSync + public IAmAMessageConsumerSync Create(Subscription subscription) + { + if (subscription.ChannelName is null) throw new ConfigurationException(nameof(subscription.ChannelName)); + + s_logger.LogDebug("MsSqlMessageConsumerFactory: create consumer for topic {ChannelName}", subscription.ChannelName); + return new MsSqlMessageConsumer(_msSqlConfiguration, subscription.ChannelName!); + } + + public IAmAMessageConsumerAsync CreateAsync(Subscription subscription) { - if (subscription.ChannelName == null) throw new ArgumentNullException(nameof(subscription.ChannelName)); + if (subscription.ChannelName is null) throw new ConfigurationException(nameof(subscription.ChannelName)); + s_logger.LogDebug("MsSqlMessageConsumerFactory: create consumer for topic {ChannelName}", subscription.ChannelName); - return new MsSqlMessageConsumer(_msSqlConfiguration, subscription.ChannelName); + return new MsSqlMessageConsumer(_msSqlConfiguration, subscription.ChannelName!); } } } diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs index 31762eb636..4382c57c9e 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ using System; using System.Diagnostics; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; @@ -31,37 +32,56 @@ THE SOFTWARE. */ namespace Paramore.Brighter.MessagingGateway.MsSql { + /// + /// The class is responsible for producing messages to an MS SQL database. + /// public class MsSqlMessageProducer : IAmAMessageProducerSync, IAmAMessageProducerAsync { private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); private readonly MsSqlMessageQueue _sqlQ; /// - /// The Publication used to configure the producer + /// Gets the publication used to configure the producer. /// public Publication Publication { get; } /// - /// The OTel Span we are writing Producer events too + /// Gets or sets the OTel span for writing producer events. /// - public Activity Span { get; set; } + public Activity? Span { get; set; } + /// + /// Initializes a new instance of the class. + /// + /// The MS SQL configuration. + /// The connection provider. + /// The publication configuration. public MsSqlMessageProducer( RelationalDatabaseConfiguration msSqlConfiguration, IAmARelationalDbConnectionProvider connectonProvider, - Publication publication = null + Publication? publication = null ) { _sqlQ = new MsSqlMessageQueue(msSqlConfiguration, connectonProvider); - Publication = publication ?? new Publication {MakeChannels = OnMissingChannel.Create}; + Publication = publication ?? new Publication { MakeChannels = OnMissingChannel.Create }; } + /// + /// Initializes a new instance of the class. + /// + /// The MS SQL configuration. + /// The publication configuration. public MsSqlMessageProducer( RelationalDatabaseConfiguration msSqlConfiguration, - Publication publication = null) : this(msSqlConfiguration, new MsSqlConnectionProvider(msSqlConfiguration), publication) + Publication? publication = null) + : this(msSqlConfiguration, new MsSqlConnectionProvider(msSqlConfiguration), publication) { } + /// + /// Sends the specified message. + /// + /// The message to send. public void Send(Message message) { var topic = message.Header.Topic; @@ -70,15 +90,14 @@ public void Send(Message message) _sqlQ.Send(message, topic); } - - public void SendWithDelay(Message message, TimeSpan? delay = null) - { - //No delay support implemented - Send(message); - } - - public async Task SendAsync(Message message) + /// + /// Sends the specified message asynchronously. + /// + /// The message to send. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous send operation. + public async Task SendAsync(Message message, CancellationToken cancellationToken = default) { var topic = message.Header.Topic; @@ -88,8 +107,40 @@ public async Task SendAsync(Message message) await _sqlQ.SendAsync(message, topic, TimeSpan.Zero); } - public void Dispose() + /// + /// Sends the specified message with a delay. + /// + /// No delay support implemented. + /// The message to send. + /// The delay to use. + public void SendWithDelay(Message message, TimeSpan? delay = null) + { + // No delay support implemented + Send(message); + } + + /// + /// Sends the specified message with a delay asynchronously. + /// + /// No delay support implemented. + /// The message to send. + /// The delay to use. + /// A cancellation token to observe while waiting for the task to complete. + /// 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); + } + + /// + /// Disposes the message producer. + /// + public void Dispose() { } + + public ValueTask DisposeAsync() { + return new ValueTask(Task.CompletedTask); } } } diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducerFactory.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducerFactory.cs index 24f49e651f..622ee55475 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducerFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducerFactory.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Threading.Tasks; namespace Paramore.Brighter.MessagingGateway.MsSql { @@ -54,10 +55,17 @@ public Dictionary Create() foreach (var publication in _publications) { + if (publication.Topic is null) throw new ConfigurationException("MS SQL Message Producer Factory: Topic is missing from the publication"); producers[publication.Topic] = new MsSqlMessageProducer(_msSqlConfiguration, publication); } return producers; } + + /// + public Task> CreateAsync() + { + return Task.FromResult(Create()); + } } } diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlProducerRegistryFactory.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlProducerRegistryFactory.cs index 1d70d7a633..bf79864c3e 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlProducerRegistryFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlProducerRegistryFactory.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; @@ -22,6 +24,10 @@ public MsSqlProducerRegistryFactory( _publications = publications; } + /// + /// Creates a message producer registry. + /// + /// A registry of middleware clients by topic, for sending messages to the middleware public IAmAProducerRegistry Create() { s_logger.LogDebug("MsSqlMessageProducerFactory: create producer"); @@ -30,5 +36,18 @@ public IAmAProducerRegistry Create() return new ProducerRegistry(producerFactory.Create()); } + + /// + /// Creates a message producer registry. + /// + /// + /// Mainly useful where the producer creation is asynchronous, such as when connecting to a remote service to create or validate infrastructure + /// + /// A cancellation token to cancel the operation + /// A registry of middleware clients by topic, for sending messages to the middleware + public Task CreateAsync(CancellationToken ct = default) + { + return Task.FromResult(Create()); + } } } diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlSubscription.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlSubscription.cs index dd191aecb6..b0d8231b8b 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlSubscription.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlSubscription.cs @@ -41,32 +41,31 @@ public class MsSqlSubscription : Subscription /// The number of times you want to requeue a message before dropping it. /// The delay the delivery of a requeue message. 0 is no delay. Defaults to 0 /// The number of unacceptable messages to handle, before stopping reading from the channel. - /// Is this channel read asynchronously + /// Is this channel read asynchronously /// The channel factory to create channels for Consumer. /// Should we make channels if they don't exist, defaults to creating /// How long to pause when a channel is empty in milliseconds /// How long to pause when there is a channel failure in milliseconds public MsSqlSubscription( Type dataType, - SubscriptionName name = null, - ChannelName channelName = null, - RoutingKey routingKey = null, + SubscriptionName? name = null, + ChannelName? channelName = null, + RoutingKey? routingKey = null, int bufferSize = 1, int noOfPerformers = 1, TimeSpan? timeOut = null, int requeueCount = -1, TimeSpan? requeueDelay = null, int unacceptableMessageLimit = 0, - bool runAsync = false, - IAmAChannelFactory channelFactory = null, + MessagePumpType messagePumpType = MessagePumpType.Proactor, + IAmAChannelFactory? channelFactory = null, OnMissingChannel makeChannels = OnMissingChannel.Create, - int emptyChannelDelay = 500, - int channelFailureDelay = 1000) + TimeSpan? emptyChannelDelay = null, + TimeSpan? channelFailureDelay = null) : base(dataType, name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount, - requeueDelay, unacceptableMessageLimit, runAsync, channelFactory, makeChannels, + requeueDelay, unacceptableMessageLimit, messagePumpType, channelFactory, makeChannels, emptyChannelDelay, channelFailureDelay) - { - } + { } } public class MsSqlSubscription : MsSqlSubscription where T : IRequest @@ -83,28 +82,28 @@ public class MsSqlSubscription : MsSqlSubscription where T : IRequest /// The number of times you want to requeue a message before dropping it. /// The Delay to the requeue of a message. 0 is no delay. Defaults to 0 /// The number of unacceptable messages to handle, before stopping reading from the channel. - /// Is this channel read asynchronously + /// Is this channel read asynchronously /// The channel factory to create channels for Consumer. /// Should we make channels if they don't exist, defaults to creating /// How long to pause when a channel is empty in milliseconds /// How long to pause when there is a channel failure in milliseconds public MsSqlSubscription( - SubscriptionName name = null, - ChannelName channelName = null, - RoutingKey routingKey = null, + SubscriptionName? name = null, + ChannelName? channelName = null, + RoutingKey? routingKey = null, int bufferSize = 1, int noOfPerformers = 1, TimeSpan? timeOut = null, int requeueCount = -1, TimeSpan? requeueDelay = null, int unacceptableMessageLimit = 0, - bool runAsync = false, - IAmAChannelFactory channelFactory = null, + MessagePumpType messagePumpType = MessagePumpType.Proactor, + IAmAChannelFactory? channelFactory = null, OnMissingChannel makeChannels = OnMissingChannel.Create, - int emptyChannelDelay = 500, - int channelFailureDelay = 1000) + TimeSpan? emptyChannelDelay = null, + TimeSpan? channelFailureDelay = null) : base(typeof(T), name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount, - requeueDelay, unacceptableMessageLimit, runAsync, channelFactory, makeChannels, emptyChannelDelay, channelFailureDelay) + requeueDelay, unacceptableMessageLimit, messagePumpType, channelFactory, makeChannels, emptyChannelDelay, channelFailureDelay) { } diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/Paramore.Brighter.MessagingGateway.MsSql.csproj b/src/Paramore.Brighter.MessagingGateway.MsSql/Paramore.Brighter.MessagingGateway.MsSql.csproj index 42ff65c620..bd791bdb50 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/Paramore.Brighter.MessagingGateway.MsSql.csproj +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/Paramore.Brighter.MessagingGateway.MsSql.csproj @@ -3,6 +3,7 @@ Provides an implementation of the messaging gateway for decoupled invocation in the Paramore.Brighter pipeline, using MS Sql Server Fred Hoogduin net8.0;net9.0 + enable RabbitMQ;AMQP;Command;Event;Service Activator;Decoupled;Invocation;Messaging;Remote;Command Dispatcher;Command Processor;Request;Service;Task Queue;Work Queue;Retry;Circuit Breaker;Availability diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs index 14ba049ff3..995c0252fc 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs @@ -86,7 +86,8 @@ public async Task SendAsync(T message, string topic, TimeSpan? timeOut, Cancella } /// - /// Try receiving a message + /// Try receiving a message + /// Sync over Async /// /// The topic name /// Timeout for reading a message of the queue; -1 or null for default timeout @@ -102,7 +103,9 @@ public ReceivedResult TryReceive(string topic, TimeSpan? timeout = null) var timeLeft = timeout.Value.TotalMilliseconds; while (!rc.IsDataValid && timeLeft > 0) { - Task.Delay(RetryDelay).Wait(); + Task.Delay(RetryDelay) + .GetAwaiter() + .GetResult(); timeLeft -= RetryDelay; rc = TryReceive(topic); } @@ -170,7 +173,11 @@ public int NumberOfMessageReady(string topic) using var connection = _connectionProvider.GetConnection(); var sqlCmd = connection.CreateCommand(); sqlCmd.CommandText = sql; - return (int) sqlCmd.ExecuteScalar(); + object? count = sqlCmd.ExecuteScalar(); + + if (count is null) return 0; + + return (int) count; } /// @@ -192,10 +199,14 @@ private static IDbDataParameter CreateDbDataParameter(string parameterName, obje private static IDbDataParameter[] InitAddDbParameters(string topic, T message) { + string? fullName = typeof(T).FullName; + //not sure how we would ever get here. + if (fullName is null) throw new ArgumentNullException(nameof(fullName), "MsSQLMessageQueue: The type of the message must have a full name"); + var parameters = new[] { CreateDbDataParameter("topic", topic), - CreateDbDataParameter("messageType", typeof(T).FullName), + CreateDbDataParameter("messageType", fullName), CreateDbDataParameter("payload", JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)) }; return parameters; diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/ReceivedResult.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/ReceivedResult.cs index 1b17df8020..65e91b1c58 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/ReceivedResult.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/ReceivedResult.cs @@ -25,15 +25,16 @@ public ReceivedResult(bool isDataValid, string jsonContent, string topic, string /// Typed received result /// /// The type of the message - public class ReceivedResult : ReceivedResult + public class ReceivedResult( + bool isDataValid, + string jsonContent, + string topic, + string messageType, + long id, + T? message) + : ReceivedResult(isDataValid, jsonContent, topic, messageType, id) { - public ReceivedResult(bool isDataValid, string jsonContent, string topic, string messageType, long id, T message) - : base(isDataValid, jsonContent, topic, messageType, id) - { - Message = message; - } - - public T Message { get; } + public T? Message { get; } = message; public new static ReceivedResult Empty => new ReceivedResult(false, string.Empty, string.Empty, string.Empty, 0, default(T)); } diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/ChannelFactory.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/ChannelFactory.cs index cf05e38ac1..089b4cba53 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/ChannelFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/ChannelFactory.cs @@ -19,49 +19,96 @@ 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; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter.MessagingGateway.RMQ; -namespace Paramore.Brighter.MessagingGateway.RMQ +/// +/// Factory class for creating RabbitMQ channels. +/// +public class ChannelFactory : IAmAChannelFactory { + private readonly RmqMessageConsumerFactory _messageConsumerFactory; + /// - /// Class RMQInputChannelFactory. - /// Creates instances of channels. Supports the creation of AMQP Application Layer channels using RabbitMQ + /// Initializes a new instance of the class. /// - public class ChannelFactory : IAmAChannelFactory + /// The factory for creating RabbitMQ message consumers. + public ChannelFactory(RmqMessageConsumerFactory messageConsumerFactory) { - private readonly RmqMessageConsumerFactory _messageConsumerFactory; - - /// - /// Initializes a new instance of the class. - /// - /// The messageConsumerFactory. - public ChannelFactory(RmqMessageConsumerFactory messageConsumerFactory) - { - _messageConsumerFactory = messageConsumerFactory; - } - - /// - /// Creates the input channel. - /// - /// An RmqSubscription with parameters to create the queue with - /// IAmAnInputChannel. - public IAmAChannel CreateChannel(Subscription subscription) - { - RmqSubscription rmqSubscription = subscription as RmqSubscription; - if (rmqSubscription == null) - throw new ConfigurationException("We expect an RmqSubscription or RmqSubscription as a parameter"); - - var messageConsumer = _messageConsumerFactory.Create(rmqSubscription); - - return new Channel( - channelName: subscription.ChannelName, - routingKey: subscription.RoutingKey, - messageConsumer: messageConsumer, - maxQueueLength: subscription.BufferSize - ); - } + _messageConsumerFactory = messageConsumerFactory; + } + + /// + /// Creates a synchronous RabbitMQ channel. + /// + /// The subscription details for the channel. + /// A synchronous RabbitMQ channel instance. + /// Thrown when the subscription is not an RmqSubscription. + public IAmAChannelSync CreateSyncChannel(Subscription subscription) + { + RmqSubscription? rmqSubscription = subscription as RmqSubscription; + if (rmqSubscription == null) + throw new ConfigurationException("We expect an RmqSubscription or RmqSubscription as a parameter"); + + var messageConsumer = _messageConsumerFactory.Create(rmqSubscription); + + return new Channel( + channelName: subscription.ChannelName, + routingKey: subscription.RoutingKey, + messageConsumer: messageConsumer, + maxQueueLength: subscription.BufferSize + ); + } + + /// + /// Creates an asynchronous RabbitMQ channel. + /// + /// The subscription details for the channel. + /// An asynchronous RabbitMQ channel instance. + /// Thrown when the subscription is not an RmqSubscription. + public IAmAChannelAsync CreateAsyncChannel(Subscription subscription) + { + RmqSubscription? rmqSubscription = subscription as RmqSubscription; + if (rmqSubscription == null) + throw new ConfigurationException("We expect an RmqSubscription or RmqSubscription as a parameter"); + + var messageConsumer = _messageConsumerFactory.CreateAsync(rmqSubscription); + + return new ChannelAsync( + channelName: subscription.ChannelName, + routingKey: subscription.RoutingKey, + messageConsumer: messageConsumer, + maxQueueLength: subscription.BufferSize + ); + } + + /// + /// Asynchronously creates an asynchronous RabbitMQ channel. + /// + /// The subscription details for the channel. + /// A token to cancel the operation. + /// A task representing the asynchronous operation, with an asynchronous RabbitMQ channel instance as the result. + /// Thrown when the subscription is not an RmqSubscription. + public Task CreateAsyncChannelAsync(Subscription subscription, CancellationToken ct = default) + { + RmqSubscription? rmqSubscription = subscription as RmqSubscription; + if (rmqSubscription == null) + throw new ConfigurationException("We expect an RmqSubscription or RmqSubscription as a parameter"); + + var messageConsumer = _messageConsumerFactory.CreateAsync(rmqSubscription); + + var channel = new ChannelAsync( + channelName: subscription.ChannelName, + routingKey: subscription.RoutingKey, + messageConsumer: messageConsumer, + maxQueueLength: subscription.BufferSize + ); + + return Task.FromResult(channel); } } diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/ConnectionPolicyFactory.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/ConnectionPolicyFactory.cs index 61d165282a..2903f9ed3a 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/ConnectionPolicyFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/ConnectionPolicyFactory.cs @@ -51,14 +51,17 @@ public ConnectionPolicyFactory() /// public ConnectionPolicyFactory(RmqMessagingGatewayConnection connection) { + if (connection.Exchange is null) throw new ConfigurationException("RabbitMQ Exchange is not set"); + if (connection.AmpqUri is null) throw new ConfigurationException("RabbitMQ Broker URL is not set"); + var retries = connection.AmpqUri.ConnectionRetryCount; var retryWaitInMilliseconds = connection.AmpqUri.RetryWaitInMilliseconds; var circuitBreakerTimeout = connection.AmpqUri.CircuitBreakTimeInMilliseconds; - RetryPolicy = Policy + RetryPolicyAsync = Policy .Handle() .Or() - .WaitAndRetry( + .WaitAndRetryAsync( retries, retryAttempt => TimeSpan.FromMilliseconds(retryWaitInMilliseconds * Math.Pow(2, retryAttempt)), (exception, timeSpan, context) => @@ -84,21 +87,21 @@ public ConnectionPolicyFactory(RmqMessagingGatewayConnection connection) } }); - CircuitBreakerPolicy = Policy + CircuitBreakerPolicyAsync = Policy .Handle() - .CircuitBreaker(1, TimeSpan.FromMilliseconds(circuitBreakerTimeout)); + .CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(circuitBreakerTimeout)); } /// /// Gets the retry policy. /// /// The retry policy. - public Policy RetryPolicy { get; } + public AsyncPolicy RetryPolicyAsync { get; } /// /// Gets the circuit breaker policy. /// /// The circuit breaker policy. - public Policy CircuitBreakerPolicy { get; } + public AsyncPolicy CircuitBreakerPolicyAsync { get; } } } diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/ExchangeConfigurationHelper.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/ExchangeConfigurationHelper.cs index 201d558b0a..46d7abe8c2 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/ExchangeConfigurationHelper.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/ExchangeConfigurationHelper.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2014 Ian Cooper @@ -24,66 +25,81 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using RabbitMQ.Client; using RabbitMQ.Client.Exceptions; -namespace Paramore.Brighter.MessagingGateway.RMQ +namespace Paramore.Brighter.MessagingGateway.RMQ; + +public static class ExchangeConfigurationHelper { - public static class ExchangeConfigurationHelper + public static async Task DeclareExchangeForConnection(this IChannel channel, RmqMessagingGatewayConnection connection, + OnMissingChannel onMissingChannel, + CancellationToken cancellationToken = default) { - public static void DeclareExchangeForConnection(this IModel channel, RmqMessagingGatewayConnection connection, OnMissingChannel onMissingChannel) + if (onMissingChannel == OnMissingChannel.Assume) { - if (onMissingChannel == OnMissingChannel.Assume) - return; + return; + } - if (onMissingChannel == OnMissingChannel.Create) - { - CreateExchange(channel, connection); - } - else if (onMissingChannel == OnMissingChannel.Validate) - { - ValidateExchange(channel, connection); - } + if (onMissingChannel == OnMissingChannel.Create) + { + await CreateExchange(channel, connection, cancellationToken); } + else if (onMissingChannel == OnMissingChannel.Validate) + { + await ValidateExchange(channel, connection, cancellationToken); + } + } - private static void CreateExchange(IModel channel, RmqMessagingGatewayConnection connection) + private static async Task CreateExchange(IChannel channel, RmqMessagingGatewayConnection connection, CancellationToken cancellationToken) + { + if (connection.Exchange is null) throw new ConfigurationException("RabbitMQ Exchange is not set"); + + var arguments = new Dictionary(); + if (connection.Exchange.SupportDelay) { - var arguments = new Dictionary(); - if (connection.Exchange.SupportDelay) - { - arguments.Add("x-delayed-type", connection.Exchange.Type); - connection.Exchange.Type = "x-delayed-message"; - } + arguments.Add("x-delayed-type", connection.Exchange.Type); + connection.Exchange.Type = "x-delayed-message"; + } - channel.ExchangeDeclare( - connection.Exchange.Name, - connection.Exchange.Type, - connection.Exchange.Durable, + await channel.ExchangeDeclareAsync( + connection.Exchange.Name, + connection.Exchange.Type, + connection.Exchange.Durable, + autoDelete: false, + arguments: arguments, + cancellationToken: cancellationToken); + + + if (connection.DeadLetterExchange != null) + { + await channel.ExchangeDeclareAsync( + connection.DeadLetterExchange.Name, + connection.DeadLetterExchange.Type, + connection.DeadLetterExchange.Durable, autoDelete: false, - arguments: arguments); + cancellationToken: cancellationToken); + } + } + + private static async Task ValidateExchange(IChannel channel, RmqMessagingGatewayConnection connection, CancellationToken cancellationToken) + { + if (connection.Exchange is null) throw new ConfigurationException("RabbitMQ Exchange is not set"); + + try + { + await channel.ExchangeDeclarePassiveAsync(connection.Exchange.Name, cancellationToken); if (connection.DeadLetterExchange != null) { - channel.ExchangeDeclare( - connection.DeadLetterExchange.Name, - connection.DeadLetterExchange.Type, - connection.DeadLetterExchange.Durable, - autoDelete: false); + await channel.ExchangeDeclarePassiveAsync(connection.DeadLetterExchange.Name, cancellationToken); } } - private static void ValidateExchange(IModel channel, RmqMessagingGatewayConnection connection) - + catch (Exception e) { - try - { - channel.ExchangeDeclarePassive(connection.Exchange.Name); - if (connection.DeadLetterExchange != null) channel.ExchangeDeclarePassive(connection.DeadLetterExchange.Name); - } - catch (Exception e) - { - throw new BrokerUnreachableException(e); - } + throw new BrokerUnreachableException(e); } - - } + } } diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/HeaderResult.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/HeaderResult.cs index 2cc94195e8..b89cb9ff13 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/HeaderResult.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/HeaderResult.cs @@ -49,7 +49,7 @@ public HeaderResult(TResult result, bool success) /// The type of the t new. /// The map. /// HeaderResult<TNew>. - public HeaderResult Map(Func> map) + public HeaderResult Map(Func> map) { if (Success) return map(Result); @@ -71,19 +71,19 @@ public HeaderResult Map(Func> map) /// Empties this instance. /// /// HeaderResult<TResult>. - public static HeaderResult Empty() + public static HeaderResult Empty() { if (typeof(TResult) == typeof(string)) { - return new HeaderResult((TResult)(object)string.Empty, false); + return new HeaderResult((TResult)(object)string.Empty, false); } if (typeof(TResult) == typeof(RoutingKey)) { - return new HeaderResult((TResult)(object)RoutingKey.Empty, false); + return new HeaderResult((TResult)(object)RoutingKey.Empty, false); } - return new HeaderResult(default(TResult), false); + return new HeaderResult(default(TResult), false); } } } diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/Paramore.Brighter.MessagingGateway.RMQ.csproj b/src/Paramore.Brighter.MessagingGateway.RMQ/Paramore.Brighter.MessagingGateway.RMQ.csproj index a38ce77a4b..56014fbfde 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/Paramore.Brighter.MessagingGateway.RMQ.csproj +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/Paramore.Brighter.MessagingGateway.RMQ.csproj @@ -3,6 +3,7 @@ Provides an implementation of the messaging gateway for decoupled invocation in the Paramore.Brighter pipeline, using RabbitMQ Ian Cooper netstandard2.0;net8.0;net9.0 + enable RabbitMQ;AMQP;Command;Event;Service Activator;Decoupled;Invocation;Messaging;Remote;Command Dispatcher;Command Processor;Request;Service;Task Queue;Work Queue;Retry;Circuit Breaker;Availability diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/PullConsumer.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/PullConsumer.cs index aa27ecb8e6..46290f1747 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/PullConsumer.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/PullConsumer.cs @@ -22,111 +22,119 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #endregion - using System; + +using System; using System.Collections.Concurrent; +using System.Threading; using System.Threading.Tasks; - using Microsoft.Extensions.Logging; - using Paramore.Brighter.Logging; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Logging; using RabbitMQ.Client; using RabbitMQ.Client.Events; -namespace Paramore.Brighter.MessagingGateway.RMQ +namespace Paramore.Brighter.MessagingGateway.RMQ; + +public class PullConsumer(IChannel channel) : AsyncDefaultBasicConsumer(channel) { - public class PullConsumer : DefaultBasicConsumer + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + + //we do end up creating a second buffer to the Brighter Channel, but controlling the flow from RMQ depends + //on us being able to buffer up to the set QoS and then pull. This matches other implementations. + private readonly ConcurrentQueue _messages = new(); + + /// + /// Sets the number of messages to fetch from the broker in a single batch + /// + /// + /// Works on BasicConsume, no impact on BasicGet§ + /// + /// The batch size defaults to 1 unless set on subscription + public async Task SetChannelBatchSizeAsync(ushort batchSize = 1) { - private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - - //we do end up creating a second buffer to the Brighter Channel, but controlling the flow from RMQ depends - //on us being able to buffer up to the set QoS and then pull. This matches other implementations. - private readonly ConcurrentQueue _messages = new ConcurrentQueue(); - - public PullConsumer(IModel channel, ushort batchSize) - : base(channel) - { - //set the number of messages to fetch -- defaults to 1 unless set on subscription, no impact on - //BasicGet, only works on BasicConsume - channel.BasicQos(0, batchSize, false); - } + await Channel.BasicQosAsync(0, batchSize, false); + } + + /// + /// Used to pull from the buffer of messages delivered to us via BasicConsumer + /// + /// The total time to spend waiting for the buffer to fill up to bufferSize + /// The size of the buffer we want to fill wit messages + /// A tuple containing: the number of messages in the buffer, and the buffer itself + public async Task<(int bufferIndex, BasicDeliverEventArgs[]? buffer)> DeQueue(TimeSpan timeOut, int bufferSize) + { + var now = DateTime.UtcNow; + var end = now.Add(timeOut); + var pause = (timeOut > TimeSpan.FromMilliseconds(25)) ? Convert.ToInt32(timeOut.TotalMilliseconds) / 5 : 5; + + + var buffer = new BasicDeliverEventArgs[bufferSize]; + var bufferIndex = 0; + - /// - /// Used to pull from the buffer of messages delivered to us via BasicConsumer - /// - /// The total time to spend waiting for the buffer to fill up to bufferSize - /// The size of the buffer we want to fill wit messages - /// A tuple containing: the number of messages in the buffer, and the buffer itself - public (int, BasicDeliverEventArgs[]) DeQueue(TimeSpan timeOut, int bufferSize) + while (now < end && bufferIndex < bufferSize) { - var now = DateTime.UtcNow; - var end = now.Add(timeOut); - var pause = (timeOut > TimeSpan.FromMilliseconds(25)) ? Convert.ToInt32(timeOut.TotalMilliseconds) / 5 : 5; - - - var buffer = new BasicDeliverEventArgs[bufferSize]; - var bufferIndex = 0; - - - while (now < end && bufferIndex < bufferSize) + if (_messages.TryDequeue(out BasicDeliverEventArgs? result)) { - if (_messages.TryDequeue(out BasicDeliverEventArgs result)) - { - buffer[bufferIndex] = result; - ++bufferIndex; - } - else - { - Task.Delay(pause).Wait(); - } - now = DateTime.UtcNow; + buffer[bufferIndex] = result; + ++bufferIndex; } - - return bufferIndex == 0 ? (0, null) : (bufferIndex, buffer); - } - - public override void HandleBasicDeliver( - string consumerTag, - ulong deliveryTag, - bool redelivered, - string exchange, - string routingKey, - IBasicProperties properties, - ReadOnlyMemory body) - { - //We have to copy the body, before returning, as the memory in body is pooled and may be re-used after (see base class documentation) - //See also https://docs.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines - var payload = new byte[body.Length]; - body.CopyTo(payload); - - _messages.Enqueue(new BasicDeliverEventArgs + else { - BasicProperties = properties, - Body = payload, - ConsumerTag = consumerTag, - DeliveryTag = deliveryTag, - Exchange = exchange, - Redelivered = redelivered, - RoutingKey = routingKey - }); + await Task.Delay(pause); + } + + now = DateTime.UtcNow; } - public override void OnCancel(params string[] consumerTags) + return bufferIndex == 0 ? (0, Array.Empty()) : (bufferIndex, buffer); + } + + public override Task HandleBasicDeliverAsync(string consumerTag, + ulong deliveryTag, + bool redelivered, + string exchange, + string routingKey, + IReadOnlyBasicProperties properties, + ReadOnlyMemory body, + CancellationToken cancellationToken = default) + { + //We have to copy the body, before returning, as the memory in body is pooled and may be re-used after (see base class documentation) + //See also https://docs.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines + var payload = new byte[body.Length]; + body.CopyTo(payload); + + + _messages.Enqueue(new BasicDeliverEventArgs(consumerTag, + deliveryTag, + redelivered, + exchange, + routingKey, + properties, + body, + cancellationToken)); + + return Task.CompletedTask; + } + + + protected override async Task OnCancelAsync(string[] consumerTags, + CancellationToken cancellationToken = default) + { + //try to nack anything in the buffer. + try { - //try to nack anything in the buffer. - try + foreach (var message in _messages) { - foreach (var message in _messages) - { - Model.BasicNack(message.DeliveryTag, false, true); - } + await Channel.BasicNackAsync(message.DeliveryTag, false, true, cancellationToken); } - catch (Exception e) - { - //don't impede shutdown, just log - s_logger.LogWarning("Tried to nack unhandled messages on shutdown but failed for {ErrorMessage}", - e.Message); - } - - base.OnCancel(); + } + catch (Exception e) + { + //don't impede shutdown, just log + s_logger.LogWarning("Tried to nack unhandled messages on shutdown but failed for {ErrorMessage}", + e.Message); } - } + await base.OnCancelAsync(consumerTags, cancellationToken); + } } diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageConsumer.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageConsumer.cs index 3290fcb68f..98524f3202 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageConsumer.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageConsumer.cs @@ -28,237 +28,215 @@ THE SOFTWARE. */ using System.IO; using System.Linq; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; +using Paramore.Brighter.Tasks; using Polly.CircuitBreaker; -using RabbitMQ.Client; using RabbitMQ.Client.Exceptions; -namespace Paramore.Brighter.MessagingGateway.RMQ +namespace Paramore.Brighter.MessagingGateway.RMQ; + +/// +/// Class RmqMessageConsumer. +/// The is used on the server to receive messages from the broker. It abstracts away the details of +/// inter-process communication tasks from the server. It handles subscription establishment, request reception and dispatching, +/// result sending, and error handling. +/// +public class RmqMessageConsumer : RmqMessageGateway, IAmAMessageConsumerSync, IAmAMessageConsumerAsync { + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + + private PullConsumer? _consumer; + private readonly ChannelName _queueName; + private readonly RoutingKeys _routingKeys; + private readonly bool _isDurable; + private readonly RmqMessageCreator _messageCreator; + private readonly Message _noopMessage = new(); + private readonly string _consumerTag; + private readonly OnMissingChannel _makeChannels; + private readonly ushort _batchSize; + private readonly bool _highAvailability; + private readonly ChannelName? _deadLetterQueueName; + private readonly RoutingKey? _deadLetterRoutingKey; + private readonly bool _hasDlq; + private readonly TimeSpan? _ttl; + private readonly int? _maxQueueLength; + /// - /// Class RmqMessageConsumer. - /// The is used on the server to receive messages from the broker. It abstracts away the details of - /// inter-process communication tasks from the server. It handles subscription establishment, request reception and dispatching, - /// result sending, and error handling. + /// Initializes a new instance of the class. /// - public class RmqMessageConsumer : RmqMessageGateway, IAmAMessageConsumer + /// + /// The queue name. + /// The routing key. + /// Is the queue definition persisted + /// Is the queue available on all nodes in a cluster + /// How many messages to retrieve at one time; ought to be size of channel buffer + /// The dead letter queue + /// The routing key for dead letter messages + /// How long before a message on the queue expires. Defaults to infinite + /// How lare can the buffer grow before we stop accepting new work? + /// Should we validate, or create missing channels + public RmqMessageConsumer( + RmqMessagingGatewayConnection connection, + ChannelName queueName, + RoutingKey routingKey, + bool isDurable, + bool highAvailability = false, + int batchSize = 1, + ChannelName? deadLetterQueueName = null, + RoutingKey? deadLetterRoutingKey = null, + TimeSpan? ttl = null, + int? maxQueueLength = null, + OnMissingChannel makeChannels = OnMissingChannel.Create) + : this(connection, queueName, new RoutingKeys([routingKey]), isDurable, highAvailability, + batchSize, deadLetterQueueName, deadLetterRoutingKey, ttl, maxQueueLength, makeChannels) { - private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - - private PullConsumer _consumer; - private readonly ChannelName _queueName; - private readonly RoutingKeys _routingKeys; - private readonly bool _isDurable; - private readonly RmqMessageCreator _messageCreator; - private readonly Message _noopMessage = new Message(); - private readonly string _consumerTag; - private readonly OnMissingChannel _makeChannels; - private readonly ushort _batchSize; - private readonly bool _highAvailability; - private readonly ChannelName _deadLetterQueueName; - private readonly RoutingKey _deadLetterRoutingKey; - private readonly bool _hasDlq; - private readonly TimeSpan? _ttl; - private readonly int? _maxQueueLength; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The queue name. - /// The routing key. - /// Is the queue definition persisted - /// Is the queue available on all nodes in a cluster - /// How many messages to retrieve at one time; ought to be size of channel buffer - /// The dead letter queue - /// The routing key for dead letter messages - /// How long before a message on the queue expires. Defaults to infinite - /// How lare can the buffer grow before we stop accepting new work? - /// Should we validate, or create missing channels - public RmqMessageConsumer( - RmqMessagingGatewayConnection connection, - ChannelName queueName, - RoutingKey routingKey, - bool isDurable, - bool highAvailability = false, - int batchSize = 1, - ChannelName deadLetterQueueName = null, - RoutingKey deadLetterRoutingKey = null, - TimeSpan? ttl = null, - int? maxQueueLength = null, - OnMissingChannel makeChannels = OnMissingChannel.Create) - : this(connection, queueName, new RoutingKeys([routingKey]), isDurable, highAvailability, - batchSize, deadLetterQueueName, deadLetterRoutingKey, ttl, maxQueueLength, makeChannels) - { - } + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The queue name. + /// The routing keys. + /// Is the queue persisted to disk + /// Are the queues mirrored across nodes of the cluster + /// How many messages to retrieve at one time; ought to be size of channel buffer + /// The dead letter queue + /// The routing key for dead letter messages + /// How long before a message on the queue expires. Defaults to infinite + /// The maximum number of messages on the queue before we begin to reject publication of messages + /// Should we validate or create missing channels + public RmqMessageConsumer( + RmqMessagingGatewayConnection connection, + ChannelName queueName, + RoutingKeys routingKeys, + bool isDurable, + bool highAvailability = false, + int batchSize = 1, + ChannelName? deadLetterQueueName = null, + RoutingKey? deadLetterRoutingKey = null, + TimeSpan? ttl = null, + int? maxQueueLength = null, + OnMissingChannel makeChannels = OnMissingChannel.Create) + : base(connection) + { + _queueName = queueName; + _routingKeys = routingKeys; + _isDurable = isDurable; + _highAvailability = highAvailability; + _messageCreator = new RmqMessageCreator(); + _batchSize = Convert.ToUInt16(batchSize); + _makeChannels = makeChannels; + _consumerTag = Connection.Name + Guid.NewGuid(); + _deadLetterQueueName = deadLetterQueueName; + _deadLetterRoutingKey = deadLetterRoutingKey; + _hasDlq = !string.IsNullOrEmpty(deadLetterQueueName!) && !string.IsNullOrEmpty(_deadLetterRoutingKey!); + _ttl = ttl; + _maxQueueLength = maxQueueLength; + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The queue name. - /// The routing keys. - /// Is the queue persisted to disk - /// Are the queues mirrored across nodes of the cluster - /// How many messages to retrieve at one time; ought to be size of channel buffer - /// The dead letter queue - /// The routing key for dead letter messages - /// How long before a message on the queue expires. Defaults to infinite - /// The maximum number of messages on the queue before we begin to reject publication of messages - /// Should we validate or create missing channels - public RmqMessageConsumer( - RmqMessagingGatewayConnection connection, - ChannelName queueName, - RoutingKeys routingKeys, - bool isDurable, - bool highAvailability = false, - int batchSize = 1, - ChannelName deadLetterQueueName = null, - RoutingKey deadLetterRoutingKey = null, - TimeSpan? ttl = null, - int? maxQueueLength = null, - OnMissingChannel makeChannels = OnMissingChannel.Create) - : base(connection) + /// + /// Acknowledges the specified message. + /// + /// The message. + public void Acknowledge(Message message) => BrighterSynchronizationHelper.Run(() =>AcknowledgeAsync(message)); + + public async Task AcknowledgeAsync(Message message, CancellationToken cancellationToken = default) + { + var deliveryTag = message.DeliveryTag; + try { - _queueName = queueName; - _routingKeys = routingKeys; - _isDurable = isDurable; - _highAvailability = highAvailability; - _messageCreator = new RmqMessageCreator(); - _batchSize = Convert.ToUInt16(batchSize); - _makeChannels = makeChannels; - _consumerTag = Connection.Name + Guid.NewGuid(); - _deadLetterQueueName = deadLetterQueueName; - _deadLetterRoutingKey = deadLetterRoutingKey; - _hasDlq = !string.IsNullOrEmpty(deadLetterQueueName) && !string.IsNullOrEmpty(_deadLetterRoutingKey); - _ttl = ttl; - _maxQueueLength = maxQueueLength; + await EnsureBrokerAsync(cancellationToken: cancellationToken); + + if (Channel is null) throw new ChannelFailureException($"RmqMessageConsumer: channel {_queueName.Value} is null"); + + s_logger.LogInformation( + "RmqMessageConsumer: Acknowledging message {Id} as completed with delivery tag {DeliveryTag}", + message.Id, deliveryTag); + await Channel.BasicAckAsync(deliveryTag, false, cancellationToken); } - - /// - /// Acknowledges the specified message. - /// - /// The message. - public void Acknowledge(Message message) + catch (Exception exception) { - var deliveryTag = message.DeliveryTag; - try - { - EnsureBroker(); - s_logger.LogInformation( - "RmqMessageConsumer: Acknowledging message {Id} as completed with delivery tag {DeliveryTag}", - message.Id, deliveryTag); - Channel.BasicAck(deliveryTag, false); - } - catch (Exception exception) - { - s_logger.LogError(exception, - "RmqMessageConsumer: Error acknowledging message {Id} as completed with delivery tag {DeliveryTag}", - message.Id, deliveryTag); - throw; - } + s_logger.LogError(exception, + "RmqMessageConsumer: Error acknowledging message {Id} as completed with delivery tag {DeliveryTag}", + message.Id, deliveryTag); + throw; } + } - /// - /// Purges the specified queue name. - /// - public void Purge() + /// + /// Purges the specified queue name. + /// + public void Purge() => BrighterSynchronizationHelper.Run(() => PurgeAsync()); + + public async Task PurgeAsync(CancellationToken cancellationToken = default) + { + try { - try - { - //Why bind a queue? Because we use purge to initialize a queue for RPC - EnsureChannel(); - s_logger.LogDebug("RmqMessageConsumer: Purging channel {ChannelName}", _queueName.Value); + //Why bind a queue? Because we use purge to initialize a queue for RPC + await EnsureChannelAsync(cancellationToken); + + if (Channel is null) throw new ChannelFailureException($"RmqMessageConsumer: channel {_queueName.Value} is null"); - try { Channel.QueuePurge(_queueName.Value); } - catch (OperationInterruptedException operationInterruptedException) - { - if (operationInterruptedException.ShutdownReason.ReplyCode == 404) { return; } + s_logger.LogDebug("RmqMessageConsumer: Purging channel {ChannelName}", _queueName.Value); - throw; - } - } - catch (Exception exception) + try { - s_logger.LogError(exception, "RmqMessageConsumer: Error purging channel {ChannelName}", - _queueName.Value); - throw; + await Channel.QueuePurgeAsync(_queueName.Value, cancellationToken); } - } - - /// - /// Requeues the specified message. - /// - /// - /// Time to delay delivery of the message. - /// True if message deleted, false otherwise - public bool Requeue(Message message, TimeSpan? timeout = null) - { - timeout ??= TimeSpan.Zero; - - try + catch (OperationInterruptedException operationInterruptedException) { - s_logger.LogDebug("RmqMessageConsumer: Re-queueing message {Id} with a delay of {Delay} milliseconds", message.Id, timeout.Value.TotalMilliseconds); - EnsureBroker(_queueName); - - var rmqMessagePublisher = new RmqMessagePublisher(Channel, Connection); - if (DelaySupported) + if (operationInterruptedException.ShutdownReason?.ReplyCode == 404) { - rmqMessagePublisher.RequeueMessage(message, _queueName, timeout.Value); + return; } - else - { - if (timeout > TimeSpan.Zero) Task.Delay(timeout.Value).Wait(); - rmqMessagePublisher.RequeueMessage(message, _queueName, TimeSpan.Zero); - } - - //ack the original message to remove it from the queue - var deliveryTag = message.DeliveryTag; - s_logger.LogInformation( - "RmqMessageConsumer: Deleting message {Id} with delivery tag {DeliveryTag} as re-queued", - message.Id, deliveryTag); - Channel.BasicAck(deliveryTag, false); - return true; - } - catch (Exception exception) - { - s_logger.LogError(exception, "RmqMessageConsumer: Error re-queueing message {Id}", message.Id); - return false; + throw; } } - - /// - /// Rejects the specified message. - /// - /// The message. - public void Reject(Message message) + catch (Exception exception) { - try - { - EnsureBroker(_queueName); - s_logger.LogInformation("RmqMessageConsumer: NoAck message {Id} with delivery tag {DeliveryTag}", - message.Id, message.DeliveryTag); - //if we have a DLQ, this will force over to the DLQ - Channel.BasicReject(message.DeliveryTag, false); - } - catch (Exception exception) - { - s_logger.LogError(exception, "RmqMessageConsumer: Error try to NoAck message {Id}", message.Id); - throw; - } + s_logger.LogError(exception, "RmqMessageConsumer: Error purging channel {ChannelName}", + _queueName.Value); + throw; } + } + + /// + /// Receives the specified queue name. + /// + /// + /// Sync over async as RMQ does not support a sync consumer - Brighter pauses the message pump + /// whilst waiting anyway, so it is unlikely to deadlock + /// + /// The timeout in milliseconds. We retry on timeout 5 ms intervals, with a min of 5ms + /// until the timeout value is reached. + /// Message. + public Message[] Receive(TimeSpan? timeOut = null) => BrighterSynchronizationHelper.Run(async () => await ReceiveAsync(timeOut)); - /// - /// Receives the specified queue name. - /// - /// The timeout in milliseconds. We retry on timeout 5 ms intervals, with a min of 5ms - /// until the timeout value is reached. - /// Message. - public Message[] Receive(TimeSpan? timeOut = null) + /// + /// Receives the specified queue name. + /// + /// The timeout in milliseconds. We retry on timeout 5 ms intervals, with a min of 5ms + /// until the timeout value is reached. + /// The for the receive operation + /// Message. + public async Task ReceiveAsync(TimeSpan? timeOut = null, CancellationToken cancellationToken = default(CancellationToken)) + { + + timeOut ??= TimeSpan.FromMilliseconds(5); + + try { + await EnsureChannelAsync(cancellationToken); + + if (_consumer is null) throw new ChannelFailureException($"RmwMessageConsumer: consumer for {_queueName.Value} is null"); + if (Connection.Exchange is null) throw new ConfigurationException($"RmqMessageConsumer: exchange for {_queueName.Value} is null"); + if (Connection.AmpqUri is null) throw new ConfigurationException($"RmqMessageConsumer: ampqUri for {_queueName.Value} is null"); + s_logger.LogDebug( "RmqMessageConsumer: Preparing to retrieve next message from queue {ChannelName} with routing key {RoutingKeys} via exchange {ExchangeName} on subscription {URL}", _queueName.Value, @@ -266,228 +244,349 @@ public Message[] Receive(TimeSpan? timeOut = null) Connection.Exchange.Name, Connection.AmpqUri.GetSanitizedUri() ); + + var (resultCount, results) = await _consumer.DeQueue(timeOut.Value, _batchSize); - timeOut ??= TimeSpan.FromMilliseconds(5); - - try - { - EnsureChannel(); - - var (resultCount, results) = _consumer.DeQueue(timeOut.Value, _batchSize); - - if (results != null && results.Length != 0) - { - var messages = new Message[resultCount]; - for (var i = 0; i < resultCount; i++) - { - var message = _messageCreator.CreateMessage(results[i]); - messages[i] = message; - - s_logger.LogInformation( - "RmqMessageConsumer: Received message from queue {ChannelName} with routing key {RoutingKeys} via exchange {ExchangeName} on subscription {URL}, message: {Request}", - _queueName.Value, - string.Join(";", _routingKeys.Select(rk => rk.Value)), - Connection.Exchange.Name, - Connection.AmpqUri.GetSanitizedUri(), - JsonSerializer.Serialize(message, JsonSerialisationOptions.Options) - ); - } - - return messages; - } - else - { - return new Message[] { _noopMessage }; - } - } - catch (Exception exception) when (exception is BrokerUnreachableException || - exception is AlreadyClosedException || - exception is TimeoutException) - { - HandleException(exception, true); - } - catch (Exception exception) when (exception is EndOfStreamException || - exception is OperationInterruptedException || - exception is NotSupportedException || - exception is BrokenCircuitException) - { - HandleException(exception); - } - catch (Exception exception) - { - HandleException(exception); - } - - return new Message[] { _noopMessage }; // Default return in case of exception - } - - protected virtual void EnsureChannel() - { - if (Channel == null || Channel.IsClosed) + if (results is not null && results.Length == 0) return [_noopMessage]; + + var messages = new Message[resultCount]; + for (var i = 0; i < resultCount; i++) { - EnsureBroker(_queueName); - - if (_makeChannels == OnMissingChannel.Create) - { - CreateQueue(); - BindQueue(); - } - else if (_makeChannels == OnMissingChannel.Validate) - { - ValidateQueue(); - } - else if (_makeChannels == OnMissingChannel.Assume) - { - ; //-- pass, here for clarity on fall through to use of queue directly on assume - } - - CreateConsumer(); + var message = _messageCreator.CreateMessage(results![i]); + messages[i] = message; s_logger.LogInformation( - "RmqMessageConsumer: Created rabbitmq channel {ConsumerNumber} for queue {ChannelName} with routing key/s {RoutingKeys} via exchange {ExchangeName} on subscription {URL}", - Channel?.ChannelNumber, + "RmqMessageConsumer: Received message from queue {ChannelName} with routing key {RoutingKeys} via exchange {ExchangeName} on subscription {URL}, message: {Request}", _queueName.Value, string.Join(";", _routingKeys.Select(rk => rk.Value)), Connection.Exchange.Name, - Connection.AmpqUri.GetSanitizedUri() + Connection.AmpqUri.GetSanitizedUri(), + JsonSerializer.Serialize(message, JsonSerialisationOptions.Options) ); } + + return messages; + } + catch (Exception exception) when (exception is BrokerUnreachableException || + exception is AlreadyClosedException || + exception is TimeoutException) + { + await HandleExceptionAsync(exception, true, cancellationToken); + } + catch (Exception exception) when (exception is EndOfStreamException || + exception is OperationInterruptedException || + exception is NotSupportedException || + exception is BrokenCircuitException) + { + await HandleExceptionAsync(exception, cancellationToken: cancellationToken); + } + catch (Exception exception) + { + await HandleExceptionAsync(exception, cancellationToken: cancellationToken); + } + + return [_noopMessage]; // Default return in case of exception + } + + /// + /// Requeues the specified message. + /// + /// + /// Time to delay delivery of the message. + /// True if message deleted, false otherwise + public bool Requeue(Message message, TimeSpan? timeout = null) => BrighterSynchronizationHelper.Run(() => RequeueAsync(message, timeout)); + + public async Task RequeueAsync(Message message, TimeSpan? timeout = null, + CancellationToken cancellationToken = default) + { + timeout ??= TimeSpan.Zero; - private void CancelConsumer() + try { - if (_consumer != null) + s_logger.LogDebug("RmqMessageConsumer: Re-queueing message {Id} with a delay of {Delay} milliseconds", + message.Id, timeout.Value.TotalMilliseconds); + + await EnsureChannelAsync(cancellationToken); + + if (Channel is null) throw new ChannelFailureException($"RmqMessageConsumer: channel {_queueName.Value} is null"); + + var rmqMessagePublisher = new RmqMessagePublisher(Channel, Connection); + if (DelaySupported) { - if (_consumer.IsRunning) + await rmqMessagePublisher.RequeueMessageAsync(message, _queueName, timeout.Value, cancellationToken); + } + else + { + if (timeout > TimeSpan.Zero) { - Channel.BasicCancel(_consumerTag); + await Task.Delay(timeout.Value, cancellationToken); } - _consumer = null; + await rmqMessagePublisher.RequeueMessageAsync(message, _queueName, TimeSpan.Zero, cancellationToken); } + + //ack the original message to remove it from the queue + var deliveryTag = message.DeliveryTag; + s_logger.LogInformation( + "RmqMessageConsumer: Deleting message {Id} with delivery tag {DeliveryTag} as re-queued", + message.Id, deliveryTag); + await Channel.BasicAckAsync(deliveryTag, false, cancellationToken); + + return true; } + catch (Exception exception) + { + s_logger.LogError(exception, "RmqMessageConsumer: Error re-queueing message {Id}", message.Id); + return false; + } + } - private void CreateConsumer() + /// + /// Rejects the specified message. + /// + /// The message. + public void Reject(Message message) => BrighterSynchronizationHelper.Run(async () => await RejectAsync(message)); + + public async Task RejectAsync(Message message, CancellationToken cancellationToken = default) + { + try { - _consumer = new PullConsumer(Channel, _batchSize); + await EnsureBrokerAsync(_queueName, cancellationToken: cancellationToken); + + if (Channel is null) throw new InvalidOperationException($"RmqMessageConsumer: channel {_queueName.Value} is null"); + + s_logger.LogInformation("RmqMessageConsumer: NoAck message {Id} with delivery tag {DeliveryTag}", + message.Id, message.DeliveryTag); + //if we have a DLQ, this will force over to the DLQ + await Channel.BasicRejectAsync(message.DeliveryTag, false, cancellationToken); + } + catch (Exception exception) + { + s_logger.LogError(exception, "RmqMessageConsumer: Error try to NoAck message {Id}", message.Id); + throw; + } + } - Channel.BasicConsume(_queueName.Value, false, _consumerTag, SetQueueArguments(), _consumer); + protected virtual async Task EnsureChannelAsync(CancellationToken cancellationToken = default) + { + if (Channel == null || Channel.IsClosed) + { + await EnsureBrokerAsync(_queueName, cancellationToken: cancellationToken); - _consumer.HandleBasicConsumeOk(_consumerTag); + if (_makeChannels == OnMissingChannel.Create) + { + await CreateQueueAsync(cancellationToken); + await BindQueueAsync(cancellationToken); + } + else if (_makeChannels == OnMissingChannel.Validate) + { + await ValidateQueueAsync(cancellationToken); + } + else if (_makeChannels == OnMissingChannel.Assume) + { + //-- pass, here for clarity on fall through to use of queue directly on assume + } + + await CreateConsumerAsync(cancellationToken); + + if (Channel is null) throw new ChannelFailureException($"RmqMessageConsumer: channel {_queueName.Value} is null"); + if (Connection.Exchange is null) throw new ConfigurationException($"RmqMessageConsumer: exchange for {_queueName.Value} is null"); + if (Connection.AmpqUri is null) throw new ConfigurationException($"RmqMessageConsumer: ampqUri for {_queueName.Value} is null"); s_logger.LogInformation( - "RmqMessageConsumer: Created consumer for queue {ChannelName} with routing key {Topic} via exchange {ExchangeName} on subscription {URL}", + "RmqMessageConsumer: Created rabbitmq channel {ConsumerNumber} for queue {ChannelName} with routing key/s {RoutingKeys} via exchange {ExchangeName} on subscription {URL}", + Channel.ChannelNumber, _queueName.Value, string.Join(";", _routingKeys.Select(rk => rk.Value)), Connection.Exchange.Name, Connection.AmpqUri.GetSanitizedUri() ); } + } - private void CreateQueue() - { - s_logger.LogDebug("RmqMessageConsumer: Creating queue {ChannelName} on subscription {URL}", - _queueName.Value, Connection.AmpqUri.GetSanitizedUri()); - Channel.QueueDeclare(_queueName.Value, _isDurable, false, false, SetQueueArguments()); - if (_hasDlq) Channel.QueueDeclare(_deadLetterQueueName.Value, _isDurable, false, false); - } - - private void BindQueue() + private async Task CancelConsumerAsync(CancellationToken cancellationToken) + { + if (_consumer != null && Channel != null) { - foreach (var key in _routingKeys) + if (_consumer.IsRunning) { - Channel.QueueBind(_queueName.Value, Connection.Exchange.Name, key); + await Channel.BasicCancelAsync(_consumerTag, cancellationToken: cancellationToken); } - if (_hasDlq) - Channel.QueueBind(_deadLetterQueueName.Value, GetDeadletterExchangeName(), _deadLetterRoutingKey.Value); + _consumer = null; } + } + + private async Task CreateConsumerAsync(CancellationToken cancellationToken) + { + if (Channel is null) throw new ChannelFailureException($"RmqMessageConsumer: channel {_queueName.Value} is null"); + if (Connection.Exchange is null) throw new ConfigurationException($"RmqMessageConsumer: exchange for {_queueName.Value} is null"); + if (Connection.AmpqUri is null) throw new ConfigurationException($"RmqMessageConsumer: ampqUri for {_queueName.Value} is null"); + + _consumer = new PullConsumer(Channel); + if (_consumer is null) throw new InvalidOperationException($"RmqMessageConsumer: consumer for {_queueName.Value} is null"); + + await _consumer.SetChannelBatchSizeAsync(_batchSize); + + await Channel.BasicConsumeAsync(_queueName.Value, + false, + _consumerTag, + true, + false, + SetQueueArguments(), + _consumer, + cancellationToken: cancellationToken); + + await _consumer.HandleBasicConsumeOkAsync(_consumerTag, cancellationToken); + + s_logger.LogInformation( + "RmqMessageConsumer: Created consumer for queue {ChannelName} with routing key {Topic} via exchange {ExchangeName} on subscription {URL}", + _queueName.Value, + string.Join(";", _routingKeys.Select(rk => rk.Value)), + Connection.Exchange.Name, + Connection.AmpqUri.GetSanitizedUri() + ); + } - private void HandleException(Exception exception, bool resetConnection = false) + private async Task CreateQueueAsync(CancellationToken cancellationToken) + { + if (Channel is null) throw new ChannelFailureException($"RmqMessageConsumer: channel {_queueName.Value} is null"); + if (Connection.Exchange is null) throw new ConfigurationException($"RmqMessageConsumer: exchange for {_queueName.Value} is null"); + if (Connection.AmpqUri is null) throw new ConfigurationException($"RmqMessageConsumer: ampqUri for {_queueName.Value} is null"); + + s_logger.LogDebug("RmqMessageConsumer: Creating queue {ChannelName} on subscription {URL}", + _queueName.Value, Connection.AmpqUri.GetSanitizedUri()); + await Channel.QueueDeclareAsync(_queueName.Value, _isDurable, false, false, SetQueueArguments(), + cancellationToken: cancellationToken); + + if (_hasDlq) { - s_logger.LogError(exception, - "RmqMessageConsumer: There was an error listening to queue {ChannelName} via exchange {RoutingKeys} via exchange {ExchangeName} on subscription {URL}", - _queueName.Value, - string.Join(";", _routingKeys.Select(rk => rk.Value)), - Connection.Exchange.Name, - Connection.AmpqUri.GetSanitizedUri() - ); - if (resetConnection) ResetConnectionToBroker(); - throw new ChannelFailureException("Error connecting to RabbitMQ, see inner exception for details", exception); + await Channel.QueueDeclareAsync(_deadLetterQueueName!.Value, _isDurable, false, false, + cancellationToken: cancellationToken); } + } + + private async Task BindQueueAsync(CancellationToken cancellationToken) + { + if (Channel is null) throw new ChannelFailureException($"RmqMessageConsumer: channel {_queueName.Value} is null"); + if (Connection.Exchange is null) throw new ConfigurationException($"RmqMessageConsumer: exchange for {_queueName.Value} is null"); + if (Connection.AmpqUri is null) throw new ConfigurationException($"RmqMessageConsumer: ampqUri for {_queueName.Value} is null"); - private void ValidateQueue() + foreach (var key in _routingKeys) { - s_logger.LogDebug("RmqMessageConsumer: Validating queue {ChannelName} on subscription {URL}", - _queueName.Value, Connection.AmpqUri.GetSanitizedUri()); - - try - { - Channel.QueueDeclarePassive(_queueName.Value); - } - catch (Exception e) - { - throw new BrokerUnreachableException(e); - } + await Channel.QueueBindAsync(_queueName.Value, Connection.Exchange.Name, key, + cancellationToken: cancellationToken); } - private Dictionary SetQueueArguments() + if (_hasDlq) { - var arguments = new Dictionary(); - if (_highAvailability) - { - // Only work for RabbitMQ Server version before 3.0 - //http://www.rabbitmq.com/blog/2012/11/19/breaking-things-with-rabbitmq-3-0/ - arguments.Add("x-ha-policy", "all"); - } - - if (_hasDlq) - { - //You can set a different exchange for the DLQ to the Queue - arguments.Add("x-dead-letter-exchange", GetDeadletterExchangeName()); - arguments.Add("x-dead-letter-routing-key", _deadLetterRoutingKey.Value); - } + await Channel.QueueBindAsync(_deadLetterQueueName!.Value, GetDeadletterExchangeName(), + _deadLetterRoutingKey!.Value, cancellationToken: cancellationToken); + } + } - if (_ttl.HasValue) - { - arguments.Add("x-message-ttl", _ttl.Value.Milliseconds); - } + private async Task HandleExceptionAsync(Exception exception, bool resetConnection = false, CancellationToken cancellationToken = default) + { + if (Connection.Exchange is null) throw new ConfigurationException($"RmqMessageConsumer: exchange for {_queueName.Value} is null", exception); + if (Connection.AmpqUri is null) throw new ConfigurationException($"RmqMessageConsumer: ampqUri for {_queueName.Value} is null", exception); + + s_logger.LogError(exception, + "RmqMessageConsumer: There was an error listening to queue {ChannelName} via exchange {RoutingKeys} via exchange {ExchangeName} on subscription {URL}", + _queueName.Value, + string.Join(";", _routingKeys.Select(rk => rk.Value)), + Connection.Exchange.Name, + Connection.AmpqUri.GetSanitizedUri() + ); + + if (resetConnection) await ResetConnectionToBrokerAsync(cancellationToken); + throw new ChannelFailureException("Error connecting to RabbitMQ, see inner exception for details", exception); + } - if (_maxQueueLength.HasValue) - { - arguments.Add("x-max-length", _maxQueueLength.Value); - if (_hasDlq) - { - arguments.Add("x-overflow", "reject-publish-dlx"); - } + private async Task ValidateQueueAsync(CancellationToken cancellationToken) + { + if (Channel is null) throw new ChannelFailureException($"RmqMessageConsumer: channel {_queueName.Value} is null"); + if (Connection.Exchange is null) throw new ConfigurationException($"RmqMessageConsumer: exchange for {_queueName.Value} is null"); + if (Connection.AmpqUri is null) throw new ConfigurationException($"RmqMessageConsumer: ampqUri for {_queueName.Value} is null"); - arguments.Add("x-overflow", "reject-publish"); - } + s_logger.LogDebug("RmqMessageConsumer: Validating queue {ChannelName} on subscription {URL}", + _queueName.Value, Connection.AmpqUri.GetSanitizedUri()); - return arguments; + try + { + await Channel.QueueDeclarePassiveAsync(_queueName.Value, cancellationToken); } + catch (Exception e) + { + throw new BrokerUnreachableException(e); + } + } - private string GetDeadletterExchangeName() + private Dictionary SetQueueArguments() + { + var arguments = new Dictionary(); + if (_highAvailability) { - return Connection.DeadLetterExchange == null - ? Connection.Exchange.Name - : Connection.DeadLetterExchange.Name; + // Only work for RabbitMQ Server version before 3.0 + //http://www.rabbitmq.com/blog/2012/11/19/breaking-things-with-rabbitmq-3-0/ + arguments.Add("x-ha-policy", "all"); } + if (_hasDlq) + { + //You can set a different exchange for the DLQ to the Queue + arguments.Add("x-dead-letter-exchange", GetDeadletterExchangeName()); + arguments.Add("x-dead-letter-routing-key", _deadLetterRoutingKey?.Value); + } - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public override void Dispose() + if (_ttl.HasValue) { - CancelConsumer(); - Dispose(true); - GC.SuppressFinalize(this); + arguments.Add("x-message-ttl", _ttl.Value.Milliseconds); } - ~RmqMessageConsumer() + if (_maxQueueLength.HasValue) { - Dispose(false); + arguments.Add("x-max-length", _maxQueueLength.Value); + if (_hasDlq) + { + arguments.Add("x-overflow", "reject-publish-dlx"); + } + + arguments.Add("x-overflow", "reject-publish"); } + + return arguments; + } + + private string GetDeadletterExchangeName() + { + //never likely to happen as caller will generally have asserted this + if (Connection.Exchange is null) throw new ConfigurationException($"RmqMessageConsumer: exchange for {_queueName.Value} is null"); + + return Connection.DeadLetterExchange is not null ? Connection.DeadLetterExchange.Name : Connection.Exchange.Name; + } + + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public override void Dispose() + { + // A wait here, will die in the Brighter synchronization context + // CancelConsumerAsync(CancellationToken.None).GetAwaiter().GetResult(); + Dispose(true); + GC.SuppressFinalize(this); + } + + public override async ValueTask DisposeAsync() + { + await CancelConsumerAsync(CancellationToken.None); + Dispose(true); + GC.SuppressFinalize(this); + } + + ~RmqMessageConsumer() + { + Dispose(false); } } diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageConsumerFactory.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageConsumerFactory.cs index 0d77edf284..f5c550cb1b 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageConsumerFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageConsumerFactory.cs @@ -41,10 +41,30 @@ public RmqMessageConsumerFactory(RmqMessagingGatewayConnection rmqConnection) /// Creates a consumer for the specified queue. /// /// The queue to connect to - /// IAmAMessageConsumer. - public IAmAMessageConsumer Create(Subscription subscription) + /// IAmAMessageConsumerSync. + public IAmAMessageConsumerSync Create(Subscription subscription) { - RmqSubscription rmqSubscription = subscription as RmqSubscription; + RmqSubscription? rmqSubscription = subscription as RmqSubscription; + if (rmqSubscription == null) + throw new ConfigurationException("We expect an SQSConnection or SQSConnection as a parameter"); + + return new RmqMessageConsumer( + _rmqConnection, + rmqSubscription.ChannelName, //RMQ Queue Name + rmqSubscription.RoutingKey, + rmqSubscription.IsDurable, + rmqSubscription.HighAvailability, + rmqSubscription.BufferSize, + rmqSubscription.DeadLetterChannelName, + rmqSubscription.DeadLetterRoutingKey, + rmqSubscription.Ttl, + rmqSubscription.MaxQueueLength, + subscription.MakeChannels); + } + + public IAmAMessageConsumerAsync CreateAsync(Subscription subscription) + { + RmqSubscription? rmqSubscription = subscription as RmqSubscription; if (rmqSubscription == null) throw new ConfigurationException("We expect an SQSConnection or SQSConnection as a parameter"); diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageCreator.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageCreator.cs index 571520773c..532a2d5b47 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageCreator.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageCreator.cs @@ -33,268 +33,263 @@ THE SOFTWARE. */ using RabbitMQ.Client; using RabbitMQ.Client.Events; -namespace Paramore.Brighter.MessagingGateway.RMQ +namespace Paramore.Brighter.MessagingGateway.RMQ; + +internal class RmqMessageCreator { - internal class RmqMessageCreator + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + + public Message CreateMessage(BasicDeliverEventArgs fromQueue) { - private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + var headers = fromQueue.BasicProperties.Headers ?? new Dictionary(); + var topic = HeaderResult.Empty(); + var messageId = HeaderResult.Empty(); + var deliveryMode = fromQueue.BasicProperties.DeliveryMode; - public Message CreateMessage(BasicDeliverEventArgs fromQueue) + Message message; + try { - var headers = fromQueue.BasicProperties.Headers ?? new Dictionary(); - var topic = HeaderResult.Empty(); - var messageId = HeaderResult.Empty(); - var timeStamp = HeaderResult.Empty(); - var handledCount = HeaderResult.Empty(); - var delay = HeaderResult.Empty(); - var redelivered = HeaderResult.Empty(); - var deliveryTag = HeaderResult.Empty(); - var messageType = HeaderResult.Empty(); - var replyTo = HeaderResult.Empty(); - var deliveryMode = fromQueue.BasicProperties.DeliveryMode; - - Message message; - try + topic = ReadTopic(fromQueue, headers); + messageId = ReadMessageId(fromQueue.BasicProperties.MessageId); + HeaderResult timeStamp = ReadTimeStamp(fromQueue.BasicProperties); + HeaderResult handledCount = ReadHandledCount(headers); + HeaderResult delay = ReadDelay(headers); + HeaderResult redelivered = ReadRedeliveredFlag(fromQueue.Redelivered); + HeaderResult deliveryTag = ReadDeliveryTag(fromQueue.DeliveryTag); + HeaderResult messageType = ReadMessageType(headers); + HeaderResult replyTo = ReadReplyTo(fromQueue.BasicProperties); + + if (false == (topic.Success && messageId.Success && messageType.Success && timeStamp.Success && handledCount.Success)) { - topic = ReadTopic(fromQueue, headers); - messageId = ReadMessageId(fromQueue.BasicProperties.MessageId); - timeStamp = ReadTimeStamp(fromQueue.BasicProperties); - handledCount = ReadHandledCount(headers); - delay = ReadDelay(headers); - redelivered = ReadRedeliveredFlag(fromQueue.Redelivered); - deliveryTag = ReadDeliveryTag(fromQueue.DeliveryTag); - messageType = ReadMessageType(headers); - replyTo = ReadReplyTo(fromQueue.BasicProperties); - - if (false == (topic.Success && messageId.Success && messageType.Success && timeStamp.Success && handledCount.Success)) - { - message = FailureMessage(topic, messageId); - } - else - { - //TODO:CLOUD_EVENTS parse from headers + message = FailureMessage(topic, messageId); + } + else + { + //TODO:CLOUD_EVENTS parse from headers - var messageHeader = new MessageHeader( - messageId: messageId.Result, - topic: topic.Result, - messageType.Result, - source: null, - type: "", - timeStamp: timeStamp.Success ? timeStamp.Result : DateTime.UtcNow, - correlationId: "", - replyTo: new RoutingKey(replyTo.Result), - contentType: "", - handledCount: handledCount.Result, - dataSchema: null, - subject: null, - delayed: delay.Result - ); - - - //this effectively transfers ownership of our buffer - message = new Message(messageHeader, new MessageBody(fromQueue.Body, fromQueue.BasicProperties.Type)); - - headers.Each(header => message.Header.Bag.Add(header.Key, ParseHeaderValue(header.Value))); - } + var messageHeader = new MessageHeader( + messageId: messageId.Result ?? string.Empty, + topic: topic.Result ?? RoutingKey.Empty, + messageType.Result, + source: null, + type: "", + timeStamp: timeStamp.Success ? timeStamp.Result : DateTime.UtcNow, + correlationId: "", + replyTo: new RoutingKey(replyTo.Result ?? string.Empty), + contentType: "", + handledCount: handledCount.Result, + dataSchema: null, + subject: null, + delayed: delay.Result + ); + + //this effectively transfers ownership of our buffer + message = new Message(messageHeader, new MessageBody(fromQueue.Body, fromQueue.BasicProperties.Type ?? "plain/text")); + + headers.Each(header => message.Header.Bag.Add(header.Key, ParseHeaderValue(header.Value))); + } - if (headers.TryGetValue(HeaderNames.CORRELATION_ID, out object correlationHeader)) + if (headers.TryGetValue(HeaderNames.CORRELATION_ID, out object? correlationHeader)) + { + var bytes = (byte[]?)correlationHeader; + if (bytes != null) { - var correlationId = Encoding.UTF8.GetString((byte[])correlationHeader); + var correlationId = Encoding.UTF8.GetString(bytes); message.Header.CorrelationId = correlationId; } - - message.DeliveryTag = deliveryTag.Result; - message.Redelivered = redelivered.Result; - message.Header.ReplyTo = replyTo.Result; - message.Persist = deliveryMode == 2; - } - catch (Exception e) - { - s_logger.LogWarning(e,"Failed to create message from amqp message"); - message = FailureMessage(topic, messageId); } - return message; + message.DeliveryTag = deliveryTag.Result; + message.Redelivered = redelivered.Result; + message.Header.ReplyTo = replyTo.Result; + message.Persist = deliveryMode == DeliveryModes.Persistent; } - - - private HeaderResult ReadHeader(IDictionary dict, string key, bool dieOnMissing = false) + catch (Exception e) { - if (false == dict.TryGetValue(key, out object value)) - { - return new HeaderResult(string.Empty, !dieOnMissing); - } + s_logger.LogWarning(e,"Failed to create message from amqp message"); + message = FailureMessage(topic, messageId); + } - if (!(value is byte[] bytes)) - { - s_logger.LogWarning("The value of header {Key} could not be cast to a byte array", key); - return new HeaderResult(null, false); - } + return message; + } - try - { - var val = Encoding.UTF8.GetString(bytes); - return new HeaderResult(val, true); - } - catch (Exception e) - { - var firstTwentyBytes = BitConverter.ToString(bytes.Take(20).ToArray()); - s_logger.LogWarning(e,"Failed to read the value of header {Key} as UTF-8, first 20 byes follow: \n\t{1}", key, firstTwentyBytes); - return new HeaderResult(null, false); - } - } - private Message FailureMessage(HeaderResult topic, HeaderResult messageId) + private HeaderResult ReadHeader(IDictionary dict, string key, bool dieOnMissing = false) + { + if (false == dict.TryGetValue(key, out object? value)) { - var header = new MessageHeader( - messageId.Success ? messageId.Result : string.Empty, - topic.Success ? topic.Result : RoutingKey.Empty, - MessageType.MT_UNACCEPTABLE); - var message = new Message(header, new MessageBody(string.Empty)); - return message; + return new HeaderResult(string.Empty, !dieOnMissing); } - private HeaderResult ReadDeliveryTag(ulong deliveryTag) + if (!(value is byte[] bytes)) { - return new HeaderResult(deliveryTag, true); + s_logger.LogWarning("The value of header {Key} could not be cast to a byte array", key); + return new HeaderResult(null, false); } - private HeaderResult ReadTimeStamp(IBasicProperties basicProperties) + try { - if (basicProperties.IsTimestampPresent()) - { - return new HeaderResult(UnixTimestamp.DateTimeFromUnixTimestampSeconds(basicProperties.Timestamp.UnixTime), true); - } - - return new HeaderResult(DateTime.UtcNow, true); + var val = Encoding.UTF8.GetString(bytes); + return new HeaderResult(val, true); } - - private HeaderResult ReadMessageType(IDictionary headers) + catch (Exception e) { - return ReadHeader(headers, HeaderNames.MESSAGE_TYPE) - .Map(s => - { - if (string.IsNullOrEmpty(s)) - { - return new HeaderResult(MessageType.MT_EVENT, true); - } - - var success = Enum.TryParse(s, true, out MessageType result); - return new HeaderResult(result, success); - }); + var firstTwentyBytes = BitConverter.ToString(bytes.Take(20).ToArray()); + s_logger.LogWarning(e,"Failed to read the value of header {Key} as UTF-8, first 20 byes follow: \n\t{1}", key, firstTwentyBytes); + return new HeaderResult(null, false); } + } - private HeaderResult ReadHandledCount(IDictionary headers) + private Message FailureMessage(HeaderResult topic, HeaderResult messageId) + { + var header = new MessageHeader( + messageId.Success ? messageId.Result! : string.Empty, + topic.Success ? topic.Result! : RoutingKey.Empty, + MessageType.MT_UNACCEPTABLE); + var message = new Message(header, new MessageBody(string.Empty)); + return message; + } + + private static HeaderResult ReadDeliveryTag(ulong deliveryTag) + { + return new HeaderResult(deliveryTag, true); + } + + private static HeaderResult ReadTimeStamp(IReadOnlyBasicProperties basicProperties) + { + if (basicProperties.IsTimestampPresent()) { - if (headers.TryGetValue(HeaderNames.HANDLED_COUNT, out object header) == false) - { - return new HeaderResult(0, true); - } + return new HeaderResult(UnixTimestamp.DateTimeFromUnixTimestampSeconds(basicProperties.Timestamp.UnixTime), true); + } - switch (header) + return new HeaderResult(DateTime.UtcNow, true); + } + + private HeaderResult ReadMessageType(IDictionary headers) + { + return ReadHeader(headers, HeaderNames.MESSAGE_TYPE) + .Map(s => { - case byte[] value: + if (string.IsNullOrEmpty(s)) { - var val = int.TryParse(Encoding.UTF8.GetString(value), out var handledCount) ? handledCount : 0; - return new HeaderResult(val, true); + return new HeaderResult(MessageType.MT_EVENT, true); } - case int value: - return new HeaderResult(value, true); - default: - return new HeaderResult(0, true); - } + + var success = Enum.TryParse(s, true, out MessageType result); + return new HeaderResult(result, success); + }); + } + + private HeaderResult ReadHandledCount(IDictionary headers) + { + if (headers.TryGetValue(HeaderNames.HANDLED_COUNT, out object? header) == false) + { + return new HeaderResult(0, true); } - private HeaderResult ReadDelay(IDictionary headers) + switch (header) { - if (headers.ContainsKey(HeaderNames.DELAYED_MILLISECONDS) == false) + case byte[] value: { - return new HeaderResult(TimeSpan.Zero, true); + var val = int.TryParse(Encoding.UTF8.GetString(value), out var handledCount) ? handledCount : 0; + return new HeaderResult(val, true); } + case int value: + return new HeaderResult(value, true); + default: + return new HeaderResult(0, true); + } + } - int delayedMilliseconds; + private HeaderResult ReadDelay(IDictionary headers) + { + if (headers.ContainsKey(HeaderNames.DELAYED_MILLISECONDS) == false) + { + return new HeaderResult(TimeSpan.Zero, true); + } - // on 32 bit systems the x-delay value will be a int and on 64 bit it will be a long, thank you erlang - // The number will be negative after a message has been delayed - // sticking with an int as you should not be delaying for more than 49 days - switch (headers[HeaderNames.DELAYED_MILLISECONDS]) + int delayedMilliseconds; + + // on 32 bit systems the x-delay value will be a int and on 64 bit it will be a long, thank you erlang + // The number will be negative after a message has been delayed + // sticking with an int as you should not be delaying for more than 49 days + switch (headers[HeaderNames.DELAYED_MILLISECONDS]) + { + case byte[] value: { - case byte[] value: + if (!int.TryParse(Encoding.UTF8.GetString(value), out var handledCount)) + delayedMilliseconds = 0; + else { - if (!int.TryParse(Encoding.UTF8.GetString(value), out var handledCount)) - delayedMilliseconds = 0; - else - { - if (handledCount < 0) - handledCount = Math.Abs(handledCount); - delayedMilliseconds = handledCount; - } - - break; + if (handledCount < 0) + handledCount = Math.Abs(handledCount); + delayedMilliseconds = handledCount; } - case int value: - { - if (value < 0) - value = Math.Abs(value); + + break; + } + case int value: + { + if (value < 0) + value = Math.Abs(value); - delayedMilliseconds = value; - break; - } - case long value: - { - if (value < 0) - value = Math.Abs(value); + delayedMilliseconds = value; + break; + } + case long value: + { + if (value < 0) + value = Math.Abs(value); - delayedMilliseconds = (int)value; - break; - } - default: - return new HeaderResult(TimeSpan.Zero, false); + delayedMilliseconds = (int)value; + break; } - - return new HeaderResult(TimeSpan.FromMilliseconds( delayedMilliseconds), true); + default: + return new HeaderResult(TimeSpan.Zero, false); } - private HeaderResult ReadTopic(BasicDeliverEventArgs fromQueue, IDictionary headers) - { - return ReadHeader(headers, HeaderNames.TOPIC).Map(s => - { - var val = string.IsNullOrEmpty(s) ? new RoutingKey(fromQueue.RoutingKey) : new RoutingKey(s); - return new HeaderResult(val, true); - }); - } + return new HeaderResult(TimeSpan.FromMilliseconds( delayedMilliseconds), true); + } - private HeaderResult ReadMessageId(string messageId) + private HeaderResult ReadTopic(BasicDeliverEventArgs fromQueue, IDictionary headers) + { + return ReadHeader(headers, HeaderNames.TOPIC).Map(s => { - var newMessageId = Guid.NewGuid().ToString(); - - if (string.IsNullOrEmpty(messageId)) - { - s_logger.LogDebug("No message id found in message MessageId, new message id is {Id}", newMessageId); - return new HeaderResult(newMessageId, true); - } + var val = string.IsNullOrEmpty(s) ? new RoutingKey(fromQueue.RoutingKey) : new RoutingKey(s!); + return new HeaderResult(val, true); + }); + } - return new HeaderResult(messageId, true); - } + private static HeaderResult ReadMessageId(string? messageId) + { + var newMessageId = Guid.NewGuid().ToString(); - private HeaderResult ReadRedeliveredFlag(bool redelivered) + if (string.IsNullOrEmpty(messageId)) { - return new HeaderResult(redelivered, true); + s_logger.LogDebug("No message id found in message MessageId, new message id is {Id}", newMessageId); + return new HeaderResult(newMessageId, true); } - private HeaderResult ReadReplyTo(IBasicProperties basicProperties) - { - if (basicProperties.IsReplyToPresent()) - { - return new HeaderResult(basicProperties.ReplyTo, true); - } + return new HeaderResult(messageId, true); + } - return new HeaderResult(null, true); - } + private static HeaderResult ReadRedeliveredFlag(bool redelivered) + { + return new HeaderResult(redelivered, true); + } - private static object ParseHeaderValue(object value) + private static HeaderResult ReadReplyTo(IReadOnlyBasicProperties basicProperties) + { + if (basicProperties.IsReplyToPresent()) { - return value is byte[] bytes ? Encoding.UTF8.GetString(bytes) : value; + return new HeaderResult(basicProperties.ReplyTo!, true); } + + return new HeaderResult(null, true); + } + + private static object ParseHeaderValue(object? value) + { + return (value is byte[] bytes ? Encoding.UTF8.GetString(bytes) : value) ?? string.Empty; } } diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageGateway.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageGateway.cs index 86c77a31c8..119dae360e 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageGateway.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageGateway.cs @@ -25,152 +25,189 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; +using Paramore.Brighter.Tasks; using Polly; using RabbitMQ.Client; using RabbitMQ.Client.Events; -namespace Paramore.Brighter.MessagingGateway.RMQ +namespace Paramore.Brighter.MessagingGateway.RMQ; + +/// +/// Class RmqMessageGateway. +/// Base class for messaging gateway used by a to communicate with a RabbitMQ server, +/// to consume messages from the server or +/// to send a message to the RabbitMQ server. +/// A channel is associated with a queue name, which binds to a when +/// sends over a task queue. +/// So to listen for messages on that Topic you need to bind to the matching queue name. +/// The configuration holds a <serviceActivatorConnections> section which in turn contains a <connections> +/// collection that contains a set of connections. +/// Each subscription identifies a mapping between a queue name and a derived type. At runtime we +/// read this list and listen on the associated channels. +/// The Message Pump (Reactor or Proactor) then uses the associated with the configured +/// request type in to translate between the +/// on-the-wire message and the or +/// +public class RmqMessageGateway : IDisposable, IAsyncDisposable { + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + private readonly AsyncPolicy _circuitBreakerPolicy; + private readonly ConnectionFactory _connectionFactory; + private readonly AsyncPolicy _retryPolicy; + protected readonly RmqMessagingGatewayConnection Connection; + protected IChannel? Channel; + /// - /// Class RmqMessageGateway. - /// Base class for messaging gateway used by a to communicate with a RabbitMQ server, - /// to consume messages from the server or - /// to send a message to the RabbitMQ server. - /// A channel is associated with a queue name, which binds to a when - /// sends over a task queue. - /// So to listen for messages on that Topic you need to bind to the matching queue name. - /// The configuration holds a <serviceActivatorConnections> section which in turn contains a <connections> - /// collection that contains a set of connections. - /// Each subscription identifies a mapping between a queue name and a derived type. At runtime we - /// read this list and listen on the associated channels. - /// The then uses the associated with the configured - /// request type in to translate between the - /// on-the-wire message and the or + /// Initializes a new instance of the class. + /// Use if you need to inject a test logger /// - public class RmqMessageGateway : IDisposable + /// The amqp uri and exchange to connect to + protected RmqMessageGateway(RmqMessagingGatewayConnection connection) { - private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - private readonly Policy _circuitBreakerPolicy; - private readonly ConnectionFactory _connectionFactory; - private readonly Policy _retryPolicy; - protected readonly RmqMessagingGatewayConnection Connection; - protected IModel Channel; - - /// - /// Initializes a new instance of the class. - /// Use if you need to inject a test logger - /// - /// The amqp uri and exchange to connect to - /// How many messages to read from a channel at one time. Only used by consumer, defaults to 1 - protected RmqMessageGateway(RmqMessagingGatewayConnection connection) - { - Connection = connection; + Connection = connection; - var connectionPolicyFactory = new ConnectionPolicyFactory(Connection); + var connectionPolicyFactory = new ConnectionPolicyFactory(Connection); - _retryPolicy = connectionPolicyFactory.RetryPolicy; - _circuitBreakerPolicy = connectionPolicyFactory.CircuitBreakerPolicy; + _retryPolicy = connectionPolicyFactory.RetryPolicyAsync; + _circuitBreakerPolicy = connectionPolicyFactory.CircuitBreakerPolicyAsync; + + if (Connection.AmpqUri is null) throw new ConfigurationException("RMQMessagingGateway: No AMPQ URI specified"); - _connectionFactory = new ConnectionFactory - { - Uri = Connection.AmpqUri.Uri, - RequestedHeartbeat = TimeSpan.FromSeconds(connection.Heartbeat), - ContinuationTimeout = TimeSpan.FromSeconds(connection.ContinuationTimeout) - }; + _connectionFactory = new ConnectionFactory + { + Uri = Connection.AmpqUri.Uri, + RequestedHeartbeat = TimeSpan.FromSeconds(connection.Heartbeat), + ContinuationTimeout = TimeSpan.FromSeconds(connection.ContinuationTimeout) + }; + + if (Connection.Exchange is null) throw new InvalidOperationException("RMQMessagingGateway: No Exchange specified"); + + DelaySupported = Connection.Exchange.SupportDelay; + } - DelaySupported = Connection.Exchange.SupportDelay; - } + /// + /// Gets if the current provider configuration is able to support delayed delivery of messages. + /// + public bool DelaySupported { get; } - /// - /// Gets if the current provider configuration is able to support delayed delivery of messages. - /// - public bool DelaySupported { get; } + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public virtual void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public virtual void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + /// + /// Connects the specified queue name. + /// + /// Name of the queue. For producer use default of "Producer Channel". Passed to Polly for debugging + /// Do we create the exchange if it does not exist + /// Cancel the operation + /// true if XXXX, false otherwise. + protected async Task EnsureBrokerAsync( + ChannelName? queueName = null, + OnMissingChannel makeExchange = OnMissingChannel.Create, + CancellationToken cancellationToken = default + ) + { + queueName ??= new ChannelName("Producer Channel"); + + await ConnectWithCircuitBreakerAsync(queueName, makeExchange, cancellationToken); + } + + private async Task ConnectWithCircuitBreakerAsync(ChannelName queueName, OnMissingChannel makeExchange, CancellationToken cancellationToken = default) + { + await _circuitBreakerPolicy.ExecuteAsync(async () => await ConnectWithRetryAsync(queueName, makeExchange, cancellationToken)); + } + + private async Task ConnectWithRetryAsync(ChannelName queueName, OnMissingChannel makeExchange, CancellationToken cancellationToken = default) + { + await _retryPolicy.ExecuteAsync(async _ => await ConnectToBrokerAsync(makeExchange,cancellationToken), + new Dictionary { { "queueName", queueName.Value } }); + } - /// - /// Connects the specified queue name. - /// - /// Name of the queue. For producer use default of "Producer Channel". Passed to Polly for debugging - /// Do we create the exchange if it does not exist - /// true if XXXX, false otherwise. - protected void EnsureBroker(ChannelName queueName = null, OnMissingChannel makeExchange = OnMissingChannel.Create) + protected virtual async Task ConnectToBrokerAsync(OnMissingChannel makeExchange, CancellationToken cancellationToken = default) + { + if (Channel == null || Channel.IsClosed) { - queueName ??= new ChannelName("Producer Channel"); + var connection = await new RmqMessageGatewayConnectionPool(Connection.Name, Connection.Heartbeat) + .GetConnectionAsync(_connectionFactory, cancellationToken); - ConnectWithCircuitBreaker(queueName, makeExchange); - } + if (Connection.AmpqUri is null) throw new ConfigurationException("RMQMessagingGateway: No AMPQ URI specified"); - private void ConnectWithCircuitBreaker(ChannelName queueName, OnMissingChannel makeExchange) - { - _circuitBreakerPolicy.Execute(() => ConnectWithRetry(queueName, makeExchange)); - } + connection.ConnectionBlockedAsync += HandleBlockedAsync; + connection.ConnectionUnblockedAsync += HandleUnBlockedAsync; - private void ConnectWithRetry(ChannelName queueName, OnMissingChannel makeExchange) - { - _retryPolicy.Execute((ctx) => ConnectToBroker(makeExchange), new Dictionary {{"queueName", queueName.Value}}); - } + s_logger.LogDebug("RMQMessagingGateway: Opening channel to Rabbit MQ on {URL}", Connection.AmpqUri.GetSanitizedUri()); - protected virtual void ConnectToBroker(OnMissingChannel makeExchange) - { - if (Channel == null || Channel.IsClosed) - { - var connection = new RmqMessageGatewayConnectionPool(Connection.Name, Connection.Heartbeat).GetConnection(_connectionFactory); + Channel = await connection.CreateChannelAsync( + new CreateChannelOptions( + publisherConfirmationsEnabled: true, + publisherConfirmationTrackingEnabled: true), + cancellationToken); - connection.ConnectionBlocked += HandleBlocked; - connection.ConnectionUnblocked += HandleUnBlocked; + //desired state configuration of the exchange + await Channel.DeclareExchangeForConnection(Connection, makeExchange, cancellationToken: cancellationToken); + } + } - s_logger.LogDebug("RMQMessagingGateway: Opening channel to Rabbit MQ on {URL}", - Connection.AmpqUri.GetSanitizedUri()); + private Task HandleBlockedAsync(object sender, ConnectionBlockedEventArgs args) + { + if (Connection.AmpqUri is null) throw new ConfigurationException("RMQMessagingGateway: No AMPQ URI specified"); + + s_logger.LogWarning("RMQMessagingGateway: Subscription to {URL} blocked. Reason: {ErrorMessage}", + Connection.AmpqUri.GetSanitizedUri(), args.Reason); - Channel = connection.CreateModel(); + return Task.CompletedTask; + } - //desired state configuration of the exchange - Channel.DeclareExchangeForConnection(Connection, makeExchange); - } - } + private Task HandleUnBlockedAsync(object sender, AsyncEventArgs args) + { + if (Connection.AmpqUri is null) throw new ConfigurationException("RMQMessagingGateway: No AMPQ URI specified"); + + s_logger.LogInformation("RMQMessagingGateway: Subscription to {URL} unblocked", Connection.AmpqUri.GetSanitizedUri()); + return Task.CompletedTask; + } - private void HandleBlocked(object sender, ConnectionBlockedEventArgs args) - { - s_logger.LogWarning("RMQMessagingGateway: Subscription to {URL} blocked. Reason: {ErrorMessage}", - Connection.AmpqUri.GetSanitizedUri(), args.Reason); - } + protected async Task ResetConnectionToBrokerAsync(CancellationToken cancellationToken = default) + { + await new RmqMessageGatewayConnectionPool(Connection.Name, Connection.Heartbeat).ResetConnectionAsync(_connectionFactory, cancellationToken); + } - private void HandleUnBlocked(object sender, EventArgs args) - { - s_logger.LogInformation("RMQMessagingGateway: Subscription to {URL} unblocked", Connection.AmpqUri.GetSanitizedUri()); - } + ~RmqMessageGateway() + { + Dispose(false); + } - protected void ResetConnectionToBroker() + public virtual async ValueTask DisposeAsync() + { + if (Channel != null) { - new RmqMessageGatewayConnectionPool(Connection.Name, Connection.Heartbeat).ResetConnection(_connectionFactory); + await Channel.AbortAsync(); + await Channel.DisposeAsync(); + Channel = null; } - ~RmqMessageGateway() - { - Dispose(false); - } + await new RmqMessageGatewayConnectionPool(Connection.Name, Connection.Heartbeat).RemoveConnectionAsync(_connectionFactory); + } - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (disposing) { - if (disposing) - { - - Channel?.Abort(); - Channel?.Dispose(); - Channel = null; + Channel?.AbortAsync().Wait(); + Channel?.Dispose(); + Channel = null; - new RmqMessageGatewayConnectionPool(Connection.Name, Connection.Heartbeat).RemoveConnection(_connectionFactory); - } + new RmqMessageGatewayConnectionPool(Connection.Name, Connection.Heartbeat).RemoveConnectionAsync(_connectionFactory) + .GetAwaiter() + .GetResult(); } } } diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageGatewayConnectionPool.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageGatewayConnectionPool.cs index 417964b743..5f147f6b2b 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageGatewayConnectionPool.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageGatewayConnectionPool.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2015 Ian Cooper @@ -24,154 +25,181 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; +using Paramore.Brighter.Tasks; using RabbitMQ.Client; +using RabbitMQ.Client.Events; using RabbitMQ.Client.Exceptions; -namespace Paramore.Brighter.MessagingGateway.RMQ +namespace Paramore.Brighter.MessagingGateway.RMQ; + +/// +/// Class MessageGatewayConnectionPool. +/// +public class RmqMessageGatewayConnectionPool(string connectionName, ushort connectionHeartbeat) { - /// - /// Class MessageGatewayConnectionPool. - /// - public class RmqMessageGatewayConnectionPool - { - private readonly string _connectionName; - private readonly ushort _connectionHeartbeat; - private static readonly Dictionary s_connectionPool = new Dictionary(); - private static readonly object s_lock = new object(); - private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - private static readonly Random jitter = new Random(); - - public RmqMessageGatewayConnectionPool(string connectionName, ushort connectionHeartbeat) - { - _connectionName = connectionName; - _connectionHeartbeat = connectionHeartbeat; - } - - /// - /// Return matching RabbitMQ subscription if exist (match by amqp scheme) - /// or create new subscription to RabbitMQ (thread-safe) - /// - /// - /// - public IConnection GetConnection(ConnectionFactory connectionFactory) - { - var connectionId = GetConnectionId(connectionFactory); + private static readonly Dictionary s_connectionPool = new(); - var connectionFound = s_connectionPool.TryGetValue(connectionId, out PooledConnection pooledConnection); + private static readonly SemaphoreSlim s_lock = new SemaphoreSlim(1, 1); + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + private static readonly Random jitter = new Random(); - if (connectionFound && pooledConnection.Connection.IsOpen) - return pooledConnection.Connection; + /// + /// Return matching RabbitMQ subscription if exist (match by amqp scheme) + /// or create new subscription to RabbitMQ (thread-safe) + /// + /// + /// + public IConnection GetConnection(ConnectionFactory connectionFactory) => BrighterSynchronizationHelper.Run(() => GetConnectionAsync(connectionFactory)); - lock (s_lock) - { - connectionFound = s_connectionPool.TryGetValue(connectionId, out pooledConnection); + /// + /// Return matching RabbitMQ subscription if exist (match by amqp scheme) + /// or create new subscription to RabbitMQ (thread-safe) + /// + /// A to create new connections + /// A to cancel the operation + /// + public async Task GetConnectionAsync(ConnectionFactory connectionFactory, CancellationToken cancellationToken = default) + { + var connectionId = GetConnectionId(connectionFactory); - if (connectionFound == false || pooledConnection.Connection.IsOpen == false) - { - pooledConnection = CreateConnection(connectionFactory); - } - } + var connectionFound = s_connectionPool.TryGetValue(connectionId, out PooledConnection? pooledConnection); + if (connectionFound && pooledConnection!.Connection.IsOpen) return pooledConnection.Connection; - } - public void ResetConnection(ConnectionFactory connectionFactory) + await s_lock.WaitAsync(cancellationToken); + + try { - lock (s_lock) + connectionFound = s_connectionPool.TryGetValue(connectionId, out pooledConnection); + + if (connectionFound == false || pooledConnection!.Connection.IsOpen == false) { - DelayReconnecting(); - - try - { - CreateConnection(connectionFactory); - } - catch (BrokerUnreachableException exception) - { - s_logger.LogError(exception, - "RmqMessageGatewayConnectionPool: Failed to reset subscription to Rabbit MQ endpoint {URL}", - connectionFactory.Endpoint); - } + pooledConnection = await CreateConnectionAsync(connectionFactory, cancellationToken); } } - - private PooledConnection CreateConnection(ConnectionFactory connectionFactory) + finally { - var connectionId = GetConnectionId(connectionFactory); + s_lock.Release(); + } - TryRemoveConnection(connectionId); + return pooledConnection.Connection; + } - s_logger.LogDebug("RmqMessageGatewayConnectionPool: Creating subscription to Rabbit MQ endpoint {URL}", connectionFactory.Endpoint); + public async Task ResetConnectionAsync(ConnectionFactory connectionFactory, CancellationToken cancellationToken = default) + { + await s_lock.WaitAsync(cancellationToken); + + try + { + await DelayReconnectingAsync(); + + try + { + await CreateConnectionAsync(connectionFactory, cancellationToken); + } + catch (BrokerUnreachableException exception) + { + s_logger.LogError(exception, + "RmqMessageGatewayConnectionPool: Failed to reset subscription to Rabbit MQ endpoint {URL}", + connectionFactory.Endpoint); + } + } + finally + { + s_lock.Release(); + } + } + + /// + /// Remove the connection from the pool + /// + /// The factory that creates broker connections + public async Task RemoveConnectionAsync(ConnectionFactory connectionFactory, CancellationToken cancellationToken = default) + { + var connectionId = GetConnectionId(connectionFactory); - connectionFactory.RequestedHeartbeat = TimeSpan.FromSeconds(_connectionHeartbeat); - connectionFactory.RequestedConnectionTimeout = TimeSpan.FromMilliseconds(5000); - connectionFactory.SocketReadTimeout = TimeSpan.FromMilliseconds(5000); - connectionFactory.SocketWriteTimeout = TimeSpan.FromMilliseconds(5000); + if (s_connectionPool.ContainsKey(connectionId)) + { + await s_lock.WaitAsync(cancellationToken); + try + { + await TryRemoveConnectionAsync(connectionId); + } + finally + { + s_lock.Release(); + } + } + } - var connection = connectionFactory.CreateConnection(_connectionName); + private async Task CreateConnectionAsync(ConnectionFactory connectionFactory, CancellationToken cancellationToken = default) + { + var connectionId = GetConnectionId(connectionFactory); - s_logger.LogDebug("RmqMessageGatewayConnectionPool: new connected to {URL} added to pool named {ProviderName}", connection.Endpoint, connection.ClientProvidedName); + await TryRemoveConnectionAsync(connectionId); + s_logger.LogDebug("RmqMessageGatewayConnectionPool: Creating subscription to Rabbit MQ endpoint {URL}", + connectionFactory.Endpoint); - void ShutdownHandler(object sender, ShutdownEventArgs e) - { - s_logger.LogWarning("RmqMessageGatewayConnectionPool: The subscription {URL} has been shutdown due to {ErrorMessage}", connection.Endpoint, e.ToString()); + connectionFactory.RequestedHeartbeat = TimeSpan.FromSeconds(connectionHeartbeat); + connectionFactory.RequestedConnectionTimeout = TimeSpan.FromMilliseconds(5000); + connectionFactory.SocketReadTimeout = TimeSpan.FromMilliseconds(5000); + connectionFactory.SocketWriteTimeout = TimeSpan.FromMilliseconds(5000); - lock (s_lock) - { - TryRemoveConnection(connectionId); - } - } + var connection = await connectionFactory.CreateConnectionAsync(connectionName, cancellationToken); - connection.ConnectionShutdown += ShutdownHandler; + s_logger.LogDebug("RmqMessageGatewayConnectionPool: new connected to {URL} added to pool named {ProviderName}", + connection.Endpoint, connection.ClientProvidedName); - var pooledConnection = new PooledConnection{Connection = connection, ShutdownHandler = ShutdownHandler}; - s_connectionPool.Add(connectionId, pooledConnection); + async Task ShutdownHandler(object sender, ShutdownEventArgs e) + { + s_logger.LogWarning( + "RmqMessageGatewayConnectionPool: The subscription {URL} has been shutdown due to {ErrorMessage}", + connection.Endpoint, e.ToString()); - return pooledConnection; - } + await s_lock.WaitAsync(e.CancellationToken); - private void TryRemoveConnection(string connectionId) - { - if (s_connectionPool.TryGetValue(connectionId, out PooledConnection pooledConnection)) + try + { + await TryRemoveConnectionAsync(connectionId); + } + finally { - pooledConnection.Connection.ConnectionShutdown -= pooledConnection.ShutdownHandler; - pooledConnection.Connection.Dispose(); - s_connectionPool.Remove(connectionId); + s_lock.Release(); } } - private string GetConnectionId(ConnectionFactory connectionFactory) - { - return $"{connectionFactory.UserName}.{connectionFactory.Password}.{connectionFactory.HostName}.{connectionFactory.Port}.{connectionFactory.VirtualHost}".ToLowerInvariant(); - } + connection.ConnectionShutdownAsync += ShutdownHandler; - private static void DelayReconnecting() - { - Task.Delay(jitter.Next(5, 100)).Wait(); - } + var pooledConnection = new PooledConnection(connection, ShutdownHandler); + s_connectionPool.Add(connectionId, pooledConnection); - class PooledConnection + return pooledConnection; + } + + private static async Task DelayReconnectingAsync() => await Task.Delay(jitter.Next(5, 100)); + + private async Task TryRemoveConnectionAsync(string connectionId) + { + if (s_connectionPool.TryGetValue(connectionId, out PooledConnection? pooledConnection)) { - public IConnection Connection { get; set; } - public EventHandler ShutdownHandler { get; set; } + pooledConnection.Connection.ConnectionShutdownAsync -= pooledConnection.ShutdownHandler; + await pooledConnection.Connection.DisposeAsync(); + s_connectionPool.Remove(connectionId); } + } - public void RemoveConnection(ConnectionFactory connectionFactory) - { - var connectionId = GetConnectionId(connectionFactory); + private static string GetConnectionId(ConnectionFactory connectionFactory) + => + $"{connectionFactory.UserName}.{connectionFactory.Password}.{connectionFactory.HostName}.{connectionFactory.Port}.{connectionFactory.VirtualHost}" + .ToLowerInvariant(); - if (s_connectionPool.ContainsKey(connectionId)) - { - lock (s_lock) - { - TryRemoveConnection(connectionId); - } - } - } - } + private record PooledConnection(IConnection Connection, AsyncEventHandler ShutdownHandler); } diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageProducer.cs index 3b8bd83f45..27f5b33b10 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageProducer.cs @@ -23,199 +23,180 @@ THE SOFTWARE. */ #endregion +#nullable enable using System; using System.Collections.Concurrent; using System.Diagnostics; using System.IO; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; +using Paramore.Brighter.Tasks; using RabbitMQ.Client.Events; -namespace Paramore.Brighter.MessagingGateway.RMQ +namespace Paramore.Brighter.MessagingGateway.RMQ; + +/// +/// Class ClientRequestHandler . +/// The is used by a client to talk to a server and abstracts the infrastructure for inter-process communication away from clients. +/// It handles subscription establishment, request sending and error handling +/// +public class RmqMessageProducer : RmqMessageGateway, IAmAMessageProducerSync, IAmAMessageProducerAsync, ISupportPublishConfirmation { + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + private static readonly SemaphoreSlim s_lock = new(1, 1); + + private readonly RmqPublication _publication; + private readonly ConcurrentDictionary _pendingConfirmations = new(); + private readonly int _waitForConfirmsTimeOutInMilliseconds; + /// - /// Class ClientRequestHandler . - /// The is used by a client to talk to a server and abstracts the infrastructure for inter-process communication away from clients. - /// It handles subscription establishment, request sending and error handling + /// Action taken when a message is published, following receipt of a confirmation from the broker + /// see https://www.rabbitmq.com/blog/2011/02/10/introducing-publisher-confirms#how-confirms-work for more /// - public class RmqMessageProducer : RmqMessageGateway, IAmAMessageProducerSync, IAmAMessageProducerAsync, ISupportPublishConfirmation - { - private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - - static readonly object _lock = new object(); - private readonly RmqPublication _publication; - private readonly ConcurrentDictionary _pendingConfirmations = new ConcurrentDictionary(); - private bool _confirmsSelected; - private readonly int _waitForConfirmsTimeOutInMilliseconds; - - /// - /// Action taken when a message is published, following receipt of a confirmation from the broker - /// see https://www.rabbitmq.com/blog/2011/02/10/introducing-publisher-confirms#how-confirms-work for more - /// - public event Action OnMessagePublished; - - /// - /// The publication configuration for this producer - /// - public Publication Publication { get { return _publication; } } - - /// - /// The OTel Span we are writing Producer events too - /// - public Activity Span { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// The subscription information needed to talk to RMQ - /// Make Channels = Create - public RmqMessageProducer(RmqMessagingGatewayConnection connection) - : this(connection, new RmqPublication { MakeChannels = OnMissingChannel.Create }) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The subscription information needed to talk to RMQ - /// How should we configure this producer. If not provided use default behaviours: - /// Make Channels = Create - /// - public RmqMessageProducer(RmqMessagingGatewayConnection connection, RmqPublication publication) - : base(connection) - { - _publication = publication ?? new RmqPublication { MakeChannels = OnMissingChannel.Create }; - _waitForConfirmsTimeOutInMilliseconds = _publication.WaitForConfirmsTimeOutInMilliseconds; - } + public event Action? OnMessagePublished; - /// - /// Sends the specified message. - /// - /// The message. - public void Send(Message message) - { - SendWithDelay(message); - } + /// + /// The publication configuration for this producer + /// + public Publication Publication { get { return _publication; } } + + /// + /// The OTel Span we are writing Producer events too + /// + public Activity? Span { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The subscription information needed to talk to RMQ + /// Make Channels = Create + public RmqMessageProducer(RmqMessagingGatewayConnection connection) + : this(connection, new RmqPublication { MakeChannels = OnMissingChannel.Create }) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The subscription information needed to talk to RMQ + /// How should we configure this producer. If not provided use default behaviours: + /// Make Channels = Create + /// + public RmqMessageProducer(RmqMessagingGatewayConnection connection, RmqPublication? publication) + : base(connection) + { + _publication = publication ?? new RmqPublication { MakeChannels = OnMissingChannel.Create }; + _waitForConfirmsTimeOutInMilliseconds = _publication.WaitForConfirmsTimeOutInMilliseconds; + } + + /// + /// Sends the specified message. + /// + /// The message. + public void Send(Message message) => SendWithDelay(message); + + /// + /// Send the specified message with specified delay + /// + /// The message. + /// Delay to delivery of the message. + /// Task. + public void SendWithDelay(Message message, TimeSpan? delay = null) => BrighterSynchronizationHelper.Run(() => SendWithDelayAsync(message, delay)); - /// - /// Send the specified message with specified delay - /// - /// The message. - /// Delay to delivery of the message. - /// Task. - public void SendWithDelay(Message message, TimeSpan? delay = null) + /// + /// Sends the specified message + /// NOTE: RMQ's client has no async support, so this is not actually async and will block whilst it sends + /// + /// + /// Pass a cancellation token to kill the send operation + /// + public async Task SendAsync(Message message, CancellationToken cancellationToken = default) => await SendWithDelayAsync(message, null, cancellationToken); + + public async Task SendWithDelayAsync(Message message, TimeSpan? delay, CancellationToken cancellationToken = default) + { + if (Connection.Exchange is null) throw new ConfigurationException("RmqMessageProducer: Exchange is not set"); + if (Connection.AmpqUri is null) throw new ConfigurationException("RmqMessageProducer: Broker URL is not set"); + + delay ??= TimeSpan.Zero; + + try { - delay ??= TimeSpan.Zero; + s_logger.LogDebug("RmqMessageProducer: Preparing to send message via exchange {ExchangeName}", + Connection.Exchange.Name); + + await EnsureBrokerAsync(makeExchange: _publication.MakeChannels, cancellationToken: cancellationToken); - try + if (Channel is null) throw new ChannelFailureException($"RmqMessageProducer: Channel is not set for {_publication.Topic}"); + + var rmqMessagePublisher = new RmqMessagePublisher(Channel, Connection); + + message.Persist = Connection.PersistMessages; + Channel.BasicAcksAsync += OnPublishSucceeded; + Channel.BasicNacksAsync += OnPublishFailed; + + s_logger.LogDebug( + "RmqMessageProducer: Publishing message to exchange {ExchangeName} on subscription {URL} with a delay of {Delay} and topic {Topic} and persisted {Persist} and id {Id} and body: {Request}", + Connection.Exchange.Name, Connection.AmpqUri.GetSanitizedUri(), delay.Value.TotalMilliseconds, + message.Header.Topic, message.Persist, message.Id, message.Body.Value); + + _pendingConfirmations.TryAdd(await Channel.GetNextPublishSequenceNumberAsync(cancellationToken), message.Id); + + if (DelaySupported) { - lock (_lock) - { - s_logger.LogDebug("RmqMessageProducer: Preparing to send message via exchange {ExchangeName}", Connection.Exchange.Name); - EnsureBroker(makeExchange: _publication.MakeChannels); - - var rmqMessagePublisher = new RmqMessagePublisher(Channel, Connection); - - message.Persist = Connection.PersistMessages; - Channel.BasicAcks += OnPublishSucceeded; - Channel.BasicNacks += OnPublishFailed; - Channel.ConfirmSelect(); - _confirmsSelected = true; - - - s_logger.LogDebug( - "RmqMessageProducer: Publishing message to exchange {ExchangeName} on subscription {URL} with a delay of {Delay} and topic {Topic} and persisted {Persist} and id {Id} and body: {Request}", - Connection.Exchange.Name, Connection.AmpqUri.GetSanitizedUri(), delay.Value.TotalMilliseconds, - message.Header.Topic, message.Persist, message.Id, message.Body.Value); - - _pendingConfirmations.TryAdd(Channel.NextPublishSeqNo, message.Id); - - if (DelaySupported) - { - rmqMessagePublisher.PublishMessage(message, delay.Value); - } - else - { - //TODO: Replace with a Timer, don't block - Task.Delay(delay.Value).Wait(); - rmqMessagePublisher.PublishMessage(message, TimeSpan.Zero); - } - - s_logger.LogInformation( - "RmqMessageProducer: Published message to exchange {ExchangeName} on broker {URL} with a delay of {Delay} and topic {Topic} and persisted {Persist} and id {Id} and message: {Request} at {Time}", - Connection.Exchange.Name, Connection.AmpqUri.GetSanitizedUri(), delay, - message.Header.Topic, message.Persist, message.Id, - JsonSerializer.Serialize(message, JsonSerialisationOptions.Options), DateTime.UtcNow); - } + await rmqMessagePublisher.PublishMessageAsync(message, delay.Value, cancellationToken); } - catch (IOException io) + else { - s_logger.LogError(io, - "RmqMessageProducer: Error talking to the socket on {URL}, resetting subscription", - Connection.AmpqUri.GetSanitizedUri() - ); - ResetConnectionToBroker(); - throw new ChannelFailureException("Error talking to the broker, see inner exception for details", io); + //TODO: Replace with a Timer, don't block + await rmqMessagePublisher.PublishMessageAsync(message, TimeSpan.Zero, cancellationToken); } - } - /// - /// Sends the specified message - /// NOTE: RMQ's client has no async support, so this is not actually async and will block whilst it sends - /// - /// - /// - public Task SendAsync(Message message) + s_logger.LogInformation( + "RmqMessageProducer: Published message to exchange {ExchangeName} on broker {URL} with a delay of {Delay} and topic {Topic} and persisted {Persist} and id {Id} and message: {Request} at {Time}", + Connection.Exchange.Name, Connection.AmpqUri.GetSanitizedUri(), delay, + message.Header.Topic, message.Persist, message.Id, + JsonSerializer.Serialize(message, JsonSerialisationOptions.Options), DateTime.UtcNow); + } + catch (IOException io) { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - Send(message); - tcs.SetResult(new object()); - return tcs.Task; + s_logger.LogError(io, + "RmqMessageProducer: Error talking to the socket on {URL}, resetting subscription", + Connection.AmpqUri.GetSanitizedUri() + ); + await ResetConnectionToBrokerAsync(cancellationToken); + throw new ChannelFailureException("Error talking to the broker, see inner exception for details", io); } + } + public sealed override void Dispose() + { + GC.SuppressFinalize(this); + } - public sealed override void Dispose() + private Task OnPublishFailed(object sender, BasicNackEventArgs e) + { + if (_pendingConfirmations.TryGetValue(e.DeliveryTag, out var messageId)) { - Dispose(true); - GC.SuppressFinalize(this); + OnMessagePublished?.Invoke(false, messageId); + _pendingConfirmations.TryRemove(e.DeliveryTag, out _); + s_logger.LogDebug("Failed to publish message: {MessageId}", messageId); } - protected override void Dispose(bool disposing) - { - if (disposing) - { - if (Channel != null && Channel.IsOpen && _confirmsSelected) - { - //In the event this fails, then consequence is not marked as sent in outbox - //As we are disposing, just let that happen - Channel.WaitForConfirms(TimeSpan.FromMilliseconds(_waitForConfirmsTimeOutInMilliseconds), out bool timedOut); - if (timedOut) - s_logger.LogWarning("Failed to await publisher confirms when shutting down!"); - } - } - - base.Dispose(disposing); - } + return Task.CompletedTask; + } - private void OnPublishFailed(object sender, BasicNackEventArgs e) + private Task OnPublishSucceeded(object sender, BasicAckEventArgs e) + { + if (_pendingConfirmations.TryGetValue(e.DeliveryTag, out var messageId)) { - if (_pendingConfirmations.TryGetValue(e.DeliveryTag, out string messageId)) - { - OnMessagePublished?.Invoke(false, messageId); - _pendingConfirmations.TryRemove(e.DeliveryTag, out string _); - s_logger.LogDebug("Failed to publish message: {MessageId}", messageId); - } + OnMessagePublished?.Invoke(true, messageId); + _pendingConfirmations.TryRemove(e.DeliveryTag, out _); + s_logger.LogInformation("Published message: {MessageId}", messageId); } - private void OnPublishSucceeded(object sender, BasicAckEventArgs e) - { - if (_pendingConfirmations.TryGetValue(e.DeliveryTag, out string messageId)) - { - OnMessagePublished?.Invoke(true, messageId); - _pendingConfirmations.TryRemove(e.DeliveryTag, out string _); - s_logger.LogInformation("Published message: {MessageId}", messageId); - } - } + return Task.CompletedTask; } } diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageProducerFactory.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageProducerFactory.cs index 76822ba8ba..dbbe564d05 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageProducerFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessageProducerFactory.cs @@ -22,6 +22,7 @@ THE SOFTWARE. */ #endregion using System.Collections.Generic; +using System.Threading.Tasks; namespace Paramore.Brighter.MessagingGateway.RMQ { @@ -41,10 +42,19 @@ 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); } return producers; } + + /// + public Task> CreateAsync() + { + return Task.FromResult(Create()); + } } } diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessagePublisher.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessagePublisher.cs index 9a5e95b7d6..d4f2d27742 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessagePublisher.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessagePublisher.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2014 Ian Cooper @@ -26,214 +27,226 @@ THE SOFTWARE. */ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Paramore.Brighter.Extensions; using Paramore.Brighter.Logging; using RabbitMQ.Client; -namespace Paramore.Brighter.MessagingGateway.RMQ +namespace Paramore.Brighter.MessagingGateway.RMQ; + +/// +/// Class RmqMessagePublisher. +/// +internal class RmqMessagePublisher { + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + + private static readonly string[] _headersToReset = + [ + HeaderNames.DELAY_MILLISECONDS, + HeaderNames.MESSAGE_TYPE, + HeaderNames.TOPIC, + HeaderNames.HANDLED_COUNT, + HeaderNames.DELIVERY_TAG, + HeaderNames.CORRELATION_ID + ]; + + private readonly IChannel _channel; + private readonly RmqMessagingGatewayConnection _connection; + /// - /// Class RmqMessagePublisher. + /// Initializes a new instance of the class. /// -internal class RmqMessagePublisher + /// The channel. + /// The exchange we want to talk to. + /// + /// channel + /// or + /// exchangeName + /// + public RmqMessagePublisher(IChannel channel, RmqMessagingGatewayConnection connection) { - private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - private static readonly string[] _headersToReset = - [ - HeaderNames.DELAY_MILLISECONDS, - HeaderNames.MESSAGE_TYPE, - HeaderNames.TOPIC, - HeaderNames.HANDLED_COUNT, - HeaderNames.DELIVERY_TAG, - HeaderNames.CORRELATION_ID - ]; - - private readonly IModel _channel; - private readonly RmqMessagingGatewayConnection _connection; - - /// - /// Initializes a new instance of the class. - /// - /// The channel. - /// The exchange we want to talk to. - /// - /// channel - /// or - /// exchangeName - /// - public RmqMessagePublisher(IModel channel, RmqMessagingGatewayConnection connection) + if (channel is null) { - if (channel is null) - { - throw new ArgumentNullException(nameof(channel)); - } - if (connection is null) - { - throw new ArgumentNullException(nameof(connection)); - } + throw new ArgumentNullException(nameof(channel)); + } - _connection = connection; - - _channel = channel; - } - - /// - /// Publishes the message. - /// - /// The message. - /// The delay in ms. 0 is no delay. Defaults to 0 - public void PublishMessage(Message message, TimeSpan? delay = null) + if (connection is null) { - var messageId = message.Id; - var deliveryTag = message.Header.Bag.ContainsKey(HeaderNames.DELIVERY_TAG) ? message.DeliveryTag.ToString() : null; + throw new ArgumentNullException(nameof(connection)); + } - var headers = new Dictionary - { - { HeaderNames.MESSAGE_TYPE, message.Header.MessageType.ToString() }, - { HeaderNames.TOPIC, message.Header.Topic.Value }, - { HeaderNames.HANDLED_COUNT, message.Header.HandledCount } - }; + _connection = connection; - if (message.Header.CorrelationId != string.Empty) - headers.Add(HeaderNames.CORRELATION_ID, message.Header.CorrelationId); + _channel = channel; + } - message.Header.Bag.Each(header => - { - if (!_headersToReset.Any(htr => htr.Equals(header.Key))) headers.Add(header.Key, header.Value); - }); - - if (!string.IsNullOrEmpty(deliveryTag)) - headers.Add(HeaderNames.DELIVERY_TAG, deliveryTag); - - if (delay > TimeSpan.Zero) - headers.Add(HeaderNames.DELAY_MILLISECONDS, delay.Value.TotalMilliseconds); - - _channel.BasicPublish( - _connection.Exchange.Name, - message.Header.Topic, - false, - CreateBasicProperties( - messageId, - message.Header.TimeStamp, - message.Body.ContentType, - message.Header.ContentType, - message.Header.ReplyTo, - message.Persist, - headers), - message.Body.Bytes); - } + /// + /// Publishes the message. + /// + /// The message. + /// The delay in ms. 0 is no delay. Defaults to 0 + /// A that cancels the Publish operation + public async Task PublishMessageAsync(Message message, TimeSpan? delay = null, CancellationToken cancellationToken = default) + { + if (_connection.Exchange is null) throw new InvalidOperationException("RMQMessagingGateway: No Exchange specified"); + + var messageId = message.Id; + var deliveryTag = message.Header.Bag.ContainsKey(HeaderNames.DELIVERY_TAG) + ? message.DeliveryTag.ToString() + : null; - /// - /// Requeues the message. - /// - /// The message. - /// The queue name. - /// Delay. Set to TimeSpan.Zero for not delay - public void RequeueMessage(Message message, ChannelName queueName, TimeSpan timeOut) + var headers = new Dictionary { - var messageId = Guid.NewGuid().ToString() ; - const string deliveryTag = "1"; + { HeaderNames.MESSAGE_TYPE, message.Header.MessageType.ToString() }, + { HeaderNames.TOPIC, message.Header.Topic.Value }, + { HeaderNames.HANDLED_COUNT, message.Header.HandledCount } + }; - s_logger.LogInformation("RmqMessagePublisher: Regenerating message {Id} with DeliveryTag of {1} to {2} with DeliveryTag of {DeliveryTag}", message.Id, deliveryTag, messageId, 1); + if (message.Header.CorrelationId != string.Empty) + headers.Add(HeaderNames.CORRELATION_ID, message.Header.CorrelationId); - var headers = new Dictionary - { - {HeaderNames.MESSAGE_TYPE, message.Header.MessageType.ToString()}, - {HeaderNames.TOPIC, message.Header.Topic.Value}, - {HeaderNames.HANDLED_COUNT, message.Header.HandledCount}, - }; + message.Header.Bag.Each(header => + { + if (!_headersToReset.Any(htr => htr.Equals(header.Key))) headers.Add(header.Key, header.Value); + }); + + if (!string.IsNullOrEmpty(deliveryTag)) + headers.Add(HeaderNames.DELIVERY_TAG, deliveryTag!); + + if (delay > TimeSpan.Zero) + headers.Add(HeaderNames.DELAY_MILLISECONDS, delay.Value.TotalMilliseconds); + + await _channel.BasicPublishAsync( + _connection.Exchange.Name, + message.Header.Topic, + false, + CreateBasicProperties( + messageId, + message.Header.TimeStamp, + message.Body.ContentType, + message.Header.ContentType ?? "plain/text", + message.Header.ReplyTo ?? string.Empty, + message.Persist, + headers), + message.Body.Bytes, cancellationToken); + } - if (message.Header.CorrelationId != string.Empty) - headers.Add(HeaderNames.CORRELATION_ID, message.Header.CorrelationId); + /// + /// Requeues the message. + /// + /// The message. + /// The queue name. + /// Delay. Set to TimeSpan.Zero for not delay + /// A that cancels the requeue + public async Task RequeueMessageAsync(Message message, ChannelName queueName, TimeSpan timeOut, CancellationToken cancellationToken = default) + { + var messageId = Guid.NewGuid().ToString(); + const string deliveryTag = "1"; - message.Header.Bag.Each((header) => - { - if (!_headersToReset.Any(htr => htr.Equals(header.Key))) headers.Add(header.Key, header.Value); - }); - - headers.Add(HeaderNames.DELIVERY_TAG, deliveryTag); - - if (timeOut > TimeSpan.Zero) - headers.Add(HeaderNames.DELAY_MILLISECONDS, timeOut.TotalMilliseconds); - - if (!message.Header.Bag.Any(h => h.Key.Equals(HeaderNames.ORIGINAL_MESSAGE_ID, StringComparison.CurrentCultureIgnoreCase))) - headers.Add(HeaderNames.ORIGINAL_MESSAGE_ID, message.Id); - - // To send it to the right queue use the default (empty) exchange - _channel.BasicPublish( - string.Empty, - queueName.Value, - false, - CreateBasicProperties( - messageId, - message.Header.TimeStamp, - message.Body.ContentType, - message.Header.ContentType, - message.Header.ReplyTo, - message.Persist, - headers), - message.Body.Bytes); - } + s_logger.LogInformation( + "RmqMessagePublisher: Regenerating message {Id} with DeliveryTag of {1} to {2} with DeliveryTag of {DeliveryTag}", + message.Id, deliveryTag, messageId, 1); - private IBasicProperties CreateBasicProperties(string id, DateTimeOffset timeStamp, string type, string contentType, - string replyTo, bool persistMessage, IDictionary headers = null) + var headers = new Dictionary { - var basicProperties = _channel.CreateBasicProperties(); + { HeaderNames.MESSAGE_TYPE, message.Header.MessageType.ToString() }, + { HeaderNames.TOPIC, message.Header.Topic.Value }, + { HeaderNames.HANDLED_COUNT, message.Header.HandledCount }, + }; - basicProperties.DeliveryMode = (byte) (persistMessage ? 2 : 1); // delivery mode set to 2 if message is persistent or 1 if non-persistent - basicProperties.ContentType = contentType; - basicProperties.Type = type; - basicProperties.MessageId = id; - basicProperties.Timestamp = new AmqpTimestamp(UnixTimestamp.GetUnixTimestampSeconds(timeStamp.DateTime)); - if (!string.IsNullOrEmpty(replyTo)) - basicProperties.ReplyTo = replyTo; + if (message.Header.CorrelationId != string.Empty) + headers.Add(HeaderNames.CORRELATION_ID, message.Header.CorrelationId); - if (!(headers is null)) - { - foreach (var header in headers) - { - if(!IsAnAmqpType(header.Value)) - throw new ConfigurationException($"The value {header.Value} is type {header.Value.GetType()} for header {header.Key} value only supports the AMQP 0-8/0-9 standard entry types S, I, D, T and F, as well as the QPid-0-8 specific b, d, f, l, s, t, x and V types and the AMQP 0-9-1 A type."); - } + message.Header.Bag.Each((header) => + { + if (!_headersToReset.Any(htr => htr.Equals(header.Key))) headers.Add(header.Key, header.Value); + }); - basicProperties.Headers = headers; - } + headers.Add(HeaderNames.DELIVERY_TAG, deliveryTag); - return basicProperties; + if (timeOut > TimeSpan.Zero) + { + headers.Add(HeaderNames.DELAY_MILLISECONDS, timeOut.TotalMilliseconds); } - - /// - /// Supports the AMQP 0-8/0-9 standard entry types S, I, D, T - /// and F, as well as the QPid-0-8 specific b, d, f, l, s, t - /// x and V types and the AMQP 0-9-1 A type. - /// - /// - /// - private bool IsAnAmqpType(object value) + + if (!message.Header.Bag.Any(h => + h.Key.Equals(HeaderNames.ORIGINAL_MESSAGE_ID, StringComparison.CurrentCultureIgnoreCase))) + { + headers.Add(HeaderNames.ORIGINAL_MESSAGE_ID, message.Id); + } + + // To send it to the right queue use the default (empty) exchange + await _channel.BasicPublishAsync( + string.Empty, + queueName.Value, + false, + CreateBasicProperties( + messageId, + message.Header.TimeStamp, + message.Body.ContentType, + message.Header.ContentType ?? "plain/text", + message.Header.ReplyTo ?? string.Empty, + message.Persist, + headers), + message.Body.Bytes, cancellationToken); + } + + private static BasicProperties CreateBasicProperties( + string id, + DateTimeOffset timeStamp, + string type, + string contentType, + string replyTo, + bool persistMessage, + IDictionary? headers = null) + { + var basicProperties = new BasicProperties { - switch (value) + DeliveryMode = persistMessage ? DeliveryModes.Persistent : DeliveryModes.Transient, // delivery mode set to 2 if message is persistent or 1 if non-persistent + ContentType = contentType, + Type = type, + MessageId = id, + Timestamp = new AmqpTimestamp(UnixTimestamp.GetUnixTimestampSeconds(timeStamp.DateTime)) + }; + + if (!string.IsNullOrEmpty(replyTo)) + { + basicProperties.ReplyTo = replyTo; + } + + if (headers is not null) + { + foreach (var header in headers) { - case null: - case string _: - case byte[] _: - case int _: - case uint _: - case decimal _: - case AmqpTimestamp _: - case IDictionary _: - case IList _: - case byte _: - case sbyte _: - case double _: - case float _: - case long _: - case short _: - case bool _: - return true; - default: - return false; + if (header.Value is not null && !IsAnAmqpType(header.Value)) + { + throw new ConfigurationException( + $"The value {header.Value} is type {header.Value.GetType()} for header {header.Key} value only supports the AMQP 0-8/0-9 standard entry types S, I, D, T and F, as well as the QPid-0-8 specific b, d, f, l, s, t, x and V types and the AMQP 0-9-1 A type.");} } + + basicProperties.Headers = headers; } + + return basicProperties; + } + + /// + /// Supports the AMQP 0-8/0-9 standard entry types S, I, D, T + /// and F, as well as the QPid-0-8 specific b, d, f, l, s, t + /// x and V types and the AMQP 0-9-1 A type. + /// + /// + /// + private static bool IsAnAmqpType(object value) + { + return value switch + { + null or string _ or byte[] _ or int _ or uint _ or decimal _ or AmqpTimestamp _ or IDictionary _ or IList _ + or byte _ or sbyte _ or double _ or float _ or long _ or short _ or bool _ => true, + _ => false + }; } } diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessagingGatewayConnection.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessagingGatewayConnection.cs index 732e46b125..284373e582 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessagingGatewayConnection.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessagingGatewayConnection.cs @@ -30,32 +30,27 @@ namespace Paramore.Brighter.MessagingGateway.RMQ { public class RmqMessagingGatewayConnection : IAmGatewayConfiguration { - public RmqMessagingGatewayConnection() - { - Name = Environment.MachineName; - } - - /// + /// /// Sets Unique name for the subscription /// - public string Name { get; set; } + public string Name { get; set; } = Environment.MachineName; /// /// Gets or sets the ampq URI. /// /// The ampq URI. - public AmqpUriSpecification AmpqUri { get; set; } + public AmqpUriSpecification? AmpqUri { get; set; } /// /// Gets or sets the exchange. /// /// The exchange. - public Exchange Exchange { get; set; } + public Exchange? Exchange { get; set; } /// /// The exchange used for any dead letter queue /// - public Exchange DeadLetterExchange { get; set; } + public Exchange? DeadLetterExchange { get; set; } /// /// Gets or sets the Heartbeat in seconds. Defaults to 20. @@ -77,38 +72,35 @@ public RmqMessagingGatewayConnection() /// /// Class AMQPUriSpecification /// - public class AmqpUriSpecification + public class AmqpUriSpecification( + Uri uri, + int connectionRetryCount = 3, + int retryWaitInMilliseconds = 1000, + int circuitBreakTimeInMilliseconds = 60000) { - private string _sanitizedUri; + private string? _sanitizedUri; - public AmqpUriSpecification(Uri uri, int connectionRetryCount = 3, int retryWaitInMilliseconds = 1000, int circuitBreakTimeInMilliseconds = 60000) - { - Uri = uri; - ConnectionRetryCount = connectionRetryCount; - RetryWaitInMilliseconds = retryWaitInMilliseconds; - CircuitBreakTimeInMilliseconds = circuitBreakTimeInMilliseconds; - } /// /// Gets or sets the URI. /// /// The URI. - public Uri Uri { get; set; } + public Uri Uri { get; set; } = uri; /// /// Gets or sets the retry count for when a subscription fails /// - public int ConnectionRetryCount { get; set; } + public int ConnectionRetryCount { get; set; } = connectionRetryCount; /// /// The time in milliseconds to wait before retrying to connect again /// - public int RetryWaitInMilliseconds { get; set; } + public int RetryWaitInMilliseconds { get; set; } = retryWaitInMilliseconds; /// /// The time in milliseconds to wait before retrying to connect again. /// - public int CircuitBreakTimeInMilliseconds { get; set; } + public int CircuitBreakTimeInMilliseconds { get; set; } = circuitBreakTimeInMilliseconds; public string GetSanitizedUri() { diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqProducerRegistryFactory.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqProducerRegistryFactory.cs index c948b62131..debaf1bf66 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqProducerRegistryFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqProducerRegistryFactory.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace Paramore.Brighter.MessagingGateway.RMQ { @@ -21,5 +23,14 @@ public IAmAProducerRegistry Create() return new ProducerRegistry(producerFactory.Create()); } + + /// + /// Creates message producers. + /// + /// A has of middleware clients by topic, for sending messages to the middleware + public Task CreateAsync(CancellationToken ct = default) + { + return Task.FromResult(Create()); + } } } diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqSubscription.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqSubscription.cs index 108db77d37..c596cad591 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqSubscription.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqSubscription.cs @@ -32,12 +32,12 @@ public class RmqSubscription : Subscription /// /// The name of the queue to send rejects messages to /// - public ChannelName DeadLetterChannelName { get; } + public ChannelName? DeadLetterChannelName { get; } /// /// The routing key for dead letter messages /// - public RoutingKey DeadLetterRoutingKey { get; } + public RoutingKey? DeadLetterRoutingKey { get; } /// /// Is the channel mirrored across node in the cluster @@ -78,7 +78,7 @@ public class RmqSubscription : Subscription /// The delay to the delivery of a requeue message; defaults to 0 /// The number of unacceptable messages to handle, before stopping reading from the channel. /// The durability of the queue definition in the broker. - /// Is this channel read asynchronously + /// Is this channel read asynchronously /// The channel factory to create channels for Consumer. /// Should we mirror the queue over multiple nodes /// The dead letter channel @@ -90,9 +90,9 @@ public class RmqSubscription : Subscription /// The maximum number of messages in a queue before we reject messages; defaults to no limit public RmqSubscription( Type dataType, - SubscriptionName name = null, - ChannelName channelName = null, - RoutingKey routingKey = null, + SubscriptionName? name = null, + ChannelName? channelName = null, + RoutingKey? routingKey = null, int bufferSize = 1, int noOfPerformers = 1, TimeSpan? timeOut = null, @@ -100,17 +100,17 @@ public RmqSubscription( TimeSpan? requeueDelay = null, int unacceptableMessageLimit = 0, bool isDurable = false, - bool runAsync = false, - IAmAChannelFactory channelFactory = null, + MessagePumpType messagePumpType = MessagePumpType.Proactor, + IAmAChannelFactory? channelFactory = null, bool highAvailability = false, - ChannelName deadLetterChannelName = null, - RoutingKey deadLetterRoutingKey = null, + ChannelName? deadLetterChannelName = null, + RoutingKey? deadLetterRoutingKey = null, TimeSpan? ttl = null, OnMissingChannel makeChannels = OnMissingChannel.Create, - int emptyChannelDelay = 500, - int channelFailureDelay = 1000, + TimeSpan? emptyChannelDelay = null, + TimeSpan? channelFailureDelay = null, int? maxQueueLength = null) - : base(dataType, name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount, requeueDelay, unacceptableMessageLimit, runAsync, channelFactory, makeChannels, emptyChannelDelay, channelFailureDelay) + : base(dataType, name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount, requeueDelay, unacceptableMessageLimit, messagePumpType, channelFactory, makeChannels, emptyChannelDelay, channelFailureDelay) { DeadLetterRoutingKey = deadLetterRoutingKey; DeadLetterChannelName = deadLetterChannelName; @@ -136,7 +136,7 @@ public class RmqSubscription : RmqSubscription where T : IRequest /// The number of milliseconds to delay the delivery of a requeue message for. /// The number of unacceptable messages to handle, before stopping reading from the channel. /// The durability of the queue definition in the broker. - /// Is this channel read asynchronously + /// Is this channel read asynchronously /// The channel factory to create channels for Consumer. /// Should we mirror the queue over multiple nodes /// The dead letter channel @@ -145,9 +145,10 @@ public class RmqSubscription : RmqSubscription where T : IRequest /// Should we make channels if they don't exist, defaults to creating /// How long to pause when a channel is empty in milliseconds /// How long to pause when there is a channel failure in milliseconds - public RmqSubscription(SubscriptionName name = null, - ChannelName channelName = null, - RoutingKey routingKey = null, + public RmqSubscription( + SubscriptionName? name = null, + ChannelName? channelName = null, + RoutingKey? routingKey = null, int bufferSize = 1, int noOfPerformers = 1, TimeSpan? timeOut = null, @@ -155,17 +156,17 @@ public RmqSubscription(SubscriptionName name = null, TimeSpan? requeueDelay = null, int unacceptableMessageLimit = 0, bool isDurable = false, - bool runAsync = false, - IAmAChannelFactory channelFactory = null, + MessagePumpType messagePumpType = MessagePumpType.Proactor, + IAmAChannelFactory? channelFactory = null, bool highAvailability = false, - ChannelName deadLetterChannelName = null, - RoutingKey deadLetterRoutingKey = null, + ChannelName? deadLetterChannelName = null, + RoutingKey? deadLetterRoutingKey = null, TimeSpan? ttl = null, OnMissingChannel makeChannels = OnMissingChannel.Create, - int emptyChannelDelay = 500, - int channelFailureDelay = 1000) + TimeSpan? emptyChannelDelay = null, + TimeSpan? channelFailureDelay = null) : base(typeof(T), name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount, requeueDelay, - unacceptableMessageLimit, isDurable, runAsync, channelFactory, highAvailability, deadLetterChannelName, deadLetterRoutingKey, ttl, makeChannels, emptyChannelDelay, channelFailureDelay) + unacceptableMessageLimit, isDurable, messagePumpType, channelFactory, highAvailability, deadLetterChannelName, deadLetterRoutingKey, ttl, makeChannels, emptyChannelDelay, channelFailureDelay) { } } diff --git a/src/Paramore.Brighter.MessagingGateway.Redis/ChannelFactory.cs b/src/Paramore.Brighter.MessagingGateway.Redis/ChannelFactory.cs index 7decc5d595..38870a1c4e 100644 --- a/src/Paramore.Brighter.MessagingGateway.Redis/ChannelFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.Redis/ChannelFactory.cs @@ -22,11 +22,14 @@ THE SOFTWARE. */ #endregion +using System.Threading; +using System.Threading.Tasks; + namespace Paramore.Brighter.MessagingGateway.Redis { /// /// Class RMQInputChannelFactory. - /// Creates instances of channels. Supports the creation of AMQP Application Layer channels using RabbitMQ + /// Creates instances of channels. Supports the creation of AMQP Application Layer channels using RabbitMQ /// public class ChannelFactory : IAmAChannelFactory { @@ -45,10 +48,10 @@ public ChannelFactory(RedisMessageConsumerFactory messageConsumerFactory) /// Creates the input channel. /// /// The subscription parameters with which to create the channel - /// IAmAnInputChannel. - public IAmAChannel CreateChannel(Subscription subscription) + /// An that provides access to a stream or queue + public IAmAChannelSync CreateSyncChannel(Subscription subscription) { - RedisSubscription rmqSubscription = subscription as RedisSubscription; + RedisSubscription? rmqSubscription = subscription as RedisSubscription; if (rmqSubscription == null) throw new ConfigurationException("We expect an RedisSubscription or RedisSubscription as a parameter"); @@ -59,5 +62,40 @@ public IAmAChannel CreateChannel(Subscription subscription) subscription.BufferSize ); } + + /// + /// Creates the input channel. + /// + /// The subscription parameters with which to create the channel + /// An that provides access to a stream or queue + public IAmAChannelAsync CreateAsyncChannel(Subscription subscription) + { + RedisSubscription? rmqSubscription = subscription as RedisSubscription; + if (rmqSubscription == null) + throw new ConfigurationException("We expect an RedisSubscription or RedisSubscription as a parameter"); + + return new ChannelAsync( + subscription.ChannelName, + subscription.RoutingKey, + _messageConsumerFactory.CreateAsync(subscription), + subscription.BufferSize + ); + } + + public Task CreateAsyncChannelAsync(Subscription subscription, CancellationToken ct = default) + { + RedisSubscription? rmqSubscription = subscription as RedisSubscription; + if (rmqSubscription == null) + throw new ConfigurationException("We expect an RedisSubscription or RedisSubscription as a parameter"); + + IAmAChannelAsync channel = new ChannelAsync( + subscription.ChannelName, + subscription.RoutingKey, + _messageConsumerFactory.CreateAsync(subscription), + subscription.BufferSize + ); + + return Task.FromResult(channel); + } } } diff --git a/src/Paramore.Brighter.MessagingGateway.Redis/HeaderResult.cs b/src/Paramore.Brighter.MessagingGateway.Redis/HeaderResult.cs index 4003d66575..1afb5175ac 100644 --- a/src/Paramore.Brighter.MessagingGateway.Redis/HeaderResult.cs +++ b/src/Paramore.Brighter.MessagingGateway.Redis/HeaderResult.cs @@ -49,7 +49,7 @@ public HeaderResult(TResult result, bool success) /// The type of the t new. /// The map. /// HeaderResult<TNew>. - public HeaderResult Map(Func> map) + public HeaderResult Map(Func> map) { if (Success) return map(Result); @@ -71,19 +71,19 @@ public HeaderResult Map(Func> map) /// Empties this instance. /// /// HeaderResult<TResult>. - public static HeaderResult Empty() + private static HeaderResult Empty() { if (typeof(TResult) == typeof(string)) { - return new HeaderResult((TResult)(object)string.Empty, false); + return new HeaderResult((TResult)(object)string.Empty, false); } if (typeof(TResult) == typeof(RoutingKey)) { - return new HeaderResult((TResult)(object)RoutingKey.Empty, false); + return new HeaderResult((TResult)(object)RoutingKey.Empty, false); } - return new HeaderResult(default(TResult), false); + return new HeaderResult(default, false); } } } diff --git a/src/Paramore.Brighter.MessagingGateway.Redis/Paramore.Brighter.MessagingGateway.Redis.csproj b/src/Paramore.Brighter.MessagingGateway.Redis/Paramore.Brighter.MessagingGateway.Redis.csproj index c92dd2d988..838ee4de9e 100644 --- a/src/Paramore.Brighter.MessagingGateway.Redis/Paramore.Brighter.MessagingGateway.Redis.csproj +++ b/src/Paramore.Brighter.MessagingGateway.Redis/Paramore.Brighter.MessagingGateway.Redis.csproj @@ -3,6 +3,7 @@ Provides an implementation of the messaging gateway for decoupled invocation in the Paramore.Brighter pipeline, using Redis< Ian Cooper netstandard2.0;net8.0;net9.0 + enable Redis;Command;Event;Service Activator;Decoupled;Invocation;Messaging;Remote;Command Dispatcher;Command Processor;Request;Service;Task Queue;Work Queue;Retry;Circuit Breaker;Availability false diff --git a/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageConsumer.cs b/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageConsumer.cs index 4e2f6ffeaf..c9af117ba4 100644 --- a/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageConsumer.cs +++ b/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageConsumer.cs @@ -24,8 +24,10 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; @@ -33,7 +35,7 @@ THE SOFTWARE. */ namespace Paramore.Brighter.MessagingGateway.Redis { - public class RedisMessageConsumer : RedisMessageGateway, IAmAMessageConsumer + public class RedisMessageConsumer : RedisMessageGateway, IAmAMessageConsumerSync, IAmAMessageConsumerAsync { /* see RedisMessageProducer to understand how we are using a dynamic recipient list model with Redis */ @@ -41,9 +43,9 @@ public class RedisMessageConsumer : RedisMessageGateway, IAmAMessageConsumer private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); private const string QUEUES = "queues"; - private readonly string _queueName; + private readonly ChannelName _queueName; - private readonly Dictionary _inflight = new Dictionary(); + private readonly Dictionary _inflight = new(); /// /// Creates a consumer that reads from a List in Redis via a BLPOP (so will block). @@ -53,15 +55,17 @@ public class RedisMessageConsumer : RedisMessageGateway, IAmAMessageConsumer /// The topic that the list subscribes to public RedisMessageConsumer( RedisMessagingGatewayConfiguration redisMessagingGatewayConfiguration, - string queueName, - string topic) - :base(redisMessagingGatewayConfiguration) + ChannelName queueName, + RoutingKey topic) + :base(redisMessagingGatewayConfiguration, topic) { _queueName = queueName; - Topic = topic; } /// + /// Acknowledge the message, removing it from the queue + /// + /// /// This a 'do nothing operation' as with Redis we pop the message from the queue to read; /// this allows us to have competing consumers, and thus a message is always 'consumed' even /// if we fail to process it. @@ -69,34 +73,84 @@ public RedisMessageConsumer( /// the job to run to completion. Brighter uses run to completion if shut down properly, but not if you /// just kill the process. /// If you need the level of reliability that unprocessed messages that return to the queue don't use Redis. - /// + /// /// public void Acknowledge(Message message) { s_logger.LogInformation("RmqMessageConsumer: Acknowledging message {Id}", message.Id); _inflight.Remove(message.Id); } + + /// + /// Acknowledge the message, removing it from the queue + /// + /// + /// This a 'do nothing operation' as with Redis we pop the message from the queue to read; + /// this allows us to have competing consumers, and thus a message is always 'consumed' even + /// if we fail to process it. + /// The risk with Redis is that we lose any in-flight message if we kill the service, without allowing + /// the job to run to completion. Brighter uses run to completion if shut down properly, but not if you + /// just kill the process. + /// If you need the level of reliability that unprocessed messages that return to the queue don't use Redis. + /// This is async over sync as the underlying operation does not block + /// + /// + public Task AcknowledgeAsync(Message message, CancellationToken cancellationToken = default(CancellationToken)) + { + Acknowledge(message); + return Task.CompletedTask; + } /// + /// Dispose of the Redis consumer + /// + /// /// Free up our RedisMangerPool, connections not held open between invocations of Receive, so you can create /// a consumer and keep it for program lifetime, disposing at the end only, without fear of a leak - /// + /// public void Dispose() { DisposePool(); GC.SuppressFinalize(this); } + + /// + public async ValueTask DisposeAsync() + { + await DisposePoolAsync().ConfigureAwait(false); + GC.SuppressFinalize(this); + } /// /// Clear the queue /// public void Purge() { - using var client = Pool.Value.GetClient(); s_logger.LogDebug("RmqMessageConsumer: Purging channel {ChannelName}", _queueName); + + using var client = GetClient(); + if (client == null) + throw new ChannelFailureException("RedisMessagingGateway: No Redis client available"); + //This kills the queue, not the messages, which we assume expire client.RemoveAllFromList(_queueName); } + + /// + /// Clear the queue + /// + /// The cancellation token + public async Task PurgeAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + s_logger.LogDebug("RmqMessageConsumer: Purging channel {ChannelName}", _queueName); + + var client = await GetClientAsync(cancellationToken); + if (client == null) + throw new ChannelFailureException("RedisMessagingGateway: No Redis client available"); + + //This kills the queue, not the messages, which we assume expire + await client.RemoveAllFromListAsync(_queueName, token: cancellationToken); + } /// /// Get the next message off the Redis list, within a timeout @@ -114,15 +168,20 @@ public Message[] Receive(TimeSpan? timeOut = null) } Message message; - IRedisClient client = null; + IRedisClient? client = null; timeOut ??= TimeSpan.FromMilliseconds(300); try { client = GetClient(); + if (client == null) + throw new ChannelFailureException("RedisMessagingGateway: No Redis client available"); + EnsureConnection(client); - (string msgId, string rawMsg) redisMessage = ReadMessage(client, timeOut.Value); - message = new RedisMessageCreator().CreateMessage(redisMessage.rawMsg); + (string? msgId, string rawMsg) redisMessage = ReadMessage(client, timeOut.Value); + if (redisMessage.msgId == null || string.IsNullOrEmpty(redisMessage.rawMsg)) + return []; + message = new RedisMessageCreator().CreateMessage(redisMessage.rawMsg); if (message.Header.MessageType != MessageType.MT_NONE && message.Header.MessageType != MessageType.MT_UNACCEPTABLE) { _inflight.Add(message.Id, redisMessage.msgId); @@ -130,14 +189,13 @@ public Message[] Receive(TimeSpan? timeOut = null) } catch (TimeoutException te) { - s_logger.LogError("Could not connect to Redis client within {Timeout} milliseconds", timeOut.Value.TotalMilliseconds.ToString()); - throw new ChannelFailureException($"Could not connect to Redis client within {timeOut.Value.TotalMilliseconds.ToString()} milliseconds", te); + s_logger.LogError("Could not connect to Redis client within {Timeout} milliseconds", timeOut.Value.TotalMilliseconds.ToString(CultureInfo.CurrentCulture)); + throw new ChannelFailureException($"Could not connect to Redis client within {timeOut.Value.TotalMilliseconds.ToString(CultureInfo.InvariantCulture)} milliseconds", te); } catch (RedisException re) { s_logger.LogError("Could not connect to Redis: {ErrorMessage}", re.Message); throw new ChannelFailureException("Could not connect to Redis client - see inner exception for details", re); - } finally { @@ -146,35 +204,90 @@ public Message[] Receive(TimeSpan? timeOut = null) return [message]; } + /// + /// Get the next message off the Redis list, within a timeout + /// + /// The period to await a message. Defaults to 300ms. + /// Cancel the receive operation + /// The message read from the list + public async Task ReceiveAsync(TimeSpan? timeOut = null, CancellationToken cancellationToken = default(CancellationToken)) + { + s_logger.LogDebug("RedisMessageConsumer: Preparing to retrieve next message from queue {ChannelName} with routing key {Topic}", _queueName, Topic); + + if (_inflight.Any()) + { + s_logger.LogError("RedisMessageConsumer: Preparing to retrieve next message from queue {ChannelName}, but have unacked or not rejected message", _queueName); + throw new ChannelFailureException($"Unacked message still in flight with id: {_inflight.Keys.First()}"); + } + + Message message; + timeOut ??= TimeSpan.FromMilliseconds(300); + try + { + await using IRedisClientAsync? client = await GetClientAsync(cancellationToken); + if (client == null) + throw new ChannelFailureException("RedisMessagingGateway: No Redis client available"); + + await EnsureConnectionAsync(client); + (string? msgId, string rawMsg) redisMessage = await ReadMessageAsync(client, timeOut.Value); + if (redisMessage.msgId == null || string.IsNullOrEmpty(redisMessage.rawMsg)) + return []; + + message = new RedisMessageCreator().CreateMessage(redisMessage.rawMsg); + + if (message.Header.MessageType != MessageType.MT_NONE && message.Header.MessageType != MessageType.MT_UNACCEPTABLE) + { + _inflight.Add(message.Id, redisMessage.msgId); + } + } + catch (TimeoutException te) + { + s_logger.LogError("Could not connect to Redis client within {Timeout} milliseconds", timeOut.Value.TotalMilliseconds.ToString(CultureInfo.InvariantCulture)); + throw new ChannelFailureException($"Could not connect to Redis client within {timeOut.Value.TotalMilliseconds.ToString(CultureInfo.InvariantCulture)} milliseconds", te); + } + catch (RedisException re) + { + s_logger.LogError("Could not connect to Redis: {ErrorMessage}", re.Message); + throw new ChannelFailureException("Could not connect to Redis client - see inner exception for details", re); + } + return [message]; + } + /// /// This a 'do nothing operation' as we have already popped /// - /// + /// The message to reject public void Reject(Message message) { _inflight.Remove(message.Id); } + /// + /// This a 'do nothing operation' as we have already popped + /// + /// The message to reject + /// The cancellation token + public async Task RejectAsync(Message message, CancellationToken cancellationToken = default(CancellationToken)) + { + Reject(message); + await Task.CompletedTask; + } + + /// /// Requeues the specified message. /// /// - /// Time to delay delivery of the message. 0 is no delay. Defaults to 0 + /// Delay is not supported /// True if the message was requeued public bool Requeue(Message message, TimeSpan? delay = null) { - delay ??= TimeSpan.Zero; - - //TODO: This blocks. We should use a thread to repost to the queue after n milliseconds, using a Timer - if (delay > TimeSpan.Zero) - { - Task.Delay(delay.Value).Wait(); - message.Header.Delayed = delay.Value; - } - message.Header.HandledCount++; - using var client = Pool.Value.GetClient(); + using var client = GetClient(); + if (client == null) + throw new ChannelFailureException("RedisMessagingGateway: No Redis client available"); + if (_inflight.ContainsKey(message.Id)) { var msgId = _inflight[message.Id]; @@ -186,18 +299,59 @@ public bool Requeue(Message message, TimeSpan? delay = null) } else { - s_logger.LogError(string.Format("Expected to find message id {0} in-flight but was not", message.Id)); + s_logger.LogError("Expected to find message id {messageId} in-flight but was not", message.Id); return false; } } - - /*Virtual to allow testing to simulate client failure*/ - protected virtual IRedisClient GetClient() + + /// + /// Requeues the specified message. + /// + /// + /// Delay is not supported + /// Cancel the requeue operation + /// True if the message was requeued + public async Task RequeueAsync(Message message, TimeSpan? delay = null, CancellationToken cancellationToken = default(CancellationToken)) { - return Pool.Value.GetClient(); + message.Header.HandledCount++; + var client = await GetClientAsync(cancellationToken); + if (client == null) + throw new ChannelFailureException("RedisMessagingGateway: No Redis client available"); + + if (_inflight.ContainsKey(message.Id)) + { + var msgId = _inflight[message.Id]; + await client.AddItemToListAsync(_queueName, msgId); + var redisMsg = CreateRedisMessage(message); + await StoreMessageAsync(client, redisMsg, long.Parse(msgId)); + _inflight.Remove(message.Id); + return true; + } + else + { + s_logger.LogError("Expected to find message id {messageId} in-flight but was not", message.Id); + return false; + } } + + // Virtual to allow testing to simulate client failure + protected virtual IRedisClient? GetClient() + { + if (s_pool == null) + throw new ChannelFailureException("RedisMessagingGateway: No connection pool available"); + return s_pool.Value.GetClient(); + } + // Virtual to allow testing to simulate client failure + protected virtual async Task GetClientAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + if (s_pool == null) + throw new ChannelFailureException("RedisMessagingGateway: No connection pool available"); + + return await s_pool.Value.GetClientAsync(cancellationToken); + } + private void EnsureConnection(IRedisClient client) { s_logger.LogDebug("RedisMessagingGateway: Creating queue {ChannelName}", _queueName); @@ -205,10 +359,18 @@ private void EnsureConnection(IRedisClient client) var key = Topic + "." + QUEUES; //subscribe us client.AddItemToSet(key, _queueName); - + } + + private async Task EnsureConnectionAsync(IRedisClientAsync client) + { + s_logger.LogDebug("RedisMessagingGateway: Creating queue {ChannelName}", _queueName); + //what is the queue list key + var key = Topic + "." + QUEUES; + //subscribe us + await client.AddItemToSetAsync(key, _queueName); } - private (string msgId, string rawMsg) ReadMessage(IRedisClient client, TimeSpan timeOut) + private (string? msgId, string rawMsg) ReadMessage(IRedisClient client, TimeSpan timeOut) { var msg = string.Empty; var latestId = client.BlockingRemoveStartFromList(_queueName, timeOut); @@ -230,5 +392,28 @@ private void EnsureConnection(IRedisClient client) return (latestId, msg); } + private async Task<(string? msgId, string rawMsg)> ReadMessageAsync(IRedisClientAsync client, TimeSpan timeOut) + { + var msg = string.Empty; + var latestId = await client.BlockingRemoveStartFromListAsync(_queueName, timeOut); + if (latestId != null) + { + var key = Topic + "." + latestId; + msg = await client.GetValueAsync(key); + s_logger.LogInformation( + "Redis: Received message from queue {ChannelName} with routing key {Topic}, message: {Request}", + _queueName, Topic, JsonSerializer.Serialize(msg, JsonSerialisationOptions.Options)); + } + else + { + s_logger.LogDebug( + "RmqMessageConsumer: Time out without receiving message from queue {ChannelName} with routing key {Topic}", + _queueName, Topic); + + } + return (latestId, msg); + } + + } } diff --git a/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageConsumerFactory.cs b/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageConsumerFactory.cs index c611806193..c62e88c52b 100644 --- a/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageConsumerFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageConsumerFactory.cs @@ -38,11 +38,33 @@ public RedisMessageConsumerFactory(RedisMessagingGatewayConfiguration configurat /// - /// Interface IAmAMessageProducerFactory + /// Create a consumer for the specified subscrciption /// - public IAmAMessageConsumer Create(Subscription subscription) + /// The subscription to create a consumer for + /// IAmAMessageConsumerSync + public IAmAMessageConsumerSync Create(Subscription subscription) { - return new RedisMessageConsumer(_configuration, subscription.ChannelName, subscription.RoutingKey); + RequireQueueName(subscription); + + return new RedisMessageConsumer(_configuration, subscription.ChannelName!, subscription.RoutingKey); + } + + private static void RequireQueueName(Subscription subscription) + { + if (subscription.ChannelName is null) + throw new ConfigurationException("RedisMessageConsumer: ChannelName is missing from the Subscription"); + } + + /// + /// Create a consumer for the specified subscrciption + /// + /// The subscription to create a consumer for + /// IAmAMessageConsumerAsync + public IAmAMessageConsumerAsync CreateAsync(Subscription subscription) + { + RequireQueueName(subscription); + + return new RedisMessageConsumer(_configuration, subscription.ChannelName!, subscription.RoutingKey); } } } diff --git a/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageCreator.cs b/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageCreator.cs index 6cc0dc5f60..503898b31a 100644 --- a/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageCreator.cs +++ b/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageCreator.cs @@ -60,7 +60,7 @@ public Message CreateMessage(string redisMessage) using var reader = new StringReader(redisMessage); var header = reader.ReadLine(); - if (header.TrimEnd() != ", but was {ErrorMessage}", redisMessage); return message; } var body = reader.ReadLine(); + if (body is null) + { + s_logger.LogError("Expected message to have a body, but was {ErrorMessage}", redisMessage); + return message; + } + if (body.TrimEnd() != ", but was {ErrorMessage}", redisMessage); @@ -108,9 +120,24 @@ private MessageBody ReadBody(StringReader reader) /// /// The raw header JSON /// - private MessageHeader ReadHeader(string headersJson) + private MessageHeader ReadHeader(string? headersJson) { + if (headersJson is null) + { + return FailureMessageHeader( + new HeaderResult(RoutingKey.Empty, false), + new HeaderResult(string.Empty, false) + ); + } + var headers = JsonSerializer.Deserialize>(headersJson, JsonSerialisationOptions.Options); + + if (headers is null) + return FailureMessageHeader( + new HeaderResult(RoutingKey.Empty, false), + new HeaderResult(string.Empty, false) + ); + //Read Message Id var messageId = ReadMessageId(headers); //Read TimeStamp @@ -199,7 +226,7 @@ private MessageHeader FailureMessageHeader(HeaderResult topic, Heade private HeaderResult ReadContentType(Dictionary headers) { - if (headers.TryGetValue(HeaderNames.CONTENT_TYPE, out string header)) + if (headers.TryGetValue(HeaderNames.CONTENT_TYPE, out string? header)) { return new HeaderResult(header, true); } @@ -210,7 +237,7 @@ private HeaderResult ReadCorrelationId(Dictionary header { var newCorrelationId = string.Empty; - if (headers.TryGetValue(HeaderNames.CORRELATION_ID, out string correlatonId)) + if (headers.TryGetValue(HeaderNames.CORRELATION_ID, out string? correlatonId)) { return new HeaderResult(correlatonId, true); } @@ -220,7 +247,7 @@ private HeaderResult ReadCorrelationId(Dictionary header private HeaderResult ReadDelay(Dictionary headers) { - if (headers.TryGetValue(HeaderNames.DELAYED_MILLISECONDS, out string header)) + if (headers.TryGetValue(HeaderNames.DELAYED_MILLISECONDS, out string? header)) { if (int.TryParse(header, out int delayedMilliseconds)) { @@ -232,7 +259,7 @@ private HeaderResult ReadDelay(Dictionary headers) private HeaderResult ReadHandledCount(Dictionary headers) { - if (headers.TryGetValue(HeaderNames.HANDLED_COUNT, out string header)) + if (headers.TryGetValue(HeaderNames.HANDLED_COUNT, out string? header)) { if (int.TryParse(header, out int handledCount)) { @@ -254,9 +281,12 @@ private HeaderResult ReadHandledCount(Dictionary headers) private HeaderResult> ReadMessageBag(Dictionary headers) { - if (headers.TryGetValue(HeaderNames.BAG, out string header)) + if (headers.TryGetValue(HeaderNames.BAG, out string? header)) { var bag = JsonSerializer.Deserialize>(header, JsonSerialisationOptions.Options); + if (bag is null) + return new HeaderResult>(new Dictionary(), false); + return new HeaderResult>(bag, true); } return new HeaderResult>(new Dictionary(), false); @@ -265,7 +295,7 @@ private HeaderResult> ReadMessageBag(Dictionary ReadMessageType(Dictionary headers) { - if (headers.TryGetValue(HeaderNames.MESSAGE_TYPE, out string header)) + if (headers.TryGetValue(HeaderNames.MESSAGE_TYPE, out string? header)) { if (Enum.TryParse(header, out MessageType messageType)) { @@ -278,7 +308,7 @@ private HeaderResult ReadMessageType(Dictionary hea private HeaderResult ReadMessageId(IDictionary headers) { - if (headers.TryGetValue(HeaderNames.MESSAGE_ID, out string header)) + if (headers.TryGetValue(HeaderNames.MESSAGE_ID, out string? header)) { return new HeaderResult(header, true); } @@ -288,7 +318,7 @@ private HeaderResult ReadMessageId(IDictionary headers) private HeaderResult ReadReplyTo(Dictionary headers) { - if (headers.TryGetValue(HeaderNames.REPLY_TO, out string header)) + if (headers.TryGetValue(HeaderNames.REPLY_TO, out string? header)) { return new HeaderResult(header, true); } @@ -302,7 +332,7 @@ private HeaderResult ReadReplyTo(Dictionary headers) /// The result, always a success because we don't break for missing timestamp, just use now private HeaderResult ReadTimeStamp(Dictionary headers) { - if (headers.TryGetValue(HeaderNames.TIMESTAMP, out string header)) + if (headers.TryGetValue(HeaderNames.TIMESTAMP, out string? header)) { if(DateTime.TryParse(header, out DateTime timestamp)) { @@ -315,7 +345,7 @@ private HeaderResult ReadTimeStamp(Dictionary headers) private HeaderResult ReadTopic(Dictionary headers) { var topic = string.Empty; - if (headers.TryGetValue(HeaderNames.TOPIC, out string header)) + if (headers.TryGetValue(HeaderNames.TOPIC, out string? header)) { return new HeaderResult(new RoutingKey(header), false); } diff --git a/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageGateway.cs b/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageGateway.cs index 3439fd9d56..adf2234745 100644 --- a/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageGateway.cs +++ b/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageGateway.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Threading.Tasks; using ServiceStack.Redis; namespace Paramore.Brighter.MessagingGateway.Redis @@ -30,31 +31,24 @@ namespace Paramore.Brighter.MessagingGateway.Redis public class RedisMessageGateway { protected TimeSpan MessageTimeToLive; - protected static Lazy Pool; - protected string Topic; + protected static Lazy? s_pool; + protected RoutingKey Topic; private readonly RedisMessagingGatewayConfiguration _gatewayConfiguration; - - protected RedisMessageGateway(RedisMessagingGatewayConfiguration redisMessagingGatewayConfiguration) + + protected RedisMessageGateway(RedisMessagingGatewayConfiguration redisMessagingGatewayConfiguration, RoutingKey topic) { _gatewayConfiguration = redisMessagingGatewayConfiguration; + Topic = topic; - Pool = new Lazy(() => + s_pool = new Lazy(() => { OverrideRedisClientDefaults(); return new RedisManagerPool(_gatewayConfiguration.RedisConnectionString, new RedisPoolConfig()); }); - - } - - - protected void DisposePool() - { - if (Pool.IsValueCreated) - Pool.Value.Dispose(); } - /// + // /// Creates a plain/text JSON representation of the message /// /// The Brighter message to convert @@ -62,85 +56,73 @@ protected void DisposePool() protected static string CreateRedisMessage(Message message) { //Convert the message into something we can put out via Redis i.e. a string - var redisMessage = RedisMessagePublisher.EMPTY_MESSAGE; using var redisMessageFactory = new RedisMessagePublisher(); - redisMessage = redisMessageFactory.Create(message); + string redisMessage = redisMessageFactory.Create(message); return redisMessage; } + /// + /// Dispose of the pool of connections to Redis + /// + protected virtual void DisposePool() + { + if (s_pool is { IsValueCreated: true }) + s_pool.Value.Dispose(); + } + + /// + /// Dispose of the pool of connections to Redis + /// + protected virtual async ValueTask DisposePoolAsync() + { + if (s_pool is { IsValueCreated: true }) + await ((IAsyncDisposable)s_pool.Value).DisposeAsync(); + } + /// /// Service Stack Redis provides global (static) configuration settings for how Redis behaves. /// We want to be able to override the defaults (or leave them if we think they are appropriate /// We run this as part of lazy initialization, as these are static and so we want to enforce the idea /// that you are globally setting them for any worker that runs in the same process /// A client could also set RedisConfig values directly in their own code, if preferred. - /// Our preference is to hide the global nature of that config inside this Lazy + /// Our preference is to hide the global nature of that config inside this Lazy initialization /// protected void OverrideRedisClientDefaults() { if (_gatewayConfiguration.BackoffMultiplier.HasValue) - { RedisConfig.BackOffMultiplier = _gatewayConfiguration.BackoffMultiplier.Value; - } if (_gatewayConfiguration.DeactivatedClientsExpiry.HasValue) - { RedisConfig.DeactivatedClientsExpiry = _gatewayConfiguration.DeactivatedClientsExpiry.Value; - } if (_gatewayConfiguration.DefaultConnectTimeout.HasValue) - { RedisConfig.DefaultConnectTimeout = _gatewayConfiguration.DefaultConnectTimeout.Value; - } if (_gatewayConfiguration.DefaultIdleTimeOutSecs.HasValue) - { RedisConfig.DefaultIdleTimeOutSecs = _gatewayConfiguration.DefaultIdleTimeOutSecs.Value; - } if (_gatewayConfiguration.DefaultReceiveTimeout.HasValue) - { RedisConfig.DefaultReceiveTimeout = _gatewayConfiguration.DefaultReceiveTimeout.Value; - } if (_gatewayConfiguration.DefaultRetryTimeout.HasValue) - { RedisConfig.DefaultRetryTimeout = _gatewayConfiguration.DefaultRetryTimeout.Value; - } if (_gatewayConfiguration.DefaultSendTimeout.HasValue) - { RedisConfig.DefaultSendTimeout = _gatewayConfiguration.DefaultSendTimeout.Value; - } if (_gatewayConfiguration.DisableVerboseLogging.HasValue) - { RedisConfig.EnableVerboseLogging = !_gatewayConfiguration.DisableVerboseLogging.Value; - } if (_gatewayConfiguration.HostLookupTimeoutMs.HasValue) - { RedisConfig.HostLookupTimeoutMs = _gatewayConfiguration.HostLookupTimeoutMs.Value; - } - if (_gatewayConfiguration.MaxPoolSize.HasValue) - { - RedisConfig.DefaultMaxPoolSize = _gatewayConfiguration.MaxPoolSize; - } + RedisConfig.DefaultMaxPoolSize = _gatewayConfiguration.MaxPoolSize; - if (_gatewayConfiguration.MessageTimeToLive.HasValue) - { - MessageTimeToLive = _gatewayConfiguration.MessageTimeToLive.Value; - } - else - { + if (_gatewayConfiguration is { MessageTimeToLive: not null }) MessageTimeToLive = TimeSpan.FromMinutes(10); - } - - if (_gatewayConfiguration.VerifyMasterConnections.HasValue) - { + + if (_gatewayConfiguration is { VerifyMasterConnections: not null }) RedisConfig.VerifyMasterConnections = _gatewayConfiguration.VerifyMasterConnections.Value; - } } /// @@ -155,6 +137,19 @@ protected void StoreMessage(IRedisClient client, string redisMessage, long msgId var key = Topic + "." + msgId.ToString(); client.SetValue(key, redisMessage, MessageTimeToLive); } + + /// + /// Store the actual message content to Redis - we only want one copy, regardless of number of queues + /// + /// The subscription to Redis + /// The message to write to Redis + /// The id to store it under + protected async Task StoreMessageAsync(IRedisClientAsync client, string redisMessage, long msgId) + { + //we store the message at topic + msg id + var key = Topic + "." + msgId.ToString(); + await client.SetValueAsync(key, redisMessage, MessageTimeToLive); + } } } diff --git a/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageProducer.cs index 59b8b737fc..55510f50b1 100644 --- a/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageProducer.cs @@ -26,6 +26,8 @@ THE SOFTWARE. */ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; using Paramore.Brighter.Observability; @@ -51,11 +53,14 @@ We end with a */ - public class RedisMessageProducer : RedisMessageGateway, IAmAMessageProducerSync + public class RedisMessageProducer( + RedisMessagingGatewayConfiguration redisMessagingGatewayConfiguration, + RedisMessagePublication publication) + : RedisMessageGateway(redisMessagingGatewayConfiguration, publication.Topic!), IAmAMessageProducerSync, IAmAMessageProducerAsync { private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - private readonly Publication _publication; + private readonly Publication _publication = publication; private const string NEXT_ID = "nextid"; private const string QUEUES = "queues"; @@ -63,65 +68,115 @@ public class RedisMessageProducer : RedisMessageGateway, IAmAMessageProducerSync /// The publication configuration for this producer /// public Publication Publication { get { return _publication; } } - - /// - /// The OTel Span we are writing Producer events too - /// - public Activity Span { get; set; } - public RedisMessageProducer( - RedisMessagingGatewayConfiguration redisMessagingGatewayConfiguration, - RedisMessagePublication publication) - - : base(redisMessagingGatewayConfiguration) - { - _publication = publication; - } + public Activity? Span { get; set; } public void Dispose() { DisposePool(); GC.SuppressFinalize(this); } + + public async ValueTask DisposeAsync() + { + await DisposePoolAsync(); + GC.SuppressFinalize(this); + } - /// + /// /// Sends the specified message. /// /// The message. /// Task. public void Send(Message message) { - using var client = Pool.Value.GetClient(); + 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) + ); + } + + /// + /// Sends the specified message. + /// + /// The message. + /// A token to cancel the send operation + /// Task. + public async Task SendAsync(Message message, CancellationToken cancellationToken = default) + { + if (s_pool is null) + throw new ChannelFailureException("RedisMessageProducer: Connection pool has not been initialized"); + + 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); + 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); + var nextMsgId = await IncrementMessageCounterAsync(client, cancellationToken); //store the message, against that id - StoreMessage(client, redisMessage, nextMsgId); + await StoreMessageAsync(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)); + 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) + ); } - + /// /// Sends the specified message. /// + /// + /// No delay support on Redis + /// /// The message. /// The sending delay /// Task. public void SendWithDelay(Message message, TimeSpan? delay = null) { - //No delay support implemented Send(message); } - + + /// + /// Sends the specified message. + /// + /// + /// No delay support on Redis + /// + /// The message. + /// The sending delay + /// Task. + public async Task SendWithDelayAsync(Message message, TimeSpan? delay, CancellationToken cancellationToken = default) + { + await SendAsync(message, cancellationToken); + } private IEnumerable PushToQueues(IRedisClient client, long nextMsgId) { @@ -134,6 +189,18 @@ private IEnumerable PushToQueues(IRedisClient client, long nextMsgId) } return queues; } + + private async Task> PushToQueuesAsync(IRedisClientAsync client, long nextMsgId, CancellationToken cancellationToken = default) + { + var key = Topic + "." + QUEUES; + var queues = (await client.GetAllItemsFromSetAsync(key, cancellationToken)).ToList(); + foreach (var queue in queues) + { + //First add to the queue itself + await client.AddItemToListAsync(queue, nextMsgId.ToString(), cancellationToken); + } + return queues; + } private long IncrementMessageCounter(IRedisClient client) { @@ -142,6 +209,13 @@ private long IncrementMessageCounter(IRedisClient client) var key = Topic + "." + NEXT_ID; return client.IncrementValue(key); } - - } + + private async Task IncrementMessageCounterAsync(IRedisClientAsync client, CancellationToken cancellationToken = default) + { + //This holds the next id for this topic; we use that to store message contents and signal to queue + //that there is a message to read. + var key = Topic + "." + NEXT_ID; + return await client.IncrementValueAsync(key, cancellationToken); + } + } } diff --git a/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageProducerFactory.cs b/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageProducerFactory.cs index 7d36a847a3..78a43e91e4 100644 --- a/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageProducerFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessageProducerFactory.cs @@ -22,38 +22,55 @@ THE SOFTWARE. */ #endregion using System.Collections.Generic; +using System.Threading.Tasks; namespace Paramore.Brighter.MessagingGateway.Redis { + /// + /// A factory for creating Redis message producers. This is used to create a collection of producers + /// that can send messages to Redis topics. + /// public class RedisMessageProducerFactory : IAmAMessageProducerFactory { private readonly RedisMessagingGatewayConfiguration _redisConfiguration; private readonly IEnumerable _publications; /// - /// Creates a collection of Redis message producers from the Redis publication information + /// Initializes a new instance of the class. /// - /// The connection to use to connect to Redis - /// The publications describing the Redis topics that we want to use + /// The configuration settings for connecting to Redis. + /// The collection of Redis message publications. public RedisMessageProducerFactory( - RedisMessagingGatewayConfiguration redisConfiguration, + RedisMessagingGatewayConfiguration redisConfiguration, IEnumerable publications) { _redisConfiguration = redisConfiguration; _publications = publications; } - - /// - public Dictionary Create() + + /// + /// Creates a dictionary of Redis message producers. + /// + /// A dictionary of indexed by . + public Dictionary Create() { var producers = new Dictionary(); foreach (var publication in _publications) { - producers[publication.Topic] = new RedisMessageProducer(_redisConfiguration, publication); + producers[publication.Topic!] = new RedisMessageProducer(_redisConfiguration, publication); } return producers; } + + /// + /// Asynchronously creates a dictionary of Redis message producers. + /// + /// A task that represents the asynchronous operation. The task result contains a dictionary of indexed by . + public Task> CreateAsync() + { + return Task.FromResult(Create()); + } } } diff --git a/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessagePublisher.cs b/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessagePublisher.cs index 9ed7c744c9..0d60617fd5 100644 --- a/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessagePublisher.cs +++ b/src/Paramore.Brighter.MessagingGateway.Redis/RedisMessagePublisher.cs @@ -24,6 +24,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Text; using System.Text.Json; @@ -38,12 +39,7 @@ public class RedisMessagePublisher: IDisposable private const string BEGINNING_OF_BODY = " headers) { - headers.Add(HeaderNames.CONTENT_TYPE, messageHeader.ContentType.ToString()); + headers.Add(HeaderNames.CONTENT_TYPE, messageHeader.ContentType ?? "text/plain"); } private void WriteCorrelationId(MessageHeader messageHeader, Dictionary headers) { - headers.Add(HeaderNames.CORRELATION_ID, messageHeader.CorrelationId.ToString()); + headers.Add(HeaderNames.CORRELATION_ID, messageHeader.CorrelationId); } private void WriteDelayedMilliseconds(MessageHeader messageHeader, Dictionary headers) { - headers.Add(HeaderNames.DELAYED_MILLISECONDS, messageHeader.Delayed.TotalMilliseconds.ToString()); + headers.Add(HeaderNames.DELAYED_MILLISECONDS, messageHeader.Delayed.TotalMilliseconds.ToString(CultureInfo.InvariantCulture)); } private void WriteHandledCount(MessageHeader messageHeader, Dictionary headers) @@ -126,7 +122,7 @@ private void WriteMessageBag(MessageHeader messageHeader, Dictionary headers) { - headers.Add(HeaderNames.MESSAGE_ID, messageHeader.MessageId.ToString()); + headers.Add(HeaderNames.MESSAGE_ID, messageHeader.MessageId); } private void WriteMessageType(MessageHeader messageHeader, Dictionary headers) @@ -136,7 +132,7 @@ private void WriteMessageType(MessageHeader messageHeader, Dictionary headers) { - headers.Add(HeaderNames.REPLY_TO, messageHeader.ReplyTo); + headers.Add(HeaderNames.REPLY_TO, messageHeader.ReplyTo ?? string.Empty); } private void WriteTopic(MessageHeader messageHeader, Dictionary headers) @@ -152,7 +148,7 @@ private void WriteTimeStamp(MessageHeader messageHeader, Dictionary /// How do we connect to Redis /// - public string RedisConnectionString { get; set; } + public string? RedisConnectionString { get; set; } } } diff --git a/src/Paramore.Brighter.MessagingGateway.Redis/RedisProducerRegistryFactory.cs b/src/Paramore.Brighter.MessagingGateway.Redis/RedisProducerRegistryFactory.cs index a098fe4667..00ae75887b 100644 --- a/src/Paramore.Brighter.MessagingGateway.Redis/RedisProducerRegistryFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.Redis/RedisProducerRegistryFactory.cs @@ -1,29 +1,32 @@ using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace Paramore.Brighter.MessagingGateway.Redis { - public class RedisProducerRegistryFactory : IAmAProducerRegistryFactory + public class RedisProducerRegistryFactory( + RedisMessagingGatewayConfiguration redisConfiguration, + IEnumerable publications) + : IAmAProducerRegistryFactory { - private readonly RedisMessagingGatewayConfiguration _redisConfiguration; - private readonly IEnumerable _publications; - - public RedisProducerRegistryFactory( - RedisMessagingGatewayConfiguration redisConfiguration, - IEnumerable publications) - { - _redisConfiguration = redisConfiguration; - _publications = publications; - } - /// /// Creates message producers. /// /// A has of middleware clients by topic, for sending messages to the middleware public IAmAProducerRegistry Create() { - var producerFactory = new RedisMessageProducerFactory(_redisConfiguration, _publications); + var producerFactory = new RedisMessageProducerFactory(redisConfiguration, publications); return new ProducerRegistry(producerFactory.Create()); } + + /// + /// Creates message producers. + /// + /// A has of middleware clients by topic, for sending messages to the middleware + public Task CreateAsync(CancellationToken ct = default) + { + return Task.FromResult(Create()); + } } } diff --git a/src/Paramore.Brighter.MessagingGateway.Redis/RedisSubscription.cs b/src/Paramore.Brighter.MessagingGateway.Redis/RedisSubscription.cs index 522c04c689..718178f0c1 100644 --- a/src/Paramore.Brighter.MessagingGateway.Redis/RedisSubscription.cs +++ b/src/Paramore.Brighter.MessagingGateway.Redis/RedisSubscription.cs @@ -41,29 +41,29 @@ public class RedisSubscription : Subscription /// The number of times you want to requeue a message before dropping it. /// Delay the delivery of a requeue message. 0 is no delay. Defaults to zero. /// The number of unacceptable messages to handle, before stopping reading from the channel. - /// Is this channel read asynchronously + /// Is this channel read asynchronously /// The channel factory to create channels for Consumer. /// Should we make channels if they don't exist, defaults to creating /// How long to pause when a channel is empty in milliseconds /// How long to pause when there is a channel failure in milliseconds - public RedisSubscription( + protected RedisSubscription( Type dataType, - SubscriptionName name = null, - ChannelName channelName = null, - RoutingKey routingKey = null, + SubscriptionName? name = null, + ChannelName? channelName = null, + RoutingKey? routingKey = null, int bufferSize = 1, int noOfPerformers = 1, TimeSpan? timeOut = null, int requeueCount = -1, TimeSpan? requeueDelay = null, int unacceptableMessageLimit = 0, - bool runAsync = false, - IAmAChannelFactory channelFactory = null, + MessagePumpType messagePumpType = MessagePumpType.Proactor, + IAmAChannelFactory? channelFactory = null, OnMissingChannel makeChannels = OnMissingChannel.Create, - int emptyChannelDelay = 500, - int channelFailureDelay = 1000) + TimeSpan? emptyChannelDelay = null, + TimeSpan? channelFailureDelay = null) : base(dataType, name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount, - requeueDelay, unacceptableMessageLimit, runAsync, channelFactory, makeChannels, emptyChannelDelay, channelFailureDelay) + requeueDelay, unacceptableMessageLimit, messagePumpType, channelFactory, makeChannels, emptyChannelDelay, channelFailureDelay) { } } @@ -80,30 +80,30 @@ public class RedisSubscription : RedisSubscription where T : IRequest /// The no of threads reading this channel. /// The timeout in milliseconds. /// The number of times you want to requeue a message before dropping it. - /// The number of milliseconds to delay the delivery of a requeue message for. + /// The period to delay adding a requeue /// The number of unacceptable messages to handle, before stopping reading from the channel. - /// Is this channel read asynchronously + /// Is this channel read asynchronously /// The channel factory to create channels for Consumer. /// Should we make channels if they don't exist, defaults to creating /// How long to pause when a channel is empty in milliseconds /// How long to pause when there is a channel failure in milliseconds public RedisSubscription( - SubscriptionName name = null, - ChannelName channelName = null, - RoutingKey routingKey = null, + SubscriptionName? name = null, + ChannelName? channelName = null, + RoutingKey? routingKey = null, int bufferSize = 1, int noOfPerformers = 1, TimeSpan? timeOut = null, int requeueCount = -1, TimeSpan? requeueDelay = null, int unacceptableMessageLimit = 0, - bool runAsync = false, - IAmAChannelFactory channelFactory = null, + MessagePumpType messagePumpType = MessagePumpType.Proactor, + IAmAChannelFactory? channelFactory = null, OnMissingChannel makeChannels = OnMissingChannel.Create, - int emptyChannelDelay = 500, - int channelFailureDelay = 1000) + TimeSpan? emptyChannelDelay = null, + TimeSpan? channelFailureDelay = null) : base(typeof(T), name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount, - requeueDelay, unacceptableMessageLimit, runAsync, channelFactory, makeChannels, emptyChannelDelay, channelFailureDelay) + requeueDelay, unacceptableMessageLimit, messagePumpType, channelFactory, makeChannels, emptyChannelDelay, channelFailureDelay) { } } diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlChainedConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlChainedConnectionProvider.cs index 1cfd35d21d..b514d8016a 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlChainedConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlChainedConnectionProvider.cs @@ -1,4 +1,27 @@ -using System; +#region Licence +/* The MIT License (MIT) +Copyright © 2024 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; using System.Threading.Tasks; using Azure.Core; @@ -27,11 +50,21 @@ public ServiceBusChainedClientConnectionProvider(RelationalDatabaseConfiguration _credential = new ChainedTokenCredential(credentialSources); } + /// + /// Get Access Token from the Provider synchronously. + /// Sync over Async, but alright in the context of creating a connection. + /// + /// The access token protected override AccessToken GetAccessTokenFromProvider() { return GetAccessTokenFromProviderAsync(CancellationToken.None).GetAwaiter().GetResult(); } + /// + /// Get Access Token from the Provider asynchronously. + /// + /// Cancels the read of the connection + /// protected override async Task GetAccessTokenFromProviderAsync(CancellationToken cancellationToken) { return await _credential.GetTokenAsync(new TokenRequestContext(AuthenticationTokenScopes), cancellationToken); diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs index e263cd0573..6994c93131 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs @@ -1,27 +1,61 @@ -using System.Threading; +#region Licence +/* The MIT License (MIT) +Copyright © 2024 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.Threading; using System.Threading.Tasks; using Azure.Core; using Azure.Identity; namespace Paramore.Brighter.MsSql.Azure { + /// + /// Provides a connection to an MS SQL database using Default Azure Credentials to acquire Access Tokens. + /// public class MsSqlDefaultAzureConnectionProvider : MsSqlAzureConnectionProviderBase { /// - /// Initialise a new instance of Ms Sql Connection provider using Default Azure Credentials to acquire Access Tokens. + /// Initializes a new instance of the class. /// - /// Ms Sql Configuration + /// The MS SQL configuration. public MsSqlDefaultAzureConnectionProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { } - + + /// + /// Gets the access token from the provider synchronously. + /// + /// The access token. protected override AccessToken GetAccessTokenFromProvider() { return GetAccessTokenFromProviderAsync(CancellationToken.None).GetAwaiter().GetResult(); } + /// + /// Gets the access token from the provider asynchronously. + /// + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains the access token. protected override async Task GetAccessTokenFromProviderAsync(CancellationToken cancellationToken) { var credential = new DefaultAzureCredential(); - return await credential.GetTokenAsync(new TokenRequestContext(AuthenticationTokenScopes), cancellationToken); } } diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs index b77fcffdd5..026ea2878f 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs @@ -1,25 +1,61 @@ -using System.Threading; +#region Licence +/* The MIT License (MIT) +Copyright © 2024 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.Threading; using System.Threading.Tasks; using Azure.Core; using Azure.Identity; namespace Paramore.Brighter.MsSql.Azure { + /// + /// Provides a connection to an MS SQL database using Managed Identity Credentials to acquire Access Tokens. + /// public class MsSqlManagedIdentityConnectionProvider : MsSqlAzureConnectionProviderBase { /// - /// Initialise a new instance of Ms Sql Connection provider using Managed Identity Credentials to acquire Access Tokens. + /// Initializes a new instance of the class. /// - /// Ms Sql Configuration + /// The MS SQL configuration. public MsSqlManagedIdentityConnectionProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { } + /// + /// Gets the access token from the provider synchronously. + /// Sync over async, but alright in the context of creating a connection. + /// + /// The access token. protected override AccessToken GetAccessTokenFromProvider() { return GetAccessTokenFromProviderAsync(CancellationToken.None).GetAwaiter().GetResult(); } + /// + /// Gets the access token from the provider asynchronously. + /// + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains the access token. protected override async Task GetAccessTokenFromProviderAsync(CancellationToken cancellationToken) { var credential = new ManagedIdentityCredential(); diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs index 765110b511..46221e994f 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs @@ -1,4 +1,27 @@ -using System; +#region Licence +/* The MIT License (MIT) +Copyright © 2024 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; using System.Threading.Tasks; using Azure.Core; @@ -6,45 +29,64 @@ namespace Paramore.Brighter.MsSql.Azure { + /// + /// Provides a connection to an MS SQL database using Shared Token Cache Credentials to acquire Access Tokens. + /// public class MsSqlSharedTokenCacheConnectionProvider : MsSqlAzureConnectionProviderBase { private const string _azureUserNameKey = "AZURE_USERNAME"; private const string _azureTenantIdKey = "AZURE_TENANT_ID"; - + private readonly string _azureUserName; private readonly string _azureTenantId; - + /// - /// Initialise a new instance of Ms Sql Connection provider using Shared Token Cache Credentials to acquire Access Tokens. + /// Initializes a new instance of the class using environment variables for credentials. /// - /// Ms Sql Configuration + /// The MS SQL configuration. public MsSqlSharedTokenCacheConnectionProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { _azureUserName = Environment.GetEnvironmentVariable(_azureUserNameKey); _azureTenantId = Environment.GetEnvironmentVariable(_azureTenantIdKey); } - + /// - /// Initialise a new instance of Ms Sql Connection provider using Shared Token Cache Credentials to acquire Access Tokens. + /// Initializes a new instance of the class using specified credentials. /// - /// Ms Sql Configuration + /// The MS SQL configuration. + /// The Azure username. + /// The Azure tenant ID. public MsSqlSharedTokenCacheConnectionProvider(RelationalDatabaseConfiguration configuration, string userName, string tenantId) : base(configuration) { _azureUserName = userName; _azureTenantId = tenantId; } + /// + /// Gets the access token from the provider synchronously. + /// Sync over async, but alright in the context of a connection provider. + /// + /// The access token. protected override AccessToken GetAccessTokenFromProvider() { return GetAccessTokenFromProviderAsync(CancellationToken.None).GetAwaiter().GetResult(); } + /// + /// Gets the access token from the provider asynchronously. + /// + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains the access token. protected override async Task GetAccessTokenFromProviderAsync(CancellationToken cancellationToken) { var credential = GetCredential(); return await credential.GetTokenAsync(new TokenRequestContext(AuthenticationTokenScopes), cancellationToken); } - + + /// + /// Gets the shared token cache credential. + /// + /// The shared token cache credential. private SharedTokenCacheCredential GetCredential() { return new SharedTokenCacheCredential(new SharedTokenCacheCredentialOptions diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs index 84b6308882..1ca502c93f 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs @@ -1,25 +1,61 @@ -using System.Threading; +#region Licence +/* The MIT License (MIT) +Copyright © 2024 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.Threading; using System.Threading.Tasks; using Azure.Core; using Azure.Identity; namespace Paramore.Brighter.MsSql.Azure { + /// + /// Provides a connection to an MS SQL database using Visual Studio Credentials to acquire Access Tokens. + /// public class MsSqlVisualStudioConnectionProvider : MsSqlAzureConnectionProviderBase { /// - /// Initialise a new instance of Ms Sql Connection provider using Visual Studio Credentials to acquire Access Tokens. + /// Initializes a new instance of the class. /// - /// Ms Sql Configuration + /// The MS SQL configuration. public MsSqlVisualStudioConnectionProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { } + /// + /// Gets the access token from the provider synchronously. + /// Sync over async, but alright in the context of a connection provider. + /// + /// The access token. protected override AccessToken GetAccessTokenFromProvider() { return GetAccessTokenFromProviderAsync(CancellationToken.None).GetAwaiter().GetResult(); } + /// + /// Gets the access token from the provider asynchronously. + /// + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains the access token. protected override async Task GetAccessTokenFromProviderAsync(CancellationToken cancellationToken) { var credential = new VisualStudioCredential(); diff --git a/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs b/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs index 1943ad7ec7..51bfedb469 100644 --- a/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs +++ b/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs @@ -117,6 +117,7 @@ public DynamoDbOutbox(DynamoDBContext context, DynamoDbConfiguration configurati /// /// /// Adds a message to the Outbox + /// Sync over async /// /// The message to be stored /// What is the context of this request; used to provide Span information to the call @@ -207,6 +208,7 @@ public async Task AddAsync( /// /// Delete messages from the Outbox + /// Sync over async /// /// The messages to delete /// What is the context for this request; used to access the Span @@ -237,6 +239,7 @@ public async Task DeleteAsync( /// /// Returns messages that have been successfully dispatched. Eventually consistent. + /// Sync over async /// /// How long ago was the message dispatched? /// What is the context for this request; used to access the Span @@ -307,6 +310,7 @@ public async Task> DispatchedMessagesAsync( /// /// Finds a message with the specified identifier. + /// Sync over async /// /// The identifier. /// What is the context for this request; used to access the Span @@ -321,7 +325,6 @@ public Message Get(string messageId, RequestContext requestContext, int outBoxTi .GetResult(); } - /// /// Finds a message with the specified identifier. /// @@ -418,6 +421,7 @@ private static void MarkMessageDispatched(DateTimeOffset dispatchedAt, MessageIt /// /// Returns messages that have yet to be dispatched + /// Sync over async /// /// How long ago as the message sent? /// What is the context for this request; used to access the Span diff --git a/src/Paramore.Brighter.ServiceActivator/BrighterSynchronizationContext.cs b/src/Paramore.Brighter.ServiceActivator/BrighterSynchronizationContext.cs deleted file mode 100644 index 3f3d323fa0..0000000000 --- a/src/Paramore.Brighter.ServiceActivator/BrighterSynchronizationContext.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; - -//Based on https://devblogs.microsoft.com/pfxteam/await-synchronizationcontext-and-console-apps/ - -namespace Paramore.Brighter.ServiceActivator -{ - internal class BrighterSynchronizationContext : SynchronizationContext - { - private readonly BlockingCollection> _queue = new(); - private int _operationCount; - - /// - /// When we have completed the operations, we can exit - /// - public override void OperationCompleted() - { - if (Interlocked.Decrement(ref _operationCount) == 0) - Complete(); - } - - /// - /// Tracks the number of ongoing operations, so we know when 'done' - /// - public override void OperationStarted() - { - Interlocked.Increment(ref _operationCount); - } - - /// Dispatches an asynchronous message to the synchronization context. - /// The System.Threading.SendOrPostCallback delegate to call. - /// The object passed to the delegate. - public override void Post(SendOrPostCallback d, object? state) - { - if (d == null) throw new ArgumentNullException(nameof(d)); - _queue.Add(new KeyValuePair(d, state)); - } - - /// Not supported. - public override void Send(SendOrPostCallback d, object? state) - { - throw new NotSupportedException("Synchronously sending is not supported."); - } - - /// Runs a loop to process all queued work items. - public void RunOnCurrentThread() - { - foreach (var workItem in _queue.GetConsumingEnumerable()) - workItem.Key(workItem.Value); - } - - /// Notifies the context that no more work will arrive. - private void Complete() { _queue.CompleteAdding(); } - } -} diff --git a/src/Paramore.Brighter.ServiceActivator/CommandProcessorProvider.cs b/src/Paramore.Brighter.ServiceActivator/CommandProcessorProvider.cs index 8985107860..55fdb8b1b8 100644 --- a/src/Paramore.Brighter.ServiceActivator/CommandProcessorProvider.cs +++ b/src/Paramore.Brighter.ServiceActivator/CommandProcessorProvider.cs @@ -1,4 +1,28 @@ -namespace Paramore.Brighter.ServiceActivator +#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 + +namespace Paramore.Brighter.ServiceActivator { public class CommandProcessorProvider : IAmACommandProcessorProvider { diff --git a/src/Paramore.Brighter.ServiceActivator/ConnectionBuilder.cs b/src/Paramore.Brighter.ServiceActivator/ConnectionBuilder.cs deleted file mode 100644 index 667a237a15..0000000000 --- a/src/Paramore.Brighter.ServiceActivator/ConnectionBuilder.cs +++ /dev/null @@ -1,273 +0,0 @@ -using System; - -namespace Paramore.Brighter.ServiceActivator -{ - public class ConnectionBuilder : - ConnectionBuilder.IConnectionBuilderName, - ConnectionBuilder.IConnectionBuilderChannelFactory, - ConnectionBuilder.IConnectionBuilderChannelType, - ConnectionBuilder.IConnectionBuilderChannelName, - ConnectionBuilder.IConnectionBuilderRoutingKey, - ConnectionBuilder.IConnectionBuilderOptionalBuild - { - private string? _name; - private IAmAChannelFactory? _channelFactory; - private Type? _type; - private string? _channelName; - private TimeSpan? _timeOut = null; - private string? _routingKey; - private int _buffersize = 1; - private int _unacceptableMessageLimit = 0; - private bool _isAsync = false; - private OnMissingChannel _makeChannel = OnMissingChannel.Create; - private int _noOfPeformers = 1; - private int _requeueCount = -1; - private TimeSpan _requeueDelay = TimeSpan.Zero; - private ConnectionBuilder() {} - - public static IConnectionBuilderName With => new ConnectionBuilder(); - - /// - /// The name of the subscription - used for identification - /// - /// The name to give this subscription - /// - public IConnectionBuilderChannelFactory ConnectionName(string name) - { - _name = name; - return this; - } - - /// - /// How do we build instances of the channel - sometimes this may build a consumer that builds the channel indirectly - /// - /// The channel to use - /// - public IConnectionBuilderChannelType ChannelFactory(IAmAChannelFactory channelFactory) - { - _channelFactory = channelFactory; - return this; - } - - /// - /// The data type of the channel - /// - /// The type that represents the type of the channel - /// - public IConnectionBuilderChannelName Type(Type type) - { - _type = type; - return this; - } - - /// - /// What is the name of the channel - /// - /// The name for the channel - /// - public IConnectionBuilderRoutingKey ChannelName(string channelName) - { - _channelName = channelName; - return this; - } - - /// - /// The routing key, or topic, that represents the channel in a broker - /// - /// - /// - public IConnectionBuilderOptionalBuild RoutingKey(string routingKey) - { - _routingKey = routingKey; - return this; - } - - /// - /// The timeout for waiting for a message when polling a queue - /// - /// The number of milliseconds to timeout (defaults to 300) - /// - public IConnectionBuilderOptionalBuild TimeOut(TimeSpan? timeOut = null) - { - timeOut ??= TimeSpan.FromMilliseconds(300); - _timeOut = timeOut; - return this; - } - - /// - /// Sets the size of the message buffer - /// - /// The number of messages to buffer (defaults to 1) - public IConnectionBuilderOptionalBuild BufferSize(int bufferSize) - { - _buffersize = bufferSize; - return this; - } - - /// - /// How many unacceptable messages on a queue before we shut down to avoid taking good messages off the queue that should be recovered later - /// - /// The upper bound for unacceptable messages, 0, the default indicates no limit - public IConnectionBuilderOptionalBuild UnacceptableMessageLimit(int unacceptableMessageLimit) - { - _unacceptableMessageLimit = unacceptableMessageLimit; - return this; - } - - /// - /// Is the pipeline that handles this message async? - /// - /// True if it is an async pipeline. Defaults to false as less beneficial that might be guessed with an event loop - public IConnectionBuilderOptionalBuild IsAsync(bool isAsync) - { - _isAsync = isAsync; - return this; - } - - /// - /// Should we create channels, or assume that they have been created separately and just confirm their existence and error if not available - /// - /// The action to take if a channel is missing. Defaults to create channel - /// - public IConnectionBuilderOptionalBuild MakeChannels(OnMissingChannel onMissingChannel) - { - _makeChannel = onMissingChannel; - return this; - } - - /// - /// The number of threads to run, when you want to use a scale up approach to competing consumers - /// Each thread is its own event loop - a performer - /// - /// How many threads to run, Defaults to 1 - /// - public IConnectionBuilderOptionalBuild NoOfPeformers(int noOfPerformers) - { - _noOfPeformers = noOfPerformers; - return this; - } - - - /// - /// How many times to requeue a message before we give up on it. A count of -1 is infinite retries - /// - /// The number of retries. Defaults to -1 - public IConnectionBuilderOptionalBuild RequeueCount(int requeueCount) - { - _requeueCount = requeueCount; - return this; - } - - /// - /// How long to delay before re-queuing a failed message - /// - /// The delay before requeueing. Default is 0, or no delay - public IConnectionBuilderOptionalBuild RequeueDelay(TimeSpan? delay = null) - { - delay ??= TimeSpan.Zero; - _requeueDelay = delay.Value; - return this; - } - - public Subscription Build() - { - if (_type is null) throw new ArgumentException("Cannot build connection without a Type"); - if (_name is null) throw new ArgumentException("Cannot build connection without a Name"); - if (_channelName is null) throw new ArgumentException("Cannot build connection without a Channel Name"); - if (_routingKey is null) throw new ArgumentException("Cannot build connection without a Routing Key"); - - return new Subscription(_type, - new SubscriptionName(_name), - new ChannelName(_channelName), - new RoutingKey(_routingKey), - channelFactory:_channelFactory, - timeOut: _timeOut, - bufferSize: _buffersize, - noOfPerformers: _noOfPeformers, - requeueCount: _requeueCount, - requeueDelay: _requeueDelay, - unacceptableMessageLimit: _unacceptableMessageLimit, - runAsync: _isAsync, - makeChannels:_makeChannel); - } - - public interface IConnectionBuilderName - { - IConnectionBuilderChannelFactory ConnectionName(string name); - } - - public interface IConnectionBuilderChannelFactory - { - IConnectionBuilderChannelType ChannelFactory(IAmAChannelFactory channelFactory); - } - - public interface IConnectionBuilderChannelType - { - IConnectionBuilderChannelName Type(Type type); - } - - public interface IConnectionBuilderChannelName - { - IConnectionBuilderRoutingKey ChannelName(string channelName); - } - - public interface IConnectionBuilderRoutingKey - { - IConnectionBuilderOptionalBuild RoutingKey(string routingKey); - } - - public interface IConnectionBuilderOptionalBuild - { - Subscription Build(); - - /// - /// Gets the timeout in milliseconds that we use to infer that nothing could be read from the channel i.e. is empty - /// or busy - /// - /// The timeout in milliseconds. - IConnectionBuilderOptionalBuild TimeOut(TimeSpan? timeOut); - - /// - /// How many messages do we store in the channel at any one time. When we read from a broker we need to balance - /// supporting fairness amongst multiple consuming threads (if any) and latency from reading from the broker - /// Must be greater than 1 and less than 10. - /// - IConnectionBuilderOptionalBuild BufferSize(int bufferSize); - - /// - /// Gets the number of messages before we will terminate the channel due to high error rates - /// - IConnectionBuilderOptionalBuild UnacceptableMessageLimit(int unacceptableMessageLimit); - - /// - /// Gets a value indicating whether this subscription should use an asynchronous pipeline - /// If it does it will process new messages from the queue whilst awaiting in prior messages' pipelines - /// This increases throughput (although it will no longer throttle use of the resources on the host machine). - /// - /// true if this instance should use an asynchronous pipeline; otherwise, false - IConnectionBuilderOptionalBuild IsAsync(bool isAsync); - - /// - /// Should we declare infrastructure, or should we just validate that it exists, and assume it is declared elsewhere - /// - IConnectionBuilderOptionalBuild MakeChannels(OnMissingChannel onMissingChannel); - - /// - /// Gets the no of threads that we will use to read from this channel. - /// - /// The no of performers. - IConnectionBuilderOptionalBuild NoOfPeformers(int noOfPerformers); - - /// - /// Gets or sets the number of times that we can requeue a message before we abandon it as poison pill. - /// - /// The requeue count. - IConnectionBuilderOptionalBuild RequeueCount(int requeueCount); - - /// - /// Gets or sets number of milliseconds to delay delivery of re-queued messages. - /// - IConnectionBuilderOptionalBuild RequeueDelay(TimeSpan? delay); - } - } -} diff --git a/src/Paramore.Brighter.ServiceActivator/ConsumerFactory.cs b/src/Paramore.Brighter.ServiceActivator/ConsumerFactory.cs index 51614592f5..080201fab0 100644 --- a/src/Paramore.Brighter.ServiceActivator/ConsumerFactory.cs +++ b/src/Paramore.Brighter.ServiceActivator/ConsumerFactory.cs @@ -80,13 +80,13 @@ public ConsumerFactory( public Consumer Create() { - if (_subscription.RunAsync) - return CreateAsync(); + if (_subscription.MessagePumpType == MessagePumpType.Proactor) + return CreateProactor(); else - return CreateBlocking(); + return CreateReactor(); } - private Consumer CreateBlocking() + private Consumer CreateReactor() { if (_messageMapperRegistry is null || _messageTransformerFactory is null) throw new ArgumentException("Message Mapper Registry and Transform factory must be set"); @@ -94,8 +94,8 @@ private Consumer CreateBlocking() if (_subscription.ChannelFactory is null) throw new ArgumentException("Subscription must have a Channel Factory in order to create a consumer."); - var channel = _subscription.ChannelFactory.CreateChannel(_subscription); - var messagePump = new MessagePumpBlocking(_commandProcessorProvider, _messageMapperRegistry, + var channel = _subscription.ChannelFactory.CreateSyncChannel(_subscription); + var messagePump = new Reactor(_commandProcessorProvider, _messageMapperRegistry, _messageTransformerFactory, _requestContextFactory, channel, _tracer, _instrumentationOptions) { Channel = channel, @@ -108,7 +108,7 @@ private Consumer CreateBlocking() return new Consumer(_consumerName, _subscription, channel, messagePump); } - private Consumer CreateAsync() + private Consumer CreateProactor() { if (_messageMapperRegistryAsync is null || _messageTransformerFactoryAsync is null) throw new ArgumentException("Message Mapper Registry and Transform factory must be set"); @@ -116,8 +116,8 @@ private Consumer CreateAsync() if (_subscription.ChannelFactory is null) throw new ArgumentException("Subscription must have a Channel Factory in order to create a consumer."); - var channel = _subscription.ChannelFactory.CreateChannel(_subscription); - var messagePump = new MessagePumpAsync(_commandProcessorProvider, _messageMapperRegistryAsync, + var channel = _subscription.ChannelFactory.CreateAsyncChannel(_subscription); + var messagePump = new Proactor(_commandProcessorProvider, _messageMapperRegistryAsync, _messageTransformerFactoryAsync, _requestContextFactory, channel, _tracer, _instrumentationOptions) { Channel = channel, diff --git a/src/Paramore.Brighter.ServiceActivator/DispatchBuilder.cs b/src/Paramore.Brighter.ServiceActivator/DispatchBuilder.cs index 925a330b67..012ef3168c 100644 --- a/src/Paramore.Brighter.ServiceActivator/DispatchBuilder.cs +++ b/src/Paramore.Brighter.ServiceActivator/DispatchBuilder.cs @@ -63,7 +63,7 @@ public static INeedACommandProcessorFactory StartNew() /// The command processor used to send and publish messages to handlers by the service activator. /// /// The command processor Factory. - /// The factory used to create a request context for a pipeline + /// The factory used to create a request synchronizationHelper for a pipeline /// INeedAMessageMapper. public INeedAMessageMapper CommandProcessorFactory( Func commandProcessorFactory, @@ -180,7 +180,7 @@ public interface INeedACommandProcessorFactory /// The command processor used to send and publish messages to handlers by the service activator. /// /// The command processor provider Factory. - /// The factory used to create a request context for a pipeline + /// The factory used to create a request synchronizationHelper for a pipeline /// INeedAMessageMapper. INeedAMessageMapper CommandProcessorFactory( Func commandProcessorFactory, @@ -244,7 +244,7 @@ public interface INeedObservability /// InstrumentationOptions.None - no telemetry /// InstrumentationOptions.RequestInformation - id and type of request /// InstrumentationOptions.RequestBody - body of the request - /// InstrumentationOptions.RequestContext - what is the context of the request + /// InstrumentationOptions.RequestContext - what is the synchronizationHelper of the request /// InstrumentationOptions.All - all of the above /// /// IAmADispatchBuilder diff --git a/src/Paramore.Brighter.ServiceActivator/Dispatcher.cs b/src/Paramore.Brighter.ServiceActivator/Dispatcher.cs index e771629a96..888832a409 100644 --- a/src/Paramore.Brighter.ServiceActivator/Dispatcher.cs +++ b/src/Paramore.Brighter.ServiceActivator/Dispatcher.cs @@ -103,7 +103,7 @@ public class Dispatcher : IDispatcher /// Async message mapper registry. /// Creates instances of Transforms /// Creates instances of Transforms async - /// The factory used to make a request context + /// The factory used to make a request synchronizationHelper /// What is the we will use for telemetry /// When creating a span for operations how noisy should the attributes be /// throws You must provide at least one type of message mapper registry @@ -131,6 +131,10 @@ public Dispatcher( if (messageMapperRegistry is null && messageMapperRegistryAsync is null) throw new ConfigurationException("You must provide a message mapper registry or an async message mapper registry"); + + //not all pipelines need a transformer factory + _messageTransformerFactory ??= new EmptyMessageTransformerFactory(); + _messageTransformerFactoryAsync ??= new EmptyMessageTransformerFactoryAsync(); State = DispatcherState.DS_NOTREADY; @@ -149,7 +153,7 @@ public Dispatcher( /// Async message mapper registry. /// Creates instances of Transforms /// Creates instances of Transforms async - /// The factory used to make a request context + /// The factory used to make a request synchronizationHelper /// What is the we will use for telemetry /// When creating a span for operations how noisy should the attributes be /// throws You must provide at least one type of message mapper registry @@ -371,7 +375,9 @@ private void Start() while (State != DispatcherState.DS_RUNNING) { - Task.Delay(100).Wait(); + Task.Delay(100) + .GetAwaiter() + .GetResult(); //Block main Dispatcher thread whilst control plane starts } } @@ -392,7 +398,7 @@ private Consumer CreateConsumer(Subscription subscription, int? consumerNumber) { s_logger.LogInformation("Dispatcher: Creating consumer number {ConsumerNumber} for subscription: {ChannelName}", consumerNumber, subscription.Name); var consumerFactoryType = typeof(ConsumerFactory<>).MakeGenericType(subscription.DataType); - if (!subscription.RunAsync) + if (subscription.MessagePumpType == MessagePumpType.Reactor) { var types = new[] { @@ -417,7 +423,6 @@ private Consumer CreateConsumer(Subscription subscription, int? consumerNumber) } else { - var types = new[] { typeof(IAmACommandProcessorProvider),typeof(Subscription), typeof(IAmAMessageMapperRegistryAsync), diff --git a/src/Paramore.Brighter.ServiceActivator/IAmAMessagePump.cs b/src/Paramore.Brighter.ServiceActivator/IAmAMessagePump.cs index 84f5b1e60b..bbed8ec702 100644 --- a/src/Paramore.Brighter.ServiceActivator/IAmAMessagePump.cs +++ b/src/Paramore.Brighter.ServiceActivator/IAmAMessagePump.cs @@ -32,25 +32,23 @@ namespace Paramore.Brighter.ServiceActivator /// The message pump reads s from a channel, translates them into a s and asks to /// dispatch them to an . It is classical message loop, and so should run until it receives an /// message. Clients of the message pump need to add a with of type to the to abort - /// a loop once started. The interface provides a method for this. + /// a loop once started. The interface provides a method for this. /// public interface IAmAMessagePump { /// - /// Runs the message loop + /// The of this message pump; indicates Reactor or Proactor /// - void Run(); + MessagePumpType MessagePumpType { get; } /// - /// Gets or sets the timeout that the pump waits for a message on the queue before it yields control for an interval, prior to resuming. + /// Runs the message pump, performing the following: + /// - Gets a message from a queue/stream + /// - Translates the message to the local type system + /// - Dispatches the message to waiting handlers + /// - Handles any exceptions that occur during the dispatch and tries to keep the pump alive /// - /// The timeout in milliseconds. - TimeSpan TimeOut { get; set; } - - /// - /// Gets or sets the channel to read messages from. - /// - /// The channel. - IAmAChannel Channel { get; set; } - } + /// + void Run(); + } } diff --git a/src/Paramore.Brighter.ServiceActivator/MessagePump.cs b/src/Paramore.Brighter.ServiceActivator/MessagePump.cs index 8c3e17261f..65dbe90a59 100644 --- a/src/Paramore.Brighter.ServiceActivator/MessagePump.cs +++ b/src/Paramore.Brighter.ServiceActivator/MessagePump.cs @@ -48,47 +48,31 @@ namespace Paramore.Brighter.ServiceActivator /// Retry and circuit breaker should be provided by exception policy using an attribute on the handler /// Timeout on the handler should be provided by timeout policy using an attribute on the handler /// - public abstract class MessagePump : IAmAMessagePump where TRequest : class, IRequest + public abstract class MessagePump where TRequest : class, IRequest { internal static readonly ILogger s_logger = ApplicationLogging.CreateLogger>(); protected readonly IAmACommandProcessorProvider CommandProcessorProvider; - private readonly IAmARequestContextFactory _requestContextFactory; - private readonly IAmABrighterTracer? _tracer; - private readonly InstrumentationOptions _instrumentationOptions; - private int _unacceptableMessageCount; - + protected readonly IAmARequestContextFactory RequestContextFactory; + protected readonly IAmABrighterTracer? Tracer; + protected readonly InstrumentationOptions InstrumentationOptions; + protected int UnacceptableMessageCount; + /// - /// Constructs a message pump. The message pump is the heart of a consumer. It runs a loop that performs the following: - /// - Gets a message from a queue/stream - /// - Translates the message to the local type system - /// - Dispatches the message to waiting handlers - /// The message pump is a classic event loop and is intended to be run on a single-thread + /// The delay to wait when the channel has failed /// - /// Provides a correctly scoped command processor - /// Provides a request context - /// What is the we will use for telemetry - /// - /// When creating a span for operations how noisy should the attributes be - protected MessagePump( - IAmACommandProcessorProvider commandProcessorProvider, - IAmARequestContextFactory requestContextFactory, - IAmABrighterTracer? tracer, - IAmAChannel channel, - InstrumentationOptions instrumentationOptions = InstrumentationOptions.All) - { - CommandProcessorProvider = commandProcessorProvider; - _requestContextFactory = requestContextFactory; - _tracer = tracer; - _instrumentationOptions = instrumentationOptions; - Channel = channel; - } + public TimeSpan ChannelFailureDelay { get; set; } /// - /// How long to wait for a message before timing out + /// The delay to wait when the channel is empty /// - public TimeSpan TimeOut { get; set; } - + public TimeSpan EmptyChannelDelay { get; set; } + + /// + /// The of this message pump; indicates Reactor or Proactor + /// + public abstract MessagePumpType MessagePumpType { get; } + /// /// How many times to requeue a message before discarding it /// @@ -98,318 +82,50 @@ protected MessagePump( /// How long to wait before requeuing a message /// public TimeSpan RequeueDelay { get; set; } - - /// - /// The number of unacceptable messages to receive before stopping the message pump - /// - public int UnacceptableMessageLimit { get; set; } - - /// - /// The channel to receive messages from - /// - public IAmAChannel Channel { get; set; } /// - /// The delay to wait when the channel is empty + /// How long to wait for a message before timing out /// - public int EmptyChannelDelay { get; set; } - + public TimeSpan TimeOut { get; set; } + + /// - /// The delay to wait when the channel has failed + /// The number of unacceptable messages to receive before stopping the message pump /// - public int ChannelFailureDelay { get; set; } + public int UnacceptableMessageLimit { get; set; } /// - /// Runs the message pump, performing the following: - /// - Gets a message from a queue/stream - /// - Translates the message to the local type system - /// - Dispatches the message to waiting handlers - /// - Handles any exceptions that occur during the dispatch and tries to keep the pump alive + /// Constructs a message pump. The message pump is the heart of a consumer. It runs a loop that performs the following: + /// - Gets a message from a queue/stream + /// - Translates the message to the local type system + /// - Dispatches the message to waiting handlers + /// The message pump is a classic event loop and is intended to be run on a single-thread /// - /// - public void Run() - { - var pumpSpan = _tracer?.CreateMessagePumpSpan(MessagePumpSpanOperation.Begin, Channel.RoutingKey, MessagingSystem.InternalBus, _instrumentationOptions); - do - { - if (UnacceptableMessageLimitReached()) - { - Channel.Dispose(); - break; - } - - s_logger.LogDebug("MessagePump: Receiving messages from channel {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); - - Activity? span = null; - Message? message = null; - try - { - message = Channel.Receive(TimeOut); - span = _tracer?.CreateSpan(MessagePumpSpanOperation.Receive, message, MessagingSystem.InternalBus, _instrumentationOptions); - } - catch (ChannelFailureException ex) when (ex.InnerException is BrokenCircuitException) - { - s_logger.LogWarning("MessagePump: BrokenCircuitException messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); - var errorSpan = _tracer?.CreateMessagePumpExceptionSpan(ex, Channel.RoutingKey, MessagePumpSpanOperation.Receive, MessagingSystem.InternalBus, _instrumentationOptions); - _tracer?.EndSpan(errorSpan); - Task.Delay(ChannelFailureDelay).Wait(); - continue; - } - catch (ChannelFailureException ex) - { - s_logger.LogWarning("MessagePump: ChannelFailureException messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); - var errorSpan = _tracer?.CreateMessagePumpExceptionSpan(ex, Channel.RoutingKey, MessagePumpSpanOperation.Receive, MessagingSystem.InternalBus, _instrumentationOptions); - _tracer?.EndSpan(errorSpan ); - Task.Delay(ChannelFailureDelay).Wait(); - continue; - } - catch (Exception ex) - { - s_logger.LogError(ex, "MessagePump: Exception receiving messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); - var errorSpan = _tracer?.CreateMessagePumpExceptionSpan(ex, Channel.RoutingKey, MessagePumpSpanOperation.Receive, MessagingSystem.InternalBus, _instrumentationOptions); - _tracer?.EndSpan(errorSpan ); - } - - if (message is null) - { - Channel.Dispose(); - span?.SetStatus(ActivityStatusCode.Error, "Could not receive message. Note that should return an MT_NONE from an empty queue on timeout"); - _tracer?.EndSpan(span); - throw new Exception("Could not receive message. Note that should return an MT_NONE from an empty queue on timeout"); - } - - // empty queue - if (message.Header.MessageType == MessageType.MT_NONE) - { - span?.SetStatus(ActivityStatusCode.Ok); - _tracer?.EndSpan(span); - Task.Delay(EmptyChannelDelay).Wait(); - continue; - } - - // failed to parse a message from the incoming data - if (message.Header.MessageType == MessageType.MT_UNACCEPTABLE) - { - s_logger.LogWarning("MessagePump: Failed to parse a message from the incoming message with id {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); - span?.SetStatus(ActivityStatusCode.Error, $"MessagePump: Failed to parse a message from the incoming message with id {message.Id} from {Channel.Name} on thread # {Environment.CurrentManagedThreadId}"); - _tracer?.EndSpan(span); - IncrementUnacceptableMessageLimit(); - AcknowledgeMessage(message); - - continue; - } - - // QUIT command - if (message.Header.MessageType == MessageType.MT_QUIT) - { - s_logger.LogInformation("MessagePump: Quit receiving messages from {ChannelName} on thread #{ManagementThreadId}", Channel.Name, Environment.CurrentManagedThreadId); - span?.SetStatus(ActivityStatusCode.Ok); - _tracer?.EndSpan(span); - Channel.Dispose(); - break; - } - - // Serviceable message - try - { - RequestContext context = InitRequestContext(span, message); - - var request = TranslateMessage(message, context); - - CommandProcessorProvider.CreateScope(); - - DispatchRequest(message.Header, request, context); - - span?.SetStatus(ActivityStatusCode.Ok); - } - catch (AggregateException aggregateException) - { - var stop = false; - var defer = false; - - foreach (var exception in aggregateException.InnerExceptions) - { - if (exception is ConfigurationException configurationException) - { - s_logger.LogCritical(configurationException, "MessagePump: Stopping receiving of messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); - stop = true; - break; - } - - if (exception is DeferMessageAction) - { - defer = true; - continue; - } - - s_logger.LogError(exception, "MessagePump: Failed to dispatch message {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); - } - - if (defer) - { - s_logger.LogDebug("MessagePump: Deferring message {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); - span?.SetStatus(ActivityStatusCode.Error, $"Deferring message {message.Id} for later action"); - if (RequeueMessage(message)) - continue; - } - - if (stop) - { - RejectMessage(message); - span?.SetStatus(ActivityStatusCode.Error, $"MessagePump: Stopping receiving of messages from {Channel.Name} with {Channel.RoutingKey} on thread # {Environment.CurrentManagedThreadId}"); - Channel.Dispose(); - break; - } - - span?.SetStatus(ActivityStatusCode.Error, $"MessagePump: Failed to dispatch message {message.Id} from {Channel.Name} with {Channel.RoutingKey} on thread # {Environment.CurrentManagedThreadId}"); - } - catch (ConfigurationException configurationException) - { - s_logger.LogCritical(configurationException,"MessagePump: Stopping receiving of messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); - RejectMessage(message); - span?.SetStatus(ActivityStatusCode.Error, $"MessagePump: Stopping receiving of messages from {Channel.Name} on thread # {Environment.CurrentManagedThreadId}"); - Channel.Dispose(); - break; - } - catch (DeferMessageAction) - { - s_logger.LogDebug("MessagePump: Deferring message {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); - - span?.SetStatus(ActivityStatusCode.Error, $"Deferring message {message.Id} for later action"); - - if (RequeueMessage(message)) continue; - } - catch (MessageMappingException messageMappingException) - { - s_logger.LogWarning(messageMappingException, "MessagePump: Failed to map message {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); - - IncrementUnacceptableMessageLimit(); - - span?.SetStatus(ActivityStatusCode.Error, $"MessagePump: Failed to map message {message.Id} from {Channel.Name} with {Channel.RoutingKey} on thread # {Thread.CurrentThread.ManagedThreadId}"); - } - catch (Exception e) - { - s_logger.LogError(e, - "MessagePump: Failed to dispatch message '{Id}' from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", - message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); - - span?.SetStatus(ActivityStatusCode.Error,$"MessagePump: Failed to dispatch message '{message.Id}' from {Channel.Name} with {Channel.RoutingKey} on thread # {Environment.CurrentManagedThreadId}"); - } - finally - { - _tracer?.EndSpan(span); - CommandProcessorProvider.ReleaseScope(); - } - - AcknowledgeMessage(message); - - } while (true); - - s_logger.LogInformation( - "MessagePump0: Finished running message loop, no longer receiving messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", - Channel.Name, Channel.RoutingKey, Thread.CurrentThread.ManagedThreadId); - _tracer?.EndSpan(pumpSpan); - - } - - private void AcknowledgeMessage(Message message) + /// Provides a correctly scoped command processor + /// Provides a request synchronizationHelper + /// What is the we will use for telemetry + /// + /// When creating a span for operations how noisy should the attributes be + protected MessagePump( + IAmACommandProcessorProvider commandProcessorProvider, + IAmARequestContextFactory requestContextFactory, + IAmABrighterTracer? tracer, + InstrumentationOptions instrumentationOptions = InstrumentationOptions.All) { - s_logger.LogDebug( - "MessagePump: Acknowledge message {Id} read from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", - message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); - - Channel.Acknowledge(message); + CommandProcessorProvider = commandProcessorProvider; + RequestContextFactory = requestContextFactory; + Tracer = tracer; + InstrumentationOptions = instrumentationOptions; } - private bool DiscardRequeuedMessagesEnabled() + protected bool DiscardRequeuedMessagesEnabled() { return RequeueCount != -1; } - // Implemented in a derived class to dispatch to the relevant type of pipeline via the command processor - // i..e an async pipeline uses SendAsync/PublishAsync and a blocking pipeline uses Send/Publish - protected abstract void DispatchRequest(MessageHeader messageHeader, TRequest request, RequestContext context); - - private void IncrementUnacceptableMessageLimit() - { - _unacceptableMessageCount++; - } - - private RequestContext InitRequestContext(Activity? span, Message message) - { - var context = _requestContextFactory.Create(); - context.Span = span; - context.OriginatingMessage = message; - context.Bag.AddOrUpdate("ChannelName", Channel.Name, (_, _) => Channel.Name); - context.Bag.AddOrUpdate("RequestStart", DateTime.UtcNow, (_, _) => DateTime.UtcNow); - return context; - } - - private void RejectMessage(Message message) - { - s_logger.LogWarning("MessagePump: Rejecting message {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Thread.CurrentThread.ManagedThreadId); - IncrementUnacceptableMessageLimit(); - - Channel.Reject(message); - } - - /// - /// Requeue Message - /// - /// Message to be Requeued - /// Returns True if the message should be acked, false if the channel has handled it - private bool RequeueMessage(Message message) - { - message.Header.UpdateHandledCount(); - - if (DiscardRequeuedMessagesEnabled()) - { - if (message.HandledCountReached(RequeueCount)) - { - var originalMessageId = message.Header.Bag.TryGetValue(Message.OriginalMessageIdHeaderName, out object? value) ? value.ToString() : null; - - s_logger.LogError( - "MessagePump: Have tried {RequeueCount} times to handle this message {Id}{OriginalMessageId} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}, dropping message.", - RequeueCount, - message.Id, - string.IsNullOrEmpty(originalMessageId) - ? string.Empty - : $" (original message id {originalMessageId})", - Channel.Name, - Channel.RoutingKey, - Thread.CurrentThread.ManagedThreadId); - - RejectMessage(message); - return false; - } - } - - s_logger.LogDebug( - "MessagePump: Re-queueing message {Id} from {ManagementThreadId} on thread # {ChannelName} with {RoutingKey}", message.Id, - Channel.Name, Channel.RoutingKey, Thread.CurrentThread.ManagedThreadId); - - return Channel.Requeue(message, RequeueDelay); - } - - protected abstract TRequest TranslateMessage(Message message, RequestContext requestContext); - - private bool UnacceptableMessageLimitReached() + protected void IncrementUnacceptableMessageLimit() { - if (UnacceptableMessageLimit == 0) return false; - - if (_unacceptableMessageCount >= UnacceptableMessageLimit) - { - s_logger.LogCritical( - "MessagePump: Unacceptable message limit of {UnacceptableMessageLimit} reached, stopping reading messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", - UnacceptableMessageLimit, - Channel.Name, - Channel.RoutingKey, - Environment.CurrentManagedThreadId - ); - - return true; - } - return false; + UnacceptableMessageCount++; } protected void ValidateMessageType(MessageType messageType, TRequest request) diff --git a/src/Paramore.Brighter.ServiceActivator/MessagePumpAsync.cs b/src/Paramore.Brighter.ServiceActivator/MessagePumpAsync.cs deleted file mode 100644 index 32c6cede5a..0000000000 --- a/src/Paramore.Brighter.ServiceActivator/MessagePumpAsync.cs +++ /dev/null @@ -1,204 +0,0 @@ -#region Licence -/* The MIT License (MIT) -Copyright © 2022 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.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Paramore.Brighter.Observability; - -namespace Paramore.Brighter.ServiceActivator -{ - /// - /// Used when we don't want to block for I/O, but queue on a completion port and be notified when done - /// Adopts a single-threaded apartment model. We have one thread, all work - messages and calbacks is queued to that a single work queue - /// When a callback is signalled it is queued next, and will be picked up when the current message completes or waits itself - /// Strict ordering of messages will be lost as no guarantee what order I/O operations will complete - do not use if strict ordering required - /// Only used one thread, so predictable performance, but may have many messages queued. Once queue length exceeds buffer size, we will stop reading new work - /// Based on https://devblogs.microsoft.com/pfxteam/await-synchronizationcontext-and-console-apps/ - /// - /// The Request on the Data Type Channel - public class MessagePumpAsync : MessagePump where TRequest : class, IRequest - { - private readonly UnwrapPipelineAsync _unwrapPipeline; - - /// - /// Constructs a message pump - /// - /// Provides a way to grab a command processor correctly scoped - /// The registry of mappers - /// The factory that lets us create instances of transforms - /// A factory to create instances of request context, used to add context to a pipeline - /// What is the tracer we will use for telemetry - /// When creating a span for operations how noisy should the attributes be - public MessagePumpAsync( - IAmACommandProcessorProvider commandProcessorProvider, - IAmAMessageMapperRegistryAsync messageMapperRegistry, - IAmAMessageTransformerFactoryAsync messageTransformerFactory, - IAmARequestContextFactory requestContextFactory, - IAmAChannel channel, - IAmABrighterTracer? tracer = null, - InstrumentationOptions instrumentationOptions = InstrumentationOptions.All) - : base(commandProcessorProvider, requestContextFactory, tracer, channel, instrumentationOptions) - { - var transformPipelineBuilder = new TransformPipelineBuilderAsync(messageMapperRegistry, messageTransformerFactory); - _unwrapPipeline = transformPipelineBuilder.BuildUnwrapPipeline(); - } - - protected override void DispatchRequest(MessageHeader messageHeader, TRequest request, RequestContext requestContext) - { - s_logger.LogDebug("MessagePump: Dispatching message {Id} from {ChannelName} on thread # {ManagementThreadId}", request.Id, Thread.CurrentThread.ManagedThreadId, Channel.Name); - requestContext.Span?.AddEvent(new ActivityEvent("Dispatch Message")); - - var messageType = messageHeader.MessageType; - - ValidateMessageType(messageType, request); - - switch (messageType) - { - case MessageType.MT_COMMAND: - { - RunDispatch(SendAsync, request, requestContext); - break; - } - case MessageType.MT_DOCUMENT: - case MessageType.MT_EVENT: - { - RunDispatch(PublishAsync, request, requestContext); - break; - } - } - } - - protected override TRequest TranslateMessage(Message message, RequestContext requestContext) - { - s_logger.LogDebug( - "MessagePump: Translate message {Id} on thread # {ManagementThreadId}", - message.Id, Thread.CurrentThread.ManagedThreadId - ); - requestContext.Span?.AddEvent(new ActivityEvent("Translate Message")); - - return RunTranslate(TranslateAsync, message, requestContext); - } - - private static void RunDispatch( - Action act, TRequest request, - RequestContext requestContext, - CancellationToken cancellationToken = default - ) - { - if (act == null) throw new ArgumentNullException(nameof(act)); - - var prevCtx = SynchronizationContext.Current; - - try - { - // Establish the new context - var context = new BrighterSynchronizationContext(); - SynchronizationContext.SetSynchronizationContext(context); - - context.OperationStarted(); - - act(request, requestContext, cancellationToken); - - context.OperationCompleted(); - - // Pump continuations and propagate any exceptions - context.RunOnCurrentThread(); - } - finally - { - SynchronizationContext.SetSynchronizationContext(prevCtx); - } - } - - private static TRequest RunTranslate( - Func> act, - Message message, - RequestContext requestContext, - CancellationToken cancellationToken = default - ) - { - if (act == null) throw new ArgumentNullException(nameof(act)); - - var prevCtx = SynchronizationContext.Current; - try - { - // Establish the new context - var context = new BrighterSynchronizationContext(); - SynchronizationContext.SetSynchronizationContext(context); - - context.OperationStarted(); - - var future = act(message, requestContext, cancellationToken); - - future.ContinueWith(delegate { context.OperationCompleted(); }, TaskScheduler.Default); - - // Pump continuations and propagate any exceptions - context.RunOnCurrentThread(); - - return future.GetAwaiter().GetResult(); - } - catch (ConfigurationException) - { - throw; - } - catch (Exception exception) - { - throw new MessageMappingException($"Failed to map message {message.Id} using pipeline for type {typeof(TRequest).FullName} ", exception); - } - finally - { - SynchronizationContext.SetSynchronizationContext(prevCtx); - } - } - - private async void PublishAsync(TRequest request, RequestContext requestContext, CancellationToken cancellationToken = default) - { - await CommandProcessorProvider.Get().PublishAsync(request, requestContext, continueOnCapturedContext: true, cancellationToken); - } - - private async void SendAsync(TRequest request, RequestContext requestContext, CancellationToken cancellationToken = default) - { - await CommandProcessorProvider.Get().SendAsync(request,requestContext, continueOnCapturedContext: true, cancellationToken); - } - - private async Task TranslateAsync(Message message, RequestContext requestContext, CancellationToken cancellationToken = default) - { - try - { - return await _unwrapPipeline.UnwrapAsync(message, requestContext, cancellationToken); - } - catch (ConfigurationException) - { - throw; - } - catch (Exception exception) - { - throw new MessageMappingException($"Failed to map message {message.Id} using pipeline for type {typeof(TRequest).FullName} ", exception); - } - } - } -} diff --git a/src/Paramore.Brighter.ServiceActivator/MessagePumpBlocking.cs b/src/Paramore.Brighter.ServiceActivator/MessagePumpBlocking.cs deleted file mode 100644 index 3b01a4003d..0000000000 --- a/src/Paramore.Brighter.ServiceActivator/MessagePumpBlocking.cs +++ /dev/null @@ -1,115 +0,0 @@ -#region Licence -/* The MIT License (MIT) -Copyright © 2022 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.Diagnostics; -using System.Threading; -using Microsoft.Extensions.Logging; -using Paramore.Brighter.Observability; - -namespace Paramore.Brighter.ServiceActivator -{ - /// - /// Used when the message pump should block for I/O - /// Will guarantee strict ordering of the messages on the queue - /// Predictable performance as only one thread, allows you to configure number of performers for number of threads to use - /// Lower throughput than async - /// - /// - public class MessagePumpBlocking : MessagePump where TRequest : class, IRequest - { - private readonly UnwrapPipeline _unwrapPipeline; - - /// - /// Constructs a message pump - /// - /// Provides a way to grab a command processor correctly scoped - /// The registry of mappers - /// The factory that lets us create instances of transforms - /// A factory to create instances of request context, used to add context to a pipeline - /// What is the tracer we will use for telemetry - /// When creating a span for operations how noisy should the attributes be - public MessagePumpBlocking( - IAmACommandProcessorProvider commandProcessorProvider, - IAmAMessageMapperRegistry messageMapperRegistry, - IAmAMessageTransformerFactory messageTransformerFactory, - IAmARequestContextFactory requestContextFactory, - IAmAChannel channel, - IAmABrighterTracer? tracer = null, - InstrumentationOptions instrumentationOptions = InstrumentationOptions.All) - : base(commandProcessorProvider, requestContextFactory, tracer, channel, instrumentationOptions) - { - var transformPipelineBuilder = new TransformPipelineBuilder(messageMapperRegistry, messageTransformerFactory); - _unwrapPipeline = transformPipelineBuilder.BuildUnwrapPipeline(); - } - - protected override void DispatchRequest(MessageHeader messageHeader, TRequest request, RequestContext requestContext) - { - s_logger.LogDebug("MessagePump: Dispatching message {Id} from {ChannelName} on thread # {ManagementThreadId}", request.Id, Thread.CurrentThread.ManagedThreadId, Channel.Name); - requestContext.Span?.AddEvent(new ActivityEvent("Dispatch Message")); - - var messageType = messageHeader.MessageType; - - ValidateMessageType(messageType, request); - - switch (messageType) - { - case MessageType.MT_COMMAND: - { - CommandProcessorProvider.Get().Send(request, requestContext); - break; - } - case MessageType.MT_DOCUMENT: - case MessageType.MT_EVENT: - { - CommandProcessorProvider.Get().Publish(request, requestContext); - break; - } - } - } - - protected override TRequest TranslateMessage(Message message, RequestContext requestContext) - { - s_logger.LogDebug("MessagePump: Translate message {Id} on thread # {ManagementThreadId}", message.Id, Thread.CurrentThread.ManagedThreadId); - requestContext.Span?.AddEvent(new ActivityEvent("Translate Message")); - - TRequest request; - - try - { - request = _unwrapPipeline.Unwrap(message, requestContext); - } - catch (ConfigurationException) - { - throw; - } - catch (Exception exception) - { - throw new MessageMappingException($"Failed to map message {message.Id} using pipeline for type {typeof(TRequest).FullName} ", exception); - } - - return request; - } - } -} diff --git a/src/Paramore.Brighter.ServiceActivator/Proactor.cs b/src/Paramore.Brighter.ServiceActivator/Proactor.cs new file mode 100644 index 0000000000..bed6ae32c1 --- /dev/null +++ b/src/Paramore.Brighter.ServiceActivator/Proactor.cs @@ -0,0 +1,414 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2022 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.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Actions; +using Paramore.Brighter.Observability; +using Paramore.Brighter.Tasks; +using Polly.CircuitBreaker; + +namespace Paramore.Brighter.ServiceActivator +{ + /// + /// Used when we don't want to block for I/O, but queue on a completion port and be notified when done + /// See Proactor Pattern + /// + /// The Request on the Data Type Channel + public class Proactor : MessagePump, IAmAMessagePump where TRequest : class, IRequest + { + private readonly UnwrapPipelineAsync _unwrapPipeline; + + /// + /// Constructs a message pump + /// + /// Provides a way to grab a command processor correctly scoped + /// The registry of mappers + /// The factory that lets us create instances of transforms + /// A factory to create instances of request synchronizationHelper, used to add synchronizationHelper to a pipeline + /// The channel to read messages from + /// What is the tracer we will use for telemetry + /// When creating a span for operations how noisy should the attributes be + public Proactor( + IAmACommandProcessorProvider commandProcessorProvider, + IAmAMessageMapperRegistryAsync messageMapperRegistry, + IAmAMessageTransformerFactoryAsync messageTransformerFactory, + IAmARequestContextFactory requestContextFactory, + IAmAChannelAsync channel, + IAmABrighterTracer? tracer = null, + InstrumentationOptions instrumentationOptions = InstrumentationOptions.All) + : base(commandProcessorProvider, requestContextFactory, tracer, instrumentationOptions) + { + var transformPipelineBuilder = new TransformPipelineBuilderAsync(messageMapperRegistry, messageTransformerFactory); + _unwrapPipeline = transformPipelineBuilder.BuildUnwrapPipeline(); + Channel = channel; + } + + /// + /// The channel to receive messages from + /// + public IAmAChannelAsync Channel { get; set; } + + /// + /// The of this message pump; indicates Reactor or Proactor + /// + public override MessagePumpType MessagePumpType => MessagePumpType.Proactor; + + /// + /// Runs the message pump, performing the following: + /// - Gets a message from a queue/stream + /// - Translates the message to the local type system + /// - Dispatches the message to waiting handlers + /// - Handles any exceptions that occur during the dispatch and tries to keep the pump alive + /// + /// + public void Run() + { + //NOTE: Don't make this a method body, as opposed to an expression, unless you want it to + //break deep in AsyncTaskMethodBuilder for some hard to explain reasons + BrighterSynchronizationHelper.Run(async () => await EventLoop()); + } + + private async Task Acknowledge(Message message) + { + s_logger.LogDebug( + "MessagePump: Acknowledge message {Id} read from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", + message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + + await Channel.AcknowledgeAsync(message); + } + + private async Task DispatchRequest(MessageHeader messageHeader, TRequest request, RequestContext requestContext) + { + s_logger.LogDebug("MessagePump: Dispatching message {Id} from {ChannelName} on thread # {ManagementThreadId}", request.Id, Thread.CurrentThread.ManagedThreadId, Channel.Name); + requestContext.Span?.AddEvent(new ActivityEvent("Dispatch Message")); + + var messageType = messageHeader.MessageType; + + ValidateMessageType(messageType, request); + + switch (messageType) + { + case MessageType.MT_COMMAND: + { + await CommandProcessorProvider + .Get() + .SendAsync(request,requestContext, continueOnCapturedContext: true, default); + break; + } + case MessageType.MT_DOCUMENT: + case MessageType.MT_EVENT: + { + await CommandProcessorProvider + .Get() + .PublishAsync(request, requestContext, continueOnCapturedContext: true, default); + break; + } + } + } + + private async Task EventLoop() + { + var pumpSpan = Tracer?.CreateMessagePumpSpan(MessagePumpSpanOperation.Begin, Channel.RoutingKey, MessagingSystem.InternalBus, InstrumentationOptions); + + do + { + if (UnacceptableMessageLimitReached()) + { + Channel.Dispose(); + break; + } + + s_logger.LogDebug("MessagePump: Receiving messages from channel {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + + Activity? span = null; + Message? message = null; + try + { + message = await Channel.ReceiveAsync(TimeOut); + span = Tracer?.CreateSpan(MessagePumpSpanOperation.Receive, message, MessagingSystem.InternalBus, InstrumentationOptions); + } + catch (ChannelFailureException ex) when (ex.InnerException is BrokenCircuitException) + { + s_logger.LogWarning("MessagePump: BrokenCircuitException messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + var errorSpan = Tracer?.CreateMessagePumpExceptionSpan(ex, Channel.RoutingKey, MessagePumpSpanOperation.Receive, MessagingSystem.InternalBus, InstrumentationOptions); + Tracer?.EndSpan(errorSpan); + await Task.Delay(ChannelFailureDelay); + continue; + } + catch (ChannelFailureException ex) + { + s_logger.LogWarning("MessagePump: ChannelFailureException messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + var errorSpan = Tracer?.CreateMessagePumpExceptionSpan(ex, Channel.RoutingKey, MessagePumpSpanOperation.Receive, MessagingSystem.InternalBus, InstrumentationOptions); + Tracer?.EndSpan(errorSpan ); + await Task.Delay(ChannelFailureDelay); + continue; + } + catch (Exception ex) + { + s_logger.LogError(ex, "MessagePump: Exception receiving messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + var errorSpan = Tracer?.CreateMessagePumpExceptionSpan(ex, Channel.RoutingKey, MessagePumpSpanOperation.Receive, MessagingSystem.InternalBus, InstrumentationOptions); + Tracer?.EndSpan(errorSpan ); + } + + if (message is null) + { + Channel.Dispose(); + span?.SetStatus(ActivityStatusCode.Error, "Could not receive message. Note that should return an MT_NONE from an empty queue on timeout"); + Tracer?.EndSpan(span); + throw new Exception("Could not receive message. Note that should return an MT_NONE from an empty queue on timeout"); + } + + // empty queue + if (message.Header.MessageType == MessageType.MT_NONE) + { + span?.SetStatus(ActivityStatusCode.Ok); + Tracer?.EndSpan(span); + await Task.Delay(EmptyChannelDelay); + continue; + } + + // failed to parse a message from the incoming data + if (message.Header.MessageType == MessageType.MT_UNACCEPTABLE) + { + s_logger.LogWarning("MessagePump: Failed to parse a message from the incoming message with id {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + span?.SetStatus(ActivityStatusCode.Error, $"MessagePump: Failed to parse a message from the incoming message with id {message.Id} from {Channel.Name} on thread # {Environment.CurrentManagedThreadId}"); + Tracer?.EndSpan(span); + IncrementUnacceptableMessageLimit(); + await Acknowledge(message); + + continue; + } + + // QUIT command + if (message.Header.MessageType == MessageType.MT_QUIT) + { + s_logger.LogInformation("MessagePump: Quit receiving messages from {ChannelName} on thread #{ManagementThreadId}", Channel.Name, Environment.CurrentManagedThreadId); + span?.SetStatus(ActivityStatusCode.Ok); + Tracer?.EndSpan(span); + Channel.Dispose(); + break; + } + + // Serviceable message + try + { + RequestContext context = InitRequestContext(span, message); + + var request = await TranslateMessage(message, context); + + CommandProcessorProvider.CreateScope(); + + await DispatchRequest(message.Header, request, context); + + span?.SetStatus(ActivityStatusCode.Ok); + } + catch (AggregateException aggregateException) + { + var stop = false; + var defer = false; + + foreach (var exception in aggregateException.InnerExceptions) + { + if (exception is ConfigurationException configurationException) + { + s_logger.LogCritical(configurationException, "MessagePump: Stopping receiving of messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + stop = true; + break; + } + + if (exception is DeferMessageAction) + { + defer = true; + continue; + } + + s_logger.LogError(exception, "MessagePump: Failed to dispatch message {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + } + + if (defer) + { + s_logger.LogDebug("MessagePump: Deferring message {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + span?.SetStatus(ActivityStatusCode.Error, $"Deferring message {message.Id} for later action"); + if (await RequeueMessage(message)) + continue; + } + + if (stop) + { + await RejectMessage(message); + span?.SetStatus(ActivityStatusCode.Error, $"MessagePump: Stopping receiving of messages from {Channel.Name} with {Channel.RoutingKey} on thread # {Environment.CurrentManagedThreadId}"); + Channel.Dispose(); + break; + } + + span?.SetStatus(ActivityStatusCode.Error, $"MessagePump: Failed to dispatch message {message.Id} from {Channel.Name} with {Channel.RoutingKey} on thread # {Environment.CurrentManagedThreadId}"); + } + catch (ConfigurationException configurationException) + { + s_logger.LogCritical(configurationException,"MessagePump: Stopping receiving of messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + await RejectMessage(message); + span?.SetStatus(ActivityStatusCode.Error, $"MessagePump: Stopping receiving of messages from {Channel.Name} on thread # {Environment.CurrentManagedThreadId}"); + Channel.Dispose(); + break; + } + catch (DeferMessageAction) + { + s_logger.LogDebug("MessagePump: Deferring message {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + + span?.SetStatus(ActivityStatusCode.Error, $"Deferring message {message.Id} for later action"); + + if (await RequeueMessage(message)) continue; + } + catch (MessageMappingException messageMappingException) + { + s_logger.LogWarning(messageMappingException, "MessagePump: Failed to map message {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + + IncrementUnacceptableMessageLimit(); + + span?.SetStatus(ActivityStatusCode.Error, $"MessagePump: Failed to map message {message.Id} from {Channel.Name} with {Channel.RoutingKey} on thread # {Thread.CurrentThread.ManagedThreadId}"); + } + catch (Exception e) + { + s_logger.LogError(e, + "MessagePump: Failed to dispatch message '{Id}' from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", + message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + + span?.SetStatus(ActivityStatusCode.Error,$"MessagePump: Failed to dispatch message '{message.Id}' from {Channel.Name} with {Channel.RoutingKey} on thread # {Environment.CurrentManagedThreadId}"); + } + finally + { + Tracer?.EndSpan(span); + CommandProcessorProvider.ReleaseScope(); + } + + await Acknowledge(message); + + } while (true); + + s_logger.LogInformation( + "MessagePump0: Finished running message loop, no longer receiving messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", + Channel.Name, Channel.RoutingKey, Thread.CurrentThread.ManagedThreadId); + Tracer?.EndSpan(pumpSpan); + } + + private async Task TranslateAsync(Message message, RequestContext requestContext, CancellationToken cancellationToken = default) + { + try + { + return await _unwrapPipeline.UnwrapAsync(message, requestContext, cancellationToken); + } + catch (ConfigurationException) + { + throw; + } + catch (Exception exception) + { + throw new MessageMappingException($"Failed to map message {message.Id} using pipeline for type {typeof(TRequest).FullName} ", exception); + } + } + + private async Task TranslateMessage(Message message, RequestContext requestContext) + { + s_logger.LogDebug( + "MessagePump: Translate message {Id} on thread # {ManagementThreadId}", + message.Id, Thread.CurrentThread.ManagedThreadId + ); + requestContext.Span?.AddEvent(new ActivityEvent("Translate Message")); + + return await TranslateAsync(message, requestContext); + } + + private RequestContext InitRequestContext(Activity? span, Message message) + { + var context = RequestContextFactory.Create(); + context.Span = span; + context.OriginatingMessage = message; + context.Bag.AddOrUpdate("ChannelName", Channel.Name, (_, _) => Channel.Name); + context.Bag.AddOrUpdate("RequestStart", DateTime.UtcNow, (_, _) => DateTime.UtcNow); + return context; + } + + private async Task RejectMessage(Message message) + { + s_logger.LogWarning("MessagePump: Rejecting message {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Thread.CurrentThread.ManagedThreadId); + IncrementUnacceptableMessageLimit(); + + await Channel.RejectAsync(message); + } + + private async Task RequeueMessage(Message message) + { + message.Header.UpdateHandledCount(); + + if (DiscardRequeuedMessagesEnabled()) + { + if (message.HandledCountReached(RequeueCount)) + { + var originalMessageId = message.Header.Bag.TryGetValue(Message.OriginalMessageIdHeaderName, out object? value) ? value.ToString() : null; + + s_logger.LogError( + "MessagePump: Have tried {RequeueCount} times to handle this message {Id}{OriginalMessageId} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}, dropping message.", + RequeueCount, + message.Id, + string.IsNullOrEmpty(originalMessageId) + ? string.Empty + : $" (original message id {originalMessageId})", + Channel.Name, + Channel.RoutingKey, + Thread.CurrentThread.ManagedThreadId); + + await RejectMessage(message); + return false; + } + } + + s_logger.LogDebug( + "MessagePump: Re-queueing message {Id} from {ManagementThreadId} on thread # {ChannelName} with {RoutingKey}", message.Id, + Channel.Name, Channel.RoutingKey, Thread.CurrentThread.ManagedThreadId); + + return await Channel.RequeueAsync(message, RequeueDelay); + } + + private bool UnacceptableMessageLimitReached() + { + if (UnacceptableMessageLimit == 0) return false; + if (UnacceptableMessageCount < UnacceptableMessageLimit) return false; + + s_logger.LogCritical( + "MessagePump: Unacceptable message limit of {UnacceptableMessageLimit} reached, stopping reading messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", + UnacceptableMessageLimit, + Channel.Name, + Channel.RoutingKey, + Environment.CurrentManagedThreadId + ); + + return true; + } + + } +} diff --git a/src/Paramore.Brighter.ServiceActivator/Reactor.cs b/src/Paramore.Brighter.ServiceActivator/Reactor.cs new file mode 100644 index 0000000000..2275b6c766 --- /dev/null +++ b/src/Paramore.Brighter.ServiceActivator/Reactor.cs @@ -0,0 +1,403 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2022 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.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Actions; +using Paramore.Brighter.Observability; +using Polly.CircuitBreaker; + +namespace Paramore.Brighter.ServiceActivator +{ + /// + /// Used when the message pump should block for I/O + /// Will guarantee strict ordering of the messages on the queue + /// Predictable performance as only one thread, allows you to configure number of performers for number of threads to use + /// Lower throughput than async + /// See Reactor Pattern for more on this approach + /// + /// + public class Reactor : MessagePump, IAmAMessagePump where TRequest : class, IRequest + { + private readonly UnwrapPipeline _unwrapPipeline; + + /// + /// Constructs a message pump + /// + /// Provides a way to grab a command processor correctly scoped + /// The registry of mappers + /// The factory that lets us create instances of transforms + /// A factory to create instances of request synchronizationHelper, used to add synchronizationHelper to a pipeline + /// The channel from which to read messages + /// What is the tracer we will use for telemetry + /// When creating a span for operations how noisy should the attributes be + public Reactor( + IAmACommandProcessorProvider commandProcessorProvider, + IAmAMessageMapperRegistry messageMapperRegistry, + IAmAMessageTransformerFactory messageTransformerFactory, + IAmARequestContextFactory requestContextFactory, + IAmAChannelSync channel, + IAmABrighterTracer? tracer = null, + InstrumentationOptions instrumentationOptions = InstrumentationOptions.All) + : base(commandProcessorProvider, requestContextFactory, tracer, instrumentationOptions) + { + var transformPipelineBuilder = new TransformPipelineBuilder(messageMapperRegistry, messageTransformerFactory); + _unwrapPipeline = transformPipelineBuilder.BuildUnwrapPipeline(); + Channel = channel; + } + + /// + /// The channel to receive messages from + /// + public IAmAChannelSync Channel { get; set; } + + /// + /// The of this message pump; indicates Reactor or Proactor + /// + public override MessagePumpType MessagePumpType => MessagePumpType.Reactor; + + /// + /// Runs the message pump, performing the following: + /// - Gets a message from a queue/stream + /// - Translates the message to the local type system + /// - Dispatches the message to waiting handlers + /// - Handles any exceptions that occur during the dispatch and tries to keep the pump alive + /// + /// + public void Run() + { + var pumpSpan = Tracer?.CreateMessagePumpSpan(MessagePumpSpanOperation.Begin, Channel.RoutingKey, MessagingSystem.InternalBus, InstrumentationOptions); + do + { + if (UnacceptableMessageLimitReached()) + { + Channel.Dispose(); + break; + } + + s_logger.LogDebug("MessagePump: Receiving messages from channel {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + + Activity? span = null; + Message? message = null; + try + { + message = Channel.Receive(TimeOut); + span = Tracer?.CreateSpan(MessagePumpSpanOperation.Receive, message, MessagingSystem.InternalBus, InstrumentationOptions); + } + catch (ChannelFailureException ex) when (ex.InnerException is BrokenCircuitException) + { + s_logger.LogWarning("MessagePump: BrokenCircuitException messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + var errorSpan = Tracer?.CreateMessagePumpExceptionSpan(ex, Channel.RoutingKey, MessagePumpSpanOperation.Receive, MessagingSystem.InternalBus, InstrumentationOptions); + Tracer?.EndSpan(errorSpan); + Task.Delay(ChannelFailureDelay).GetAwaiter().GetResult(); //-- pause pump; blocks consuming thread on empty queue; + continue; + } + catch (ChannelFailureException ex) + { + s_logger.LogWarning("MessagePump: ChannelFailureException messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + var errorSpan = Tracer?.CreateMessagePumpExceptionSpan(ex, Channel.RoutingKey, MessagePumpSpanOperation.Receive, MessagingSystem.InternalBus, InstrumentationOptions); + Tracer?.EndSpan(errorSpan ); + Task.Delay(ChannelFailureDelay).GetAwaiter().GetResult(); //-- pause pump; blocks consuming thread on empty queue; + continue; + } + catch (Exception ex) + { + s_logger.LogError(ex, "MessagePump: Exception receiving messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + var errorSpan = Tracer?.CreateMessagePumpExceptionSpan(ex, Channel.RoutingKey, MessagePumpSpanOperation.Receive, MessagingSystem.InternalBus, InstrumentationOptions); + Tracer?.EndSpan(errorSpan ); + } + + if (message is null) + { + Channel.Dispose(); + span?.SetStatus(ActivityStatusCode.Error, "Could not receive message. Note that should return an MT_NONE from an empty queue on timeout"); + Tracer?.EndSpan(span); + throw new Exception("Could not receive message. Note that should return an MT_NONE from an empty queue on timeout"); + } + + // empty queue + if (message.Header.MessageType == MessageType.MT_NONE) + { + span?.SetStatus(ActivityStatusCode.Ok); + Tracer?.EndSpan(span); + Task.Delay(EmptyChannelDelay).GetAwaiter().GetResult(); //-- pause pump; blocks consuming thread on empty queue; + continue; + } + + // failed to parse a message from the incoming data + if (message.Header.MessageType == MessageType.MT_UNACCEPTABLE) + { + s_logger.LogWarning("MessagePump: Failed to parse a message from the incoming message with id {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + span?.SetStatus(ActivityStatusCode.Error, $"MessagePump: Failed to parse a message from the incoming message with id {message.Id} from {Channel.Name} on thread # {Environment.CurrentManagedThreadId}"); + Tracer?.EndSpan(span); + IncrementUnacceptableMessageLimit(); + AcknowledgeMessage(message); + + continue; + } + + // QUIT command + if (message.Header.MessageType == MessageType.MT_QUIT) + { + s_logger.LogInformation("MessagePump: Quit receiving messages from {ChannelName} on thread #{ManagementThreadId}", Channel.Name, Environment.CurrentManagedThreadId); + span?.SetStatus(ActivityStatusCode.Ok); + Tracer?.EndSpan(span); + Channel.Dispose(); + break; + } + + // Serviceable message + try + { + RequestContext context = InitRequestContext(span, message); + + var request = TranslateMessage(message, context); + + CommandProcessorProvider.CreateScope(); + + DispatchRequest(message.Header, request, context); + + span?.SetStatus(ActivityStatusCode.Ok); + } + catch (AggregateException aggregateException) + { + var stop = false; + var defer = false; + + foreach (var exception in aggregateException.InnerExceptions) + { + if (exception is ConfigurationException configurationException) + { + s_logger.LogCritical(configurationException, "MessagePump: Stopping receiving of messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + stop = true; + break; + } + + if (exception is DeferMessageAction) + { + defer = true; + continue; + } + + s_logger.LogError(exception, "MessagePump: Failed to dispatch message {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + } + + if (defer) + { + s_logger.LogDebug("MessagePump: Deferring message {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + span?.SetStatus(ActivityStatusCode.Error, $"Deferring message {message.Id} for later action"); + if (RequeueMessage(message)) + continue; + } + + if (stop) + { + RejectMessage(message); + span?.SetStatus(ActivityStatusCode.Error, $"MessagePump: Stopping receiving of messages from {Channel.Name} with {Channel.RoutingKey} on thread # {Environment.CurrentManagedThreadId}"); + Channel.Dispose(); + break; + } + + span?.SetStatus(ActivityStatusCode.Error, $"MessagePump: Failed to dispatch message {message.Id} from {Channel.Name} with {Channel.RoutingKey} on thread # {Environment.CurrentManagedThreadId}"); + } + catch (ConfigurationException configurationException) + { + s_logger.LogCritical(configurationException,"MessagePump: Stopping receiving of messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + RejectMessage(message); + span?.SetStatus(ActivityStatusCode.Error, $"MessagePump: Stopping receiving of messages from {Channel.Name} on thread # {Environment.CurrentManagedThreadId}"); + Channel.Dispose(); + break; + } + catch (DeferMessageAction) + { + s_logger.LogDebug("MessagePump: Deferring message {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + + span?.SetStatus(ActivityStatusCode.Error, $"Deferring message {message.Id} for later action"); + + if (RequeueMessage(message)) continue; + } + catch (MessageMappingException messageMappingException) + { + s_logger.LogWarning(messageMappingException, "MessagePump: Failed to map message {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + + IncrementUnacceptableMessageLimit(); + + span?.SetStatus(ActivityStatusCode.Error, $"MessagePump: Failed to map message {message.Id} from {Channel.Name} with {Channel.RoutingKey} on thread # {Thread.CurrentThread.ManagedThreadId}"); + } + catch (Exception e) + { + s_logger.LogError(e, + "MessagePump: Failed to dispatch message '{Id}' from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", + message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + + span?.SetStatus(ActivityStatusCode.Error,$"MessagePump: Failed to dispatch message '{message.Id}' from {Channel.Name} with {Channel.RoutingKey} on thread # {Environment.CurrentManagedThreadId}"); + } + finally + { + Tracer?.EndSpan(span); + CommandProcessorProvider.ReleaseScope(); + } + + AcknowledgeMessage(message); + + } while (true); + + s_logger.LogInformation( + "MessagePump0: Finished running message loop, no longer receiving messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", + Channel.Name, Channel.RoutingKey, Thread.CurrentThread.ManagedThreadId); + Tracer?.EndSpan(pumpSpan); + + } + + private void AcknowledgeMessage(Message message) + { + s_logger.LogDebug( + "MessagePump: Acknowledge message {Id} read from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", + message.Id, Channel.Name, Channel.RoutingKey, Environment.CurrentManagedThreadId); + + Channel.Acknowledge(message); + } + + private void DispatchRequest(MessageHeader messageHeader, TRequest request, RequestContext requestContext) + { + s_logger.LogDebug("MessagePump: Dispatching message {Id} from {ChannelName} on thread # {ManagementThreadId}", request.Id, Thread.CurrentThread.ManagedThreadId, Channel.Name); + requestContext.Span?.AddEvent(new ActivityEvent("Dispatch Message")); + + var messageType = messageHeader.MessageType; + + ValidateMessageType(messageType, request); + + switch (messageType) + { + case MessageType.MT_COMMAND: + { + CommandProcessorProvider.Get().Send(request, requestContext); + break; + } + case MessageType.MT_DOCUMENT: + case MessageType.MT_EVENT: + { + CommandProcessorProvider.Get().Publish(request, requestContext); + break; + } + } + } + + private RequestContext InitRequestContext(Activity? span, Message message) + { + var context = RequestContextFactory.Create(); + context.Span = span; + context.OriginatingMessage = message; + context.Bag.AddOrUpdate("ChannelName", Channel.Name, (_, _) => Channel.Name); + context.Bag.AddOrUpdate("RequestStart", DateTime.UtcNow, (_, _) => DateTime.UtcNow); + return context; + } + + private void RejectMessage(Message message) + { + s_logger.LogWarning("MessagePump: Rejecting message {Id} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", message.Id, Channel.Name, Channel.RoutingKey, Thread.CurrentThread.ManagedThreadId); + IncrementUnacceptableMessageLimit(); + + Channel.Reject(message); + } + + private bool RequeueMessage(Message message) + { + message.Header.UpdateHandledCount(); + + if (DiscardRequeuedMessagesEnabled()) + { + if (message.HandledCountReached(RequeueCount)) + { + var originalMessageId = message.Header.Bag.TryGetValue(Message.OriginalMessageIdHeaderName, out object? value) ? value.ToString() : null; + + s_logger.LogError( + "MessagePump: Have tried {RequeueCount} times to handle this message {Id}{OriginalMessageId} from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}, dropping message.", + RequeueCount, + message.Id, + string.IsNullOrEmpty(originalMessageId) + ? string.Empty + : $" (original message id {originalMessageId})", + Channel.Name, + Channel.RoutingKey, + Thread.CurrentThread.ManagedThreadId); + + RejectMessage(message); + return false; + } + } + + s_logger.LogDebug( + "MessagePump: Re-queueing message {Id} from {ManagementThreadId} on thread # {ChannelName} with {RoutingKey}", message.Id, + Channel.Name, Channel.RoutingKey, Thread.CurrentThread.ManagedThreadId); + + return Channel.Requeue(message, RequeueDelay); + } + + private TRequest TranslateMessage(Message message, RequestContext requestContext) + { + s_logger.LogDebug("MessagePump: Translate message {Id} on thread # {ManagementThreadId}", message.Id, Thread.CurrentThread.ManagedThreadId); + requestContext.Span?.AddEvent(new ActivityEvent("Translate Message")); + + TRequest request; + + try + { + request = _unwrapPipeline.Unwrap(message, requestContext); + } + catch (ConfigurationException) + { + throw; + } + catch (Exception exception) + { + throw new MessageMappingException($"Failed to map message {message.Id} using pipeline for type {typeof(TRequest).FullName} ", exception); + } + + return request; + } + + private bool UnacceptableMessageLimitReached() + { + if (UnacceptableMessageLimit == 0) return false; + + if (UnacceptableMessageCount >= UnacceptableMessageLimit) + { + s_logger.LogCritical( + "MessagePump: Unacceptable message limit of {UnacceptableMessageLimit} reached, stopping reading messages from {ChannelName} with {RoutingKey} on thread # {ManagementThreadId}", + UnacceptableMessageLimit, + Channel.Name, + Channel.RoutingKey, + Environment.CurrentManagedThreadId + ); + + return true; + } + return false; + } + } +} diff --git a/src/Paramore.Brighter.Tranformers.AWS/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Tranformers.AWS/ServiceCollectionExtensions.cs index 9b88a94e38..6d3c78f67c 100644 --- a/src/Paramore.Brighter.Tranformers.AWS/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Tranformers.AWS/ServiceCollectionExtensions.cs @@ -34,6 +34,7 @@ public static class ServiceCollectionExtensions { /// /// Will add an S3 Luggage Store into the Service Collection + /// Sync over async, but alright as we are in startup /// /// /// diff --git a/src/Paramore.Brighter/Channel.cs b/src/Paramore.Brighter/Channel.cs index f284227c9d..823b8f5029 100644 --- a/src/Paramore.Brighter/Channel.cs +++ b/src/Paramore.Brighter/Channel.cs @@ -31,13 +31,13 @@ namespace Paramore.Brighter { /// /// Class Channel. - /// An for reading messages from a + /// An for reading messages from a /// Task Queue /// and acknowledging receipt of those messages /// - public class Channel : IAmAChannel + public class Channel : IAmAChannelSync { - private readonly IAmAMessageConsumer _messageConsumer; + private readonly IAmAMessageConsumerSync _messageConsumer; private ConcurrentQueue _queue = new(); private readonly int _maxQueueLength; private static readonly Message s_noneMessage = new(); @@ -66,7 +66,7 @@ public class Channel : IAmAChannel public Channel( ChannelName channelName, RoutingKey routingKey, - IAmAMessageConsumer messageConsumer, + IAmAMessageConsumerSync messageConsumer, int maxQueueLength = 1 ) { diff --git a/src/Paramore.Brighter/ChannelAsync.cs b/src/Paramore.Brighter/ChannelAsync.cs new file mode 100644 index 0000000000..2a5860f5b0 --- /dev/null +++ b/src/Paramore.Brighter/ChannelAsync.cs @@ -0,0 +1,203 @@ +#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.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using Paramore.Brighter.Extensions; + +namespace Paramore.Brighter +{ + /// + /// Class Channel. + /// An for reading messages from a + /// Task Queue + /// and acknowledging receipt of those messages + /// + public class ChannelAsync : IAmAChannelAsync + { + private readonly IAmAMessageConsumerAsync _messageConsumer; + private ConcurrentQueue _queue = new(); + private readonly int _maxQueueLength; + private static readonly Message s_noneMessage = new(); + + /// + /// The name of a channel is its identifier + /// See Topic for the broker routing key + /// May be used for the queue name, if known, on middleware that supports named queues + /// + /// The channel identifier + public ChannelName Name { get; } + + /// + /// The topic that this channel is for (how a broker routes to it) + /// + /// The topic on the broker + public RoutingKey RoutingKey { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Name of the queue. + /// + /// The messageConsumer. + /// What is the maximum buffer size we will accept + public ChannelAsync( + ChannelName channelName, + RoutingKey routingKey, + IAmAMessageConsumerAsync messageConsumer, + int maxQueueLength = 1 + ) + { + Name = channelName; + RoutingKey = routingKey; + _messageConsumer = messageConsumer; + + if (maxQueueLength < 1 || maxQueueLength > 10) + { + throw new ConfigurationException( + "The channel buffer must have one item, and cannot have more than 10"); + } + + _maxQueueLength = maxQueueLength + 1; //+1 so you can fit the quit message on the queue as well + } + + /// + /// Acknowledges the specified message. + /// + /// The message. + /// Cancels the acknowledge operation + public virtual async Task AcknowledgeAsync(Message message, CancellationToken cancellationToken = default) + { + await _messageConsumer.AcknowledgeAsync(message, cancellationToken); + } + + /// + /// Inserts messages into the channel for consumption by the message pump. + /// Note that there is an upperbound to what we can enqueue, although we always allow enqueueing a quit + /// message. We will always try to clear the channel, when closing, as the stop message will get inserted + /// after the queue + /// + /// The messages to insert into the channel + public virtual void Enqueue(params Message[] messages) + { + var currentLength = _queue.Count; + var messagesToAdd = messages.Length; + var newLength = currentLength + messagesToAdd; + + if (newLength > _maxQueueLength) + { + throw new InvalidOperationException($"You cannot enqueue {newLength} items which larger than the buffer length {_maxQueueLength}"); + } + + messages.Each((message) => _queue.Enqueue(message)); + } + + /// + /// Purges the queue + /// + /// Cancels the acknowledge operation + public virtual async Task PurgeAsync(CancellationToken cancellationToken = default) + { + await _messageConsumer.PurgeAsync(cancellationToken); + _queue = new ConcurrentQueue(); + } + + /// + /// The timeout to recieve wihtin. + /// + /// The "> timeout. If null default to 1s + /// Cancel the receive operation + /// Message. + public virtual async Task ReceiveAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + timeout ??= TimeSpan.FromSeconds(1); + + if (!_queue.TryDequeue(out Message? message)) + { + Enqueue(await _messageConsumer.ReceiveAsync(timeout, cancellationToken)); + if (!_queue.TryDequeue(out message)) + { + message = s_noneMessage; //Will be MT_NONE + } + } + + return message; + } + + /// + /// Rejects the specified message. + /// + /// The message. + /// Cancel the rekect operation + public virtual async Task RejectAsync(Message message, CancellationToken cancellationToken = default) + { + await _messageConsumer.RejectAsync(message, cancellationToken); + } + + /// + /// Requeues the specified message. + /// + /// + /// How long should we delay before requeueing + /// Cancels the requeue operation + /// True if the message was re-queued false otherwise + public virtual async Task RequeueAsync(Message message, TimeSpan? timeOut = null, CancellationToken cancellationToken = default) + { + return await _messageConsumer.RequeueAsync(message, timeOut, cancellationToken); + } + + /// + /// Stops this instance. + /// + public virtual void Stop(RoutingKey topic) + { + _queue.Enqueue(MessageFactory.CreateQuitMessage(topic)); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public virtual void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + _messageConsumer.DisposeAsync(); + } + } + + ~ChannelAsync() + { + Dispose(false); + } + } +} diff --git a/src/Paramore.Brighter/CombinedProducerRegistryFactory.cs b/src/Paramore.Brighter/CombinedProducerRegistryFactory.cs index 7e1791a76d..11c9e60345 100644 --- a/src/Paramore.Brighter/CombinedProducerRegistryFactory.cs +++ b/src/Paramore.Brighter/CombinedProducerRegistryFactory.cs @@ -22,34 +22,59 @@ THE SOFTWARE. */ #endregion using System.Linq; +using System.Threading; +using System.Threading.Tasks; -namespace Paramore.Brighter +namespace Paramore.Brighter; + +public class CombinedProducerRegistryFactory : IAmAProducerRegistryFactory { - public class CombinedProducerRegistryFactory : IAmAProducerRegistryFactory + private readonly IAmAMessageProducerFactory[] _messageProducerFactories; + + /// + /// Creates a combined producer registry of the message producers created by a set of message + /// producer factories. + /// + /// The set of message producer factories from which to create the combined registry + public CombinedProducerRegistryFactory(params IAmAMessageProducerFactory[] messageProducerFactories) + { + _messageProducerFactories = messageProducerFactories; + } + + /// + /// Create a combined producer registry of the producers created by the message producer factories, + /// under the key of each topic + /// + /// + /// The async producers block on async calls to create the producers, so this method is synchronous. + /// Generally the async calls are where creation of a producer needs to interrogate a remote service + /// + /// A registry of producers + public IAmAProducerRegistry Create() + { + var producers = _messageProducerFactories + .SelectMany(x => x.Create()) + .ToDictionary(x => x.Key, x => x.Value); + + return new ProducerRegistry(producers); + } + + /// + /// Create a combined producer registry of the producers created by the message producer factories, + /// under the key of each topic + /// + /// + /// The async producers block on async calls to create the producers, so this method is synchronous. + /// Generally the async calls are where creation of a producer needs to interrogate a remote service + /// + /// A registry of producers + public async Task CreateAsync(CancellationToken ct = default) { - private readonly IAmAMessageProducerFactory[] _messageProducerFactories; - - /// - /// Creates a combined producer registry of the message producers created by a set of message - /// producer factories. - /// - /// The set of message producer factories from which to create the combined registry - public CombinedProducerRegistryFactory(params IAmAMessageProducerFactory[] messageProducerFactories) - { - _messageProducerFactories = messageProducerFactories; - } - - /// - /// Create a combined producer registry of the producers created by the message producer factories, - /// under the key of each topic - /// - /// - public IAmAProducerRegistry Create() - { - var producers = _messageProducerFactories - .SelectMany(x => x.Create()) - .ToDictionary(x => x.Key, x => x.Value); - return new ProducerRegistry(producers); - } + var keyedProducerTasks = _messageProducerFactories.Select(x => x.CreateAsync()); + var keyedProducers = await Task.WhenAll(keyedProducerTasks); + var asyncProducers = keyedProducers.SelectMany(x => x) + .ToDictionary(x => x.Key, x => x.Value); + + return new ProducerRegistry(asyncProducers); } } diff --git a/src/Paramore.Brighter/CommandProcessor.cs b/src/Paramore.Brighter/CommandProcessor.cs index b8225cb0e5..9cee3135d1 100644 --- a/src/Paramore.Brighter/CommandProcessor.cs +++ b/src/Paramore.Brighter/CommandProcessor.cs @@ -101,7 +101,7 @@ public class CommandProcessor : IAmACommandProcessor /// Bus: We want to hold a reference to the bus; use double lock to let us pass parameters to the constructor from the first instance /// MethodCache: Used to reduce the cost of reflection for bulk calls /// - private static IAmAnOutboxProducerMediator? s_outboxProducerMediator; + private static IAmAnOutboxProducerMediator? s_mediator; private static readonly object s_padlock = new(); private static readonly ConcurrentDictionary s_boundDepositCalls = new(); @@ -190,7 +190,7 @@ public CommandProcessor( /// /// The request context factory. /// The policy registry. - /// The external service bus that we want to send messages over + /// The external service bus that we want to send messages over /// The feature switch config provider. /// Do we want to insert an inbox handler into pipelines without the attribute. Null (default = no), yes = how to configure /// The Subscriptions for creating the reply queues @@ -199,7 +199,7 @@ public CommandProcessor( public CommandProcessor( IAmARequestContextFactory requestContextFactory, IPolicyRegistry policyRegistry, - IAmAnOutboxProducerMediator bus, + IAmAnOutboxProducerMediator mediator, IAmAFeatureSwitchRegistry? featureSwitchRegistry = null, InboxConfiguration? inboxConfiguration = null, IEnumerable? replySubscriptions = null, @@ -214,7 +214,7 @@ public CommandProcessor( _tracer = tracer; _instrumentationOptions = instrumentationOptions; - InitExtServiceBus(bus); + InitExtServiceBus(mediator); } /// @@ -562,14 +562,14 @@ public string DepositPost( try { - Message message = s_outboxProducerMediator!.CreateMessageFromRequest(request, context); + Message message = s_mediator!.CreateMessageFromRequest(request, context); - var bus = ((IAmAnOutboxProducerMediator)s_outboxProducerMediator); + var mediator = ((IAmAnOutboxProducerMediator)s_mediator); - if (!bus.HasOutbox()) + if (!mediator.HasOutbox()) throw new InvalidOperationException("No outbox defined."); - bus.AddToOutbox(message, context, transactionProvider, batchId); + mediator.AddToOutbox(message, context, transactionProvider, batchId); return message.Id; } @@ -632,7 +632,7 @@ public string[] DepositPost( { var successfullySentMessage = new List(); - var mediator = (IAmAnOutboxProducerMediator)s_outboxProducerMediator!; + var mediator = (IAmAnOutboxProducerMediator)s_mediator!; var batchId = mediator.StartBatchAddToOutbox(); @@ -758,14 +758,14 @@ public async Task DepositPostAsync( try { - Message message = await s_outboxProducerMediator!.CreateMessageFromRequestAsync(request, context, cancellationToken); + Message message = await s_mediator!.CreateMessageFromRequestAsync(request, context, cancellationToken); - var bus = ((IAmAnOutboxProducerMediator)s_outboxProducerMediator); + var mediator = ((IAmAnOutboxProducerMediator)s_mediator); - if (!bus.HasAsyncOutbox()) + if (!mediator.HasAsyncOutbox()) throw new InvalidOperationException("No async outbox defined."); - await bus.AddToOutboxAsync(message, context, transactionProvider, continueOnCapturedContext, + await mediator.AddToOutboxAsync(message, context, transactionProvider, continueOnCapturedContext, cancellationToken, batchId); return message.Id; @@ -843,9 +843,9 @@ public async Task DepositPostAsync( { var successfullySentMessage = new List(); - var bus = (IAmAnOutboxProducerMediator)s_outboxProducerMediator!; + var mediator = (IAmAnOutboxProducerMediator)s_mediator!; - var batchId = bus.StartBatchAddToOutbox(); + var batchId = mediator.StartBatchAddToOutbox(); foreach (var request in requests) { @@ -856,7 +856,7 @@ public async Task DepositPostAsync( successfullySentMessage.Add(messageId); context.Span = createSpan; } - await bus.EndBatchAddToOutboxAsync(batchId, transactionProvider, context, cancellationToken); + await mediator.EndBatchAddToOutboxAsync(batchId, transactionProvider, context, cancellationToken); return successfullySentMessage.ToArray(); } @@ -919,7 +919,7 @@ public void ClearOutbox(string[] ids, RequestContext? requestContext = null, Dic try { - s_outboxProducerMediator!.ClearOutbox(ids, context, args); + s_mediator!.ClearOutbox(ids, context, args); } catch (Exception e) { @@ -953,7 +953,7 @@ public async Task ClearOutboxAsync( try { - await s_outboxProducerMediator!.ClearOutboxAsync(posts, context, continueOnCapturedContext, args, cancellationToken); + await s_mediator!.ClearOutboxAsync(posts, context, continueOnCapturedContext, args, cancellationToken); } catch (Exception e) { @@ -990,7 +990,7 @@ public void ClearOutstandingFromOutbox( try { var minAge = minimumAge ?? TimeSpan.FromMilliseconds(5000); - s_outboxProducerMediator!.ClearOutstandingFromOutbox(amountToClear, minAge, useBulk, context, args); + s_mediator!.ClearOutstandingFromOutbox(amountToClear, minAge, useBulk, context, args); } catch (Exception e) { @@ -1040,7 +1040,7 @@ public void ClearOutstandingFromOutbox( subscription.ChannelName = new ChannelName(channelName.ToString()); subscription.RoutingKey = new RoutingKey(routingKey); - using var responseChannel = _responseChannelFactory.CreateChannel(subscription); + using var responseChannel = _responseChannelFactory.CreateSyncChannel(subscription); s_logger.LogInformation("Create reply queue for topic {ChannelName}", channelName); request.ReplyAddress.Topic = subscription.RoutingKey; request.ReplyAddress.CorrelationId = channelName.ToString(); @@ -1055,11 +1055,11 @@ public void ClearOutstandingFromOutbox( try { - var outMessage = s_outboxProducerMediator!.CreateMessageFromRequest(request, context); + var outMessage = s_mediator!.CreateMessageFromRequest(request, context); //We don't store the message, if we continue to fail further retry is left to the sender s_logger.LogDebug("Sending request with routingkey {ChannelName}", channelName); - s_outboxProducerMediator.CallViaExternalBus(outMessage, requestContext); + s_mediator.CallViaExternalBus(outMessage, requestContext); Message? responseMessage = null; @@ -1071,7 +1071,7 @@ public void ClearOutstandingFromOutbox( { s_logger.LogDebug("Reply received from {ChannelName}", channelName); //map to request is map to a response, but it is a request from consumer point of view. Confusing, but... - s_outboxProducerMediator.CreateRequestFromMessage(responseMessage, context, out TResponse response); + s_mediator.CreateRequestFromMessage(responseMessage, context, out TResponse response); Send(response); return response; @@ -1099,12 +1099,12 @@ public void ClearOutstandingFromOutbox( /// public static void ClearServiceBus() { - if (s_outboxProducerMediator != null) + if (s_mediator != null) { lock (s_padlock) { - s_outboxProducerMediator.Dispose(); - s_outboxProducerMediator = null; + s_mediator.Dispose(); + s_mediator = null; } } s_boundDepositCalls.Clear(); @@ -1145,11 +1145,11 @@ private bool HandlerFactoryIsNotEitherIAmAHandlerFactorySyncOrAsync(IAmAHandlerF // if needed as a "get out of gaol" card. private static void InitExtServiceBus(IAmAnOutboxProducerMediator bus) { - if (s_outboxProducerMediator == null) + if (s_mediator == null) { lock (s_padlock) { - s_outboxProducerMediator ??= bus; + s_mediator ??= bus; } } } diff --git a/src/Paramore.Brighter/ExternalBusConfiguration.cs b/src/Paramore.Brighter/ExternalBusConfiguration.cs index 18efd36ae4..d46741b972 100644 --- a/src/Paramore.Brighter/ExternalBusConfiguration.cs +++ b/src/Paramore.Brighter/ExternalBusConfiguration.cs @@ -256,7 +256,6 @@ public class ExternalBusConfiguration : IAmExternalBusConfiguration public ExternalBusConfiguration() { /*allows setting of properties one-by-one, we default the required values here*/ - ProducerRegistry = new ProducerRegistry(new Dictionary()); } diff --git a/src/Paramore.Brighter/IAmABulkMessageProducerAsync.cs b/src/Paramore.Brighter/IAmABulkMessageProducerAsync.cs index 0d60c86a6d..9e6ca06e3b 100644 --- a/src/Paramore.Brighter/IAmABulkMessageProducerAsync.cs +++ b/src/Paramore.Brighter/IAmABulkMessageProducerAsync.cs @@ -33,7 +33,7 @@ namespace Paramore.Brighter /// /// Interface IAmABulkMessageProducerAsync /// Abstracts away the Application Layer used to push messages with async/await support onto a Task Queue - /// Usually clients do not need to instantiate as access is via an derived class. + /// Usually clients do not need to instantiate as access is via an derived class. /// We provide the following default gateway applications /// /// AMQP diff --git a/src/Paramore.Brighter/IAmAChannel.cs b/src/Paramore.Brighter/IAmAChannel.cs index b0af1e4afe..49587a9cb8 100644 --- a/src/Paramore.Brighter/IAmAChannel.cs +++ b/src/Paramore.Brighter/IAmAChannel.cs @@ -1,4 +1,4 @@ -#region Licence +#region Licence /* The MIT License (MIT) Copyright © 2024 Ian Cooper @@ -24,70 +24,31 @@ THE SOFTWARE. */ using System; -namespace Paramore.Brighter +namespace Paramore.Brighter; + +public interface IAmAChannel : IDisposable { /// - /// Interface IAmAChannel - /// An for reading messages from a Task Queue - /// and acknowledging receipt of those messages + /// Gets the name. /// - public interface IAmAChannel : IDisposable - { - /// - /// Gets the name. - /// - /// The name. - ChannelName Name { get; } - - /// - /// The topic that this channel is for (how a broker routes to it) - /// - /// The topic on the broker - RoutingKey RoutingKey { get; } - - /// - /// Acknowledges the specified message. - /// - /// The message. - void Acknowledge(Message message); + /// The name. + ChannelName Name { get; } - /// - /// Clears the queue - /// - void Purge(); - - /// - /// The timeout for the channel to receive a message. - /// - /// The timeout; if null default to 1 second - /// Message. - Message Receive(TimeSpan? timeout); - - /// - /// Rejects the specified message. - /// - /// The message. - void Reject(Message message); - - /// - /// Stops this instance. - /// The topic to post the MT_QUIT message too - /// - void Stop(RoutingKey topic); - - /// - /// Adds a message to the queue - /// - /// - void Enqueue(params Message[] message); + /// + /// The topic that this channel is for (how a broker routes to it) + /// + /// The topic on the broker + RoutingKey RoutingKey { get; } - /// - /// Requeues the specified message. - /// - /// The message. - /// The delay to the delivery of the message. - /// True if the message should be Acked, false otherwise - bool Requeue(Message message, TimeSpan? timeOut = null); + /// + /// Stops this instance. + /// The topic to post the MT_QUIT message too + /// + void Stop(RoutingKey topic); - } + /// + /// Adds a message to the queue + /// + /// + void Enqueue(params Message[] message); } diff --git a/src/Paramore.Brighter/IAmAChannelAsync.cs b/src/Paramore.Brighter/IAmAChannelAsync.cs new file mode 100644 index 0000000000..4570f6d415 --- /dev/null +++ b/src/Paramore.Brighter/IAmAChannelAsync.cs @@ -0,0 +1,75 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 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; +using System.Threading.Tasks; + +namespace Paramore.Brighter +{ + /// + /// Interface IAmAChannelSync + /// An for reading messages from a Task Queue + /// and acknowledging receipt of those messages + /// + public interface IAmAChannelAsync : IAmAChannel + { + /// + /// Acknowledges the specified message. + /// + /// The message. + /// Cancels the acknowledge + Task AcknowledgeAsync(Message message, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Clears the queue + /// + Task PurgeAsync(CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// The timeout for the channel to receive a message. + /// + /// The timeout; if null default to 1 second + /// Cancels the receive + /// Message. + Task ReceiveAsync(TimeSpan? timeout, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Rejects the specified message. + /// + /// The message. + /// Cancels the reject + Task RejectAsync(Message message, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Requeues the specified message. + /// + /// The message. + /// The delay to the delivery of the message. + /// Cancels the requeue + /// True if the message should be Acked, false otherwise + Task RequeueAsync(Message message, TimeSpan? timeOut = null, CancellationToken cancellationToken = default(CancellationToken)); + + } +} diff --git a/src/Paramore.Brighter/IAmAChannelFactory.cs b/src/Paramore.Brighter/IAmAChannelFactory.cs index 577a690e99..32ea0a3da2 100644 --- a/src/Paramore.Brighter/IAmAChannelFactory.cs +++ b/src/Paramore.Brighter/IAmAChannelFactory.cs @@ -22,11 +22,14 @@ THE SOFTWARE. */ #endregion +using System.Threading; +using System.Threading.Tasks; + namespace Paramore.Brighter { /// /// Interface IAmAChannelFactory - /// Creates instances of channels. We provide support for some Application Layer channels, and provide factories for those: + /// Creates instances of channels. We provide support for some Application Layer channels, and provide factories for those: /// /// AMQP /// RestML @@ -39,7 +42,23 @@ public interface IAmAChannelFactory /// Creates the input channel. /// /// The parameters with which to create the channel for the transport - /// IAmAnInputChannel. - IAmAChannel CreateChannel(Subscription subscription); + /// An instance of . + IAmAChannelSync CreateSyncChannel(Subscription subscription); + + /// + /// Creates the input channel. + /// + /// The parameters with which to create the channel for the transport + /// An instance of . + IAmAChannelAsync CreateAsyncChannel(Subscription subscription); + + /// + /// Creates the input channel. + /// + /// An SqsSubscription, the subscription parameter to create the channel with. + /// Cancel the ongoing operation + /// An instance of . + /// Thrown when the subscription is incorrect + Task CreateAsyncChannelAsync(Subscription subscription, CancellationToken ct = default); } } diff --git a/src/Paramore.Brighter/IAmAChannelSync.cs b/src/Paramore.Brighter/IAmAChannelSync.cs new file mode 100644 index 0000000000..ac11026569 --- /dev/null +++ b/src/Paramore.Brighter/IAmAChannelSync.cs @@ -0,0 +1,69 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 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; + +namespace Paramore.Brighter +{ + /// + /// Interface IAmAChannelSync + /// An for reading messages from a Task Queue + /// and acknowledging receipt of those messages + /// + public interface IAmAChannelSync : IAmAChannel + { + /// + /// Acknowledges the specified message. + /// + /// The message. + void Acknowledge(Message message); + + /// + /// Clears the queue + /// + void Purge(); + + /// + /// The timeout for the channel to receive a message. + /// + /// The timeout; if null default to 1 second + /// Message. + Message Receive(TimeSpan? timeout); + + /// + /// Rejects the specified message. + /// + /// The message. + void Reject(Message message); + + /// + /// Requeues the specified message. + /// + /// The message. + /// The delay to the delivery of the message. + /// True if the message should be Acked, false otherwise + bool Requeue(Message message, TimeSpan? timeOut = null); + + } +} diff --git a/src/Paramore.Brighter/IAmAMessageConsumerAsync.cs b/src/Paramore.Brighter/IAmAMessageConsumerAsync.cs new file mode 100644 index 0000000000..fc15f9725a --- /dev/null +++ b/src/Paramore.Brighter/IAmAMessageConsumerAsync.cs @@ -0,0 +1,75 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 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; +using System.Threading.Tasks; + +namespace Paramore.Brighter +{ + /// + /// Interface IAmAReceiveMessageGatewayAsync + /// + public interface IAmAMessageConsumerAsync : IAsyncDisposable + { + /// + /// Acknowledges the specified message. + /// + /// The message. + /// Cancel the acknowledgment + Task AcknowledgeAsync(Message message, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Rejects the specified message. + /// + /// The message. + /// Cancel the rejection + Task RejectAsync(Message message, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Purges the specified queue name. + /// + Task PurgeAsync(CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Receives the specified queue name. + /// An abstraction over a third-party messaging library. Used to read messages from the broker and to acknowledge the processing of those messages or requeue them. + /// Used by a to provide access to a third-party message queue. + /// + /// The timeout. If null default to 1000 + /// Cancel the recieve + /// An array of Messages from middleware + Task ReceiveAsync(TimeSpan? timeOut = null, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Requeues the specified message. + /// + /// + /// Time to delay delivery of the message, default to 0ms or no delay + /// Cancel the requeue + /// True if the message should be acked, false otherwise + Task RequeueAsync(Message message, TimeSpan? delay = null, CancellationToken cancellationToken = default(CancellationToken)); + } +} + diff --git a/src/Paramore.Brighter/IAmAMessageConsumerFactory.cs b/src/Paramore.Brighter/IAmAMessageConsumerFactory.cs index 3589d07165..41deafb9b5 100644 --- a/src/Paramore.Brighter/IAmAMessageConsumerFactory.cs +++ b/src/Paramore.Brighter/IAmAMessageConsumerFactory.cs @@ -26,8 +26,8 @@ namespace Paramore.Brighter { /// /// Interface IAmAMessageConsumerFactory - /// We do not know how to create a implementation, as this knowledge belongs to the specific library for that broker. - /// Implementors need to provide a concrete class to create instances of for this library to use when building a + /// We do not know how to create a implementation, as this knowledge belongs to the specific library for that broker. + /// Implementors need to provide a concrete class to create instances of for this library to use when building a /// public interface IAmAMessageConsumerFactory { @@ -35,7 +35,15 @@ public interface IAmAMessageConsumerFactory /// Creates a consumer for the specified queue. /// /// The queue to connect to - /// IAmAMessageConsumer - IAmAMessageConsumer Create(Subscription subscription); + /// IAmAMessageConsumerSync + IAmAMessageConsumerSync Create(Subscription subscription); + + /// + /// Creates a consumer for the specified queue. + /// + /// The queue to connect to + /// IAmAMessageConsumerSync + IAmAMessageConsumerAsync CreateAsync(Subscription subscription); + } } diff --git a/src/Paramore.Brighter/IAmAMessageConsumer.cs b/src/Paramore.Brighter/IAmAMessageConsumerSync.cs similarity index 97% rename from src/Paramore.Brighter/IAmAMessageConsumer.cs rename to src/Paramore.Brighter/IAmAMessageConsumerSync.cs index f34a5c9e15..baf349432d 100644 --- a/src/Paramore.Brighter/IAmAMessageConsumer.cs +++ b/src/Paramore.Brighter/IAmAMessageConsumerSync.cs @@ -29,7 +29,7 @@ namespace Paramore.Brighter /// /// Interface IAmAReceiveMessageGateway /// - public interface IAmAMessageConsumer : IDisposable + public interface IAmAMessageConsumerSync : IDisposable { /// /// Acknowledges the specified message. diff --git a/src/Paramore.Brighter/IAmAMessageProducer.cs b/src/Paramore.Brighter/IAmAMessageProducer.cs index 192ef7e666..15a3b30204 100644 --- a/src/Paramore.Brighter/IAmAMessageProducer.cs +++ b/src/Paramore.Brighter/IAmAMessageProducer.cs @@ -28,7 +28,7 @@ THE SOFTWARE. */ namespace Paramore.Brighter { - public interface IAmAMessageProducer : IDisposable + public interface IAmAMessageProducer { /// /// The that this Producer is for. diff --git a/src/Paramore.Brighter/IAmAMessageProducerAsync.cs b/src/Paramore.Brighter/IAmAMessageProducerAsync.cs index 0e0be68a0b..9b5e16125e 100644 --- a/src/Paramore.Brighter/IAmAMessageProducerAsync.cs +++ b/src/Paramore.Brighter/IAmAMessageProducerAsync.cs @@ -24,6 +24,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Threading; using System.Threading.Tasks; namespace Paramore.Brighter @@ -31,39 +32,27 @@ namespace Paramore.Brighter /// /// Interface IAmASendMessageGateway /// Abstracts away the Application Layer used to push messages with async/await support onto a Task Queue - /// Usually clients do not need to instantiate as access is via an derived class. + /// Usually clients do not need to instantiate as access is via an derived class. /// We provide the following default gateway applications /// /// AMQP /// RESTML /// /// - public interface IAmAMessageProducerAsync : IAmAMessageProducer + public interface IAmAMessageProducerAsync : IAmAMessageProducer, IAsyncDisposable { /// /// Sends the specified message. /// /// The message. - Task SendAsync(Message message); - } + Task SendAsync(Message message, CancellationToken cancellationToken = default); - /// - /// Interface IAmAMessageProducerSupportingDelay - /// Abstracts away the Application Layer used to push messages with async/await support onto a Task Queue - /// Usually clients do not need to instantiate as access is via an derived class. - /// We provide the following default gateway applications - /// - /// AMQP - /// RESTML - /// - /// - public interface IAmAnAsyncMessageProducerSupportingDelay : IAmAMessageProducerAsync, IAmAMessageGatewaySupportingDelay - { /// /// Send the specified message with specified delay /// /// The message. /// Delay to the delivery of the message. 0 is no delay. Defaults to 0 - Task SendWithDelay(Message message, TimeSpan? delay); + /// A cancellation token to end the operation + Task SendWithDelayAsync(Message message, TimeSpan? delay, CancellationToken cancellationToken = default); } } diff --git a/src/Paramore.Brighter/IAmAMessageProducerFactory.cs b/src/Paramore.Brighter/IAmAMessageProducerFactory.cs index 5029477195..ad4011c7c8 100644 --- a/src/Paramore.Brighter/IAmAMessageProducerFactory.cs +++ b/src/Paramore.Brighter/IAmAMessageProducerFactory.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ #endregion using System.Collections.Generic; +using System.Threading.Tasks; namespace Paramore.Brighter { @@ -36,5 +37,11 @@ public interface IAmAMessageProducerFactory /// /// A dictionary of middleware clients by topic/routing key, for sending messages to the middleware Dictionary Create(); + + /// + /// Creates message producers. + /// + /// A dictionary of middleware clients by topic/routing key, for sending messages to the middleware + Task> CreateAsync(); } } diff --git a/src/Paramore.Brighter/IAmAMessageProducerSync.cs b/src/Paramore.Brighter/IAmAMessageProducerSync.cs index ce855201c1..7e18c01854 100644 --- a/src/Paramore.Brighter/IAmAMessageProducerSync.cs +++ b/src/Paramore.Brighter/IAmAMessageProducerSync.cs @@ -29,14 +29,14 @@ namespace Paramore.Brighter /// /// Interface IAmASendMessageGateway /// Abstracts away the Application Layer used to push messages onto a Task Queue - /// Usually clients do not need to instantiate as access is via an derived class. + /// Usually clients do not need to instantiate as access is via an derived class. /// We provide the following default gateway applications /// /// AMQP /// RESTML /// /// - public interface IAmAMessageProducerSync : IAmAMessageProducer + public interface IAmAMessageProducerSync : IAmAMessageProducer, IDisposable { /// /// Sends the specified message. diff --git a/src/Paramore.Brighter/IAmAProducerRegistry.cs b/src/Paramore.Brighter/IAmAProducerRegistry.cs index e367f4c3ea..e709a5da43 100644 --- a/src/Paramore.Brighter/IAmAProducerRegistry.cs +++ b/src/Paramore.Brighter/IAmAProducerRegistry.cs @@ -16,8 +16,15 @@ public interface IAmAProducerRegistry : IDisposable /// Looks up the producer associated with this message via a topic. The topic lives on the message headers /// /// The we want to find the producer for - /// A producer + /// A instance IAmAMessageProducer LookupBy(RoutingKey topic); + + /// + /// Looks up the producer associated with this message via a topic. The topic lives on the message headers + /// + /// The we want to find the producer for + /// A instance + IAmAMessageProducerAsync LookupAsyncBy(RoutingKey topic); /// /// Looks up the Publication used to build a given producer; useful for obtaining CloudEvents metadata @@ -28,5 +35,12 @@ public interface IAmAProducerRegistry : IDisposable /// An iterable list of all the producers in the registry /// IEnumerable Producers { get; } + + /// + /// Looks up the producer associated with this message via a topic. The topic lives on the message headers + /// + /// The we want to find the producer for + /// A producer + IAmAMessageProducerSync LookupSyncBy(RoutingKey topic); } } diff --git a/src/Paramore.Brighter/IAmAProducerRegistryFactory.cs b/src/Paramore.Brighter/IAmAProducerRegistryFactory.cs index 7df7c8b017..846fbdb707 100644 --- a/src/Paramore.Brighter/IAmAProducerRegistryFactory.cs +++ b/src/Paramore.Brighter/IAmAProducerRegistryFactory.cs @@ -22,6 +22,9 @@ THE SOFTWARE. */ #endregion +using System.Threading; +using System.Threading.Tasks; + namespace Paramore.Brighter { /// @@ -34,5 +37,15 @@ public interface IAmAProducerRegistryFactory /// /// A registry of middleware clients by topic, for sending messages to the middleware IAmAProducerRegistry Create(); + + /// + /// Creates a message producer registry. + /// + /// + /// Mainly useful where the producer creation is asynchronous, such as when connecting to a remote service to create or validate infrastructure + /// + /// A cancellation token to cancel the operation + /// A registry of middleware clients by topic, for sending messages to the middleware + Task CreateAsync(CancellationToken ct = default); } } diff --git a/src/Paramore.Brighter/InMemoryArchiveProvider.cs b/src/Paramore.Brighter/InMemoryArchiveProvider.cs index d07491b223..8b08f6c314 100644 --- a/src/Paramore.Brighter/InMemoryArchiveProvider.cs +++ b/src/Paramore.Brighter/InMemoryArchiveProvider.cs @@ -1,4 +1,28 @@ -using System.Collections.Generic; +#region Licence +/* The MIT License (MIT) +Copyright © 2024 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.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; diff --git a/src/Paramore.Brighter/InMemoryBox.cs b/src/Paramore.Brighter/InMemoryBox.cs index 017f3242e4..30f867539e 100644 --- a/src/Paramore.Brighter/InMemoryBox.cs +++ b/src/Paramore.Brighter/InMemoryBox.cs @@ -1,4 +1,28 @@ -using System; +#region Licence +/* The MIT License (MIT) +Copyright © 2024 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.Concurrent; using System.Collections.Generic; using System.Linq; diff --git a/src/Paramore.Brighter/InMemoryChannelFactory.cs b/src/Paramore.Brighter/InMemoryChannelFactory.cs index 59e81cc619..878a96e8f6 100644 --- a/src/Paramore.Brighter/InMemoryChannelFactory.cs +++ b/src/Paramore.Brighter/InMemoryChannelFactory.cs @@ -1,16 +1,100 @@ -using System; +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper -namespace Paramore.Brighter; +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: -public class InMemoryChannelFactory(InternalBus internalBus, TimeProvider timeProvider, TimeSpan? ackTimeout = null) : IAmAChannelFactory +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; +using System.Threading.Tasks; + +namespace Paramore.Brighter { - public IAmAChannel CreateChannel(Subscription subscription) + /// + /// Factory class for creating in-memory channels. + /// + public class InMemoryChannelFactory : IAmAChannelFactory { - return new Channel( - subscription.ChannelName, - subscription.RoutingKey, - new InMemoryMessageConsumer(subscription.RoutingKey,internalBus, timeProvider, ackTimeout), - subscription.BufferSize + private readonly InternalBus _internalBus; + private readonly TimeProvider _timeProvider; + private readonly TimeSpan? _ackTimeout; + + /// + /// Initializes a new instance of the class. + /// + /// The internal bus for message routing. + /// The time provider for managing time-related operations. + /// Optional acknowledgment timeout. + public InMemoryChannelFactory(InternalBus internalBus, TimeProvider timeProvider, TimeSpan? ackTimeout = null) + { + _internalBus = internalBus; + _timeProvider = timeProvider; + _ackTimeout = ackTimeout; + } + + /// + /// Creates a synchronous channel. + /// + /// The subscription details for the channel. + /// A synchronous channel instance. + public IAmAChannelSync CreateSyncChannel(Subscription subscription) + { + return new Channel( + subscription.ChannelName, + subscription.RoutingKey, + new InMemoryMessageConsumer(subscription.RoutingKey, _internalBus, _timeProvider, _ackTimeout), + subscription.BufferSize + ); + } + + /// + /// Creates an asynchronous channel. + /// + /// The subscription details for the channel. + /// An asynchronous channel instance. + public IAmAChannelAsync CreateAsyncChannel(Subscription subscription) + { + return new ChannelAsync( + subscription.ChannelName, + subscription.RoutingKey, + new InMemoryMessageConsumer(subscription.RoutingKey, _internalBus, _timeProvider, _ackTimeout), + subscription.BufferSize + ); + } + + /// + /// Asynchronously creates an asynchronous channel. + /// + /// The subscription details for the channel. + /// A token to cancel the operation. + /// A task representing the asynchronous operation, with an asynchronous channel instance as the result. + public Task CreateAsyncChannelAsync(Subscription subscription, CancellationToken cancellationToken = default) + { + IAmAChannelAsync channel = new ChannelAsync( + subscription.ChannelName, + subscription.RoutingKey, + new InMemoryMessageConsumer(subscription.RoutingKey, _internalBus, _timeProvider, _ackTimeout), + subscription.BufferSize ); + return Task.FromResult(channel); + } } } diff --git a/src/Paramore.Brighter/InMemoryLock.cs b/src/Paramore.Brighter/InMemoryLock.cs index 681e3f578b..43c2cad738 100644 --- a/src/Paramore.Brighter/InMemoryLock.cs +++ b/src/Paramore.Brighter/InMemoryLock.cs @@ -20,6 +20,7 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 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; diff --git a/src/Paramore.Brighter/InMemoryMessageConsumer.cs b/src/Paramore.Brighter/InMemoryMessageConsumer.cs index d0292fdb1b..aaf873a99a 100644 --- a/src/Paramore.Brighter/InMemoryMessageConsumer.cs +++ b/src/Paramore.Brighter/InMemoryMessageConsumer.cs @@ -25,6 +25,7 @@ THE SOFTWARE. */ using System; using System.Collections.Concurrent; using System.Threading; +using System.Threading.Tasks; namespace Paramore.Brighter; @@ -35,7 +36,7 @@ namespace Paramore.Brighter; /// within the timeout. This is controlled by a background thread that checks the messages in the locked list /// and requeues them if they have been locked for longer than the timeout. /// -public class InMemoryMessageConsumer : IAmAMessageConsumer +public class InMemoryMessageConsumer : IAmAMessageConsumerSync, IAmAMessageConsumerAsync { private readonly ConcurrentDictionary _lockedMessages = new(); private readonly RoutingKey _topic; @@ -73,7 +74,7 @@ public InMemoryMessageConsumer(RoutingKey topic, InternalBus bus, TimeProvider t } - /// + /// /// Acknowledges the specified message. /// /// The message. @@ -82,6 +83,17 @@ public void Acknowledge(Message message) _lockedMessages.TryRemove(message.Id, out _); } + /// + /// Acknowledges the specified message. + /// We use Task.Run here to emulate async + /// + /// The message. + /// Cancel the acknowledgement + public async Task AcknowledgeAsync(Message message, CancellationToken cancellationToken = default) + { + await Task.Run(() => Acknowledge(message), cancellationToken); + } + /// /// Rejects the specified message. /// @@ -91,6 +103,17 @@ public void Reject(Message message) _lockedMessages.TryRemove(message.Id, out _); } + /// + /// Rejects the specified message. + /// We use Task.Run here to emulate async + /// + /// The message. + /// Cancel the rejection + public async Task RejectAsync(Message message, CancellationToken cancellationToken = default) + { + await Task.Run(() => Reject(message), cancellationToken); + } + /// /// Purges the specified queue name. /// @@ -101,6 +124,16 @@ public void Purge() message = _bus.Dequeue(_topic); } while (message.Header.MessageType != MessageType.MT_NONE); } + + /// + /// Purges the specified queue name. + /// We use Task.Run here to emulate async + /// + /// Cancel the purge + public async Task PurgeAsync(CancellationToken cancellationToken = default) + { + await Task.Run(() => Purge(), cancellationToken); + } /// /// Receives the specified queue name. @@ -125,10 +158,24 @@ public Message[] Receive(TimeSpan? timeOut = null) return messages; } + /// + /// Receives the specified queue name. + /// An abstraction over a third-party messaging library. Used to read messages from the broker and to acknowledge the processing of those messages or requeue them. + /// Used by a to provide access to a third-party message queue. + /// We use Task.Run here to emulate async + /// + /// The timeout + /// Cancel in the receive operation + /// An array of Messages from middleware + public async Task ReceiveAsync(TimeSpan? timeOut = null, CancellationToken cancellationToken = default) + { + return await Task.Run(() => Receive(timeOut), cancellationToken); + } + /// /// Requeues the specified message. /// - /// + /// The message to requeue /// Time span to delay delivery of the message. Defaults to 0ms /// True if the message should be acked, false otherwise public bool Requeue(Message message, TimeSpan? timeOut = null) @@ -148,10 +195,41 @@ public bool Requeue(Message message, TimeSpan? timeOut = null) return true; } + + /// + /// Requeues the specified message. + /// We use Task.Run here to emulate async + /// + /// The message to requeue + /// Time span to delay delivery of the message. Defaults to 0ms + /// True if the message should be acked, false otherwise + public Task RequeueAsync(Message message, TimeSpan? timeOut = null, CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + + if (cancellationToken.IsCancellationRequested) + { + tcs.SetCanceled(); + return tcs.Task; + } + + Requeue(message, timeOut); + tcs.SetResult(true); + return tcs.Task; + } + /// public void Dispose() { - _lockTimer.Dispose(); + DisposeCore(); + GC.SuppressFinalize(this); + } + + /// + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore().ConfigureAwait(false); + GC.SuppressFinalize(this); } private void CheckLockedMessages() @@ -165,6 +243,18 @@ private void CheckLockedMessages() } } } + + protected virtual void DisposeCore() + { + _lockTimer.Dispose(); + _requeueTimer?.Dispose(); + } + + protected virtual async ValueTask DisposeAsyncCore() + { + await _lockTimer.DisposeAsync().ConfigureAwait(false); + if (_requeueTimer != null) await _requeueTimer.DisposeAsync().ConfigureAwait(false); + } private bool RequeueNoDelay(Message message) { @@ -182,4 +272,6 @@ private bool RequeueNoDelay(Message message) } private record LockedMessage(Message Message, DateTimeOffset LockedAt); + + } diff --git a/src/Paramore.Brighter/InMemoryMessageProducerFactory.cs b/src/Paramore.Brighter/InMemoryMessageProducerFactory.cs index a352e97633..de41ae1491 100644 --- a/src/Paramore.Brighter/InMemoryMessageProducerFactory.cs +++ b/src/Paramore.Brighter/InMemoryMessageProducerFactory.cs @@ -23,31 +23,47 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Threading.Tasks; -namespace Paramore.Brighter; - -/// -/// A factory for creating a dictionary of in-memory producers indexed by topic. This is mainly intended for usage with tests. -/// It allows you to send messages to a bus and then inspect the messages that have been sent. -/// -/// An instance of typically we use an -/// The list of topics that we want to publish to -public class InMemoryMessageProducerFactory(InternalBus bus, IEnumerable publications) - : IAmAMessageProducerFactory +namespace Paramore.Brighter { - /// - public Dictionary Create() + /// + /// A factory for creating a dictionary of in-memory producers indexed by topic. This is mainly intended for usage with tests. + /// It allows you to send messages to a bus and then inspect the messages that have been sent. + /// + /// An instance of typically we use an + /// The list of topics that we want to publish to + public class InMemoryMessageProducerFactory(InternalBus bus, IEnumerable publications) + : IAmAMessageProducerFactory { - var producers = new Dictionary(); - foreach (var publication in publications) + /// + /// Creates a dictionary of in-memory message producers. + /// + /// A dictionary of indexed by + /// Thrown when a publication does not have a topic + public Dictionary Create() { - if (publication.Topic is null) - throw new ArgumentException("A publication must have a Topic to be dispatched"); - var producer = new InMemoryProducer(bus, TimeProvider.System); - producer.Publication = publication; - producers[publication.Topic] = producer; + var producers = new Dictionary(); + foreach (var publication in publications) + { + if (publication.Topic is null) + throw new ArgumentException("A publication must have a Topic to be dispatched"); + var producer = new InMemoryProducer(bus, TimeProvider.System); + producer.Publication = publication; + producers[publication.Topic] = producer; + } + + return producers; } - return producers; + /// + /// Creates a dictionary of in-memory message producers. + /// + /// A dictionary of indexed by + /// Thrown when a publication does not have a topic + public Task> CreateAsync() + { + return Task.FromResult(Create()); + } } } diff --git a/src/Paramore.Brighter/InMemoryProducer.cs b/src/Paramore.Brighter/InMemoryProducer.cs index cef25bdf3b..6b88658f8f 100644 --- a/src/Paramore.Brighter/InMemoryProducer.cs +++ b/src/Paramore.Brighter/InMemoryProducer.cs @@ -62,16 +62,32 @@ public class InMemoryProducer(IAmABus bus, TimeProvider timeProvider) public event Action? OnMessagePublished; /// - /// Dispsose of the producer; a no-op for the in-memory producer + /// Dispose of the producer + /// Clears the associated timer /// - public void Dispose() { } + public void Dispose() + { + if (_requeueTimer != null)_requeueTimer.Dispose(); + GC.SuppressFinalize(this); + } + + /// + /// Dispose of the producer + /// Clears the associated timer + /// + public async ValueTask DisposeAsync() + { + if (_requeueTimer != null) await _requeueTimer.DisposeAsync(); + GC.SuppressFinalize(this); + } /// /// Send a message to a broker; in this case an /// /// The message to send + /// Cancel the Send operation /// - public Task SendAsync(Message message) + public Task SendAsync(Message message, CancellationToken cancellationToken = default) { BrighterTracer.WriteProducerEvent(Span, MessagingSystem.InternalBus, message); @@ -99,7 +115,7 @@ public async IAsyncEnumerable SendAsync(IEnumerable messages, BrighterTracer.WriteProducerEvent(Span, MessagingSystem.InternalBus, msg); bus.Enqueue(msg); OnMessagePublished?.Invoke(true, msg.Id); - yield return new[] { msg.Id }; + yield return [msg.Id]; } } @@ -132,6 +148,29 @@ public void SendWithDelay(Message message, TimeSpan? delay = null) TimeSpan.Zero ); } + + /// + /// Send a message to a broker; in this case an with a delay + /// The delay is simulated by the + /// + /// The message to send + /// The delay of the send + /// A cancellation token for send operation + public Task SendWithDelayAsync(Message message, TimeSpan? delay, CancellationToken cancellationToken = default) + { + delay ??= TimeSpan.FromMilliseconds(0); + + //we don't want to block, so we use a timer to invoke the requeue after a delay + _requeueTimer = timeProvider.CreateTimer( + msg => SendAsync((Message)msg!, cancellationToken), + message, + delay.Value, + TimeSpan.Zero + ); + + return Task.CompletedTask; + } + private void SendNoDelay(Message message) { @@ -144,5 +183,7 @@ private void SendNoDelay(Message message) _requeueTimer?.Dispose(); } } + + } } diff --git a/src/Paramore.Brighter/InMemoryProducerRegistryFactory.cs b/src/Paramore.Brighter/InMemoryProducerRegistryFactory.cs index 056175b6a0..df0b53f41c 100644 --- a/src/Paramore.Brighter/InMemoryProducerRegistryFactory.cs +++ b/src/Paramore.Brighter/InMemoryProducerRegistryFactory.cs @@ -1,14 +1,59 @@ -using System.Collections.Generic; +#region Licence +/* The MIT License (MIT) +Copyright © 2015 Toby Henderson -namespace Paramore.Brighter; +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: -public class InMemoryProducerRegistryFactory(InternalBus bus, IEnumerable publications) - : IAmAProducerRegistryFactory +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.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter { - public IAmAProducerRegistry Create() + /// + /// A factory for creating an in-memory producer registry. This is mainly intended for usage with tests. + /// It allows you to create a registry of in-memory producers that can be used to send messages to a bus. + /// + /// An instance of typically used for testing + /// The list of topics that we want to publish to + public class InMemoryProducerRegistryFactory(InternalBus bus, IEnumerable publications) + : IAmAProducerRegistryFactory { - var producerFactory = new InMemoryMessageProducerFactory(bus, publications); + /// + /// Creates an in-memory producer registry. + /// + /// An instance of + public IAmAProducerRegistry Create() + { + var producerFactory = new InMemoryMessageProducerFactory(bus, publications); + return new ProducerRegistry(producerFactory.Create()); + } - return new ProducerRegistry(producerFactory.Create()); + /// + /// Asynchronously creates an in-memory producer registry. + /// + /// A cancellation token to cancel the operation + /// A task that represents the asynchronous operation. The task result contains an instance of + public Task CreateAsync(CancellationToken ct = default) + { + return Task.FromResult(Create()); + } } } diff --git a/src/Paramore.Brighter/InboxConfiguration.cs b/src/Paramore.Brighter/InboxConfiguration.cs index a77a7ab1e1..b2bb7d6b35 100644 --- a/src/Paramore.Brighter/InboxConfiguration.cs +++ b/src/Paramore.Brighter/InboxConfiguration.cs @@ -1,3 +1,27 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 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.Inbox; diff --git a/src/Paramore.Brighter/MessagePumpType.cs b/src/Paramore.Brighter/MessagePumpType.cs new file mode 100644 index 0000000000..80bf074148 --- /dev/null +++ b/src/Paramore.Brighter/MessagePumpType.cs @@ -0,0 +1,7 @@ +namespace Paramore.Brighter; + +public enum MessagePumpType +{ + Reactor, + Proactor +} diff --git a/src/Paramore.Brighter/OutboxProducerMediator.cs b/src/Paramore.Brighter/OutboxProducerMediator.cs index a094d0770c..799d4f7ca6 100644 --- a/src/Paramore.Brighter/OutboxProducerMediator.cs +++ b/src/Paramore.Brighter/OutboxProducerMediator.cs @@ -1,4 +1,27 @@ -using System; +#region Licence +/* The MIT License (MIT) +Copyright © 2022 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.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -248,10 +271,9 @@ public void CallViaExternalBus(Message outMessage, RequestContext? where T : class, ICall where TResponse : class, IResponse { //We assume that this only occurs over a blocking producer - var producer = _producerRegistry.LookupBy(outMessage.Header.Topic); - if (producer is IAmAMessageProducerSync producerSync) + var producer = _producerRegistry.LookupSyncBy(outMessage.Header.Topic); Retry( - () => producerSync.Send(outMessage), + () => producer.Send(outMessage), requestContext ); } @@ -437,6 +459,7 @@ CancellationToken cancellationToken /// /// Intended for usage with the CommandProcessor's Call method, this method will create a request from a message + /// Sync over async as we block on Call /// /// The message that forms a reply to a call /// The request constructed from that message diff --git a/src/Paramore.Brighter/Policies/Handlers/TimeoutPolicyHandler.cs b/src/Paramore.Brighter/Policies/Handlers/TimeoutPolicyHandler.cs index 7cd5c164a2..8ea579ba9b 100644 --- a/src/Paramore.Brighter/Policies/Handlers/TimeoutPolicyHandler.cs +++ b/src/Paramore.Brighter/Policies/Handlers/TimeoutPolicyHandler.cs @@ -90,7 +90,7 @@ public override void InitializeFromAttributeParams(params object?[] initializerL } /// - /// Runs the remainder of the pipeline within a task that will timeout if it does not complete within the + /// Runs the remainder of the pipeline within a parentTask that will timeout if it does not complete within the /// configured number of milliseconds /// /// The command. @@ -103,7 +103,7 @@ public override TRequest Handle(TRequest command) var timeoutTask = Task.Factory.StartNew( function: () => { - //we already cancelled the task + //we already cancelled the parentTask ct.ThrowIfCancellationRequested(); //allow the handlers that can timeout to grab the cancellation token Context?.Bag.AddOrUpdate(CONTEXT_BAG_TIMEOUT_CANCELLATION_TOKEN, ct, (s, o) => o = ct); @@ -125,10 +125,10 @@ public override TRequest Handle(TRequest command) private Task TimeoutAfter(Task task, int millisecondsTimeout, CancellationTokenSource cancellationTokenSource) { - // Short-circuit #1: infinite timeout or task already completed + // Short-circuit #1: infinite timeout or parentTask already completed if (task.IsCompleted || (millisecondsTimeout == Timeout.Infinite)) { - // Either the task has already completed or timeout will never occur. + // Either the parentTask has already completed or timeout will never occur. // No proxy necessary. return task; } @@ -164,7 +164,7 @@ private Task TimeoutAfter(Task task, int millisecondsTimeout Timeout.Infinite ); - // Wire up the logic for what happens when source task completes + // Wire up the logic for what happens when source parentTask completes task.ContinueWith((antecedent, state) => { // Recover our state data diff --git a/src/Paramore.Brighter/ProducerRegistry.cs b/src/Paramore.Brighter/ProducerRegistry.cs index ec7b63adb1..d11a6ec4fe 100644 --- a/src/Paramore.Brighter/ProducerRegistry.cs +++ b/src/Paramore.Brighter/ProducerRegistry.cs @@ -4,9 +4,26 @@ namespace Paramore.Brighter { - public class ProducerRegistry(Dictionary messageProducers) : IAmAProducerRegistry + public class ProducerRegistry(Dictionary? messageProducers) + : IAmAProducerRegistry { private readonly bool _hasProducers = messageProducers != null && messageProducers.Any(); + + /// + /// An iterable list of all the producers in the registry + /// + public IEnumerable Producers { get { return messageProducers is not null ? messageProducers.Values : Array.Empty(); } } + + /// + /// An iterable list of all the sync producers in the registry + /// + public IEnumerable ProducersSync => messageProducers is not null ? messageProducers.Values.Cast() : Array.Empty(); + + /// + /// An iterable list of all the sync producers in the registry + /// + public IEnumerable ProducersAsync => messageProducers is not null ? messageProducers.Values.Cast() : Array.Empty(); + /// /// Will call CloseAll to terminate producers @@ -21,29 +38,56 @@ public void Dispose() { CloseAll(); } - + + /// + /// Iterates through all the producers and disposes them, as they may have unmanaged resources that should be shut down in an orderly fashion + /// + public void CloseAll() + { + if (messageProducers is not null) + { + foreach (var producer in messageProducers) + { + if (producer.Value is IDisposable disposable) + disposable.Dispose(); + } + + messageProducers.Clear(); + } + } + + /// - /// Iterates through all the producers and disposes them, as they may have unmanaged resources that should be shut down in an orderly fashion + /// Looks up the producer associated with this message via a topic. The topic lives on the message headers /// - public void CloseAll() + /// The we want to find the producer for + /// A producer + public IAmAMessageProducer LookupBy(RoutingKey topic) { - foreach (var producer in messageProducers) - { - producer.Value.Dispose(); - } + if (!_hasProducers) + throw new ConfigurationException("No producers found in the registry"); - messageProducers.Clear(); + return messageProducers![topic]; } - /// /// Looks up the producer associated with this message via a topic. The topic lives on the message headers /// /// The we want to find the producer for /// A producer - public IAmAMessageProducer LookupBy(RoutingKey topic) + public IAmAMessageProducerAsync LookupAsyncBy(RoutingKey topic) + { + return (IAmAMessageProducerAsync)LookupBy(topic); + } + + /// + /// Looks up the producer associated with this message via a topic. The topic lives on the message headers + /// + /// The we want to find the producer for + /// A producer + public IAmAMessageProducerSync LookupSyncBy(RoutingKey topic) { - return messageProducers[topic]; + return (IAmAMessageProducerSync)LookupBy(topic); } /// @@ -58,20 +102,17 @@ public Publication LookupPublication() where TRequest : class, IReques where producer.Value.Publication.RequestType == typeof(TRequest) select producer.Value.Publication; - if (publications.Count() > 1) + var publicationsArray = publications as Publication[] ?? publications.ToArray(); + + if (publicationsArray.Count() > 1) throw new ConfigurationException("Only one producer per request type is supported. Have you added the request type to multiple Publications?"); - var publication = publications.FirstOrDefault(); + var publication = publicationsArray.FirstOrDefault(); if (publication is null) throw new ConfigurationException("No producer found for request type. Have you set the request type on the Publication?"); return publication; } - - /// - /// An iterable list of all the producers in the registry - /// - public IEnumerable Producers { get { return messageProducers.Values; } } } } diff --git a/src/Paramore.Brighter/SimpleMessageTransformerFactory.cs b/src/Paramore.Brighter/SimpleMessageTransformerFactory.cs index 5fb3c290aa..9120d1b1a0 100644 --- a/src/Paramore.Brighter/SimpleMessageTransformerFactory.cs +++ b/src/Paramore.Brighter/SimpleMessageTransformerFactory.cs @@ -31,18 +31,12 @@ namespace Paramore.Brighter /// This class allows you to return a simple function that finds a transformer. Intended for lightweight transformer pipelines. /// We recommend you wrap your IoC container for heavyweight mapping. /// - public class SimpleMessageTransformerFactory : IAmAMessageTransformerFactory + public class SimpleMessageTransformerFactory(Func factoryMethod) + : IAmAMessageTransformerFactory { - private readonly Func _factoryMethod; - - public SimpleMessageTransformerFactory(Func factoryMethod) - { - _factoryMethod = factoryMethod; - } - public IAmAMessageTransform Create(Type transformerType) { - return _factoryMethod(transformerType); + return factoryMethod(transformerType); } public void Release(IAmAMessageTransform transformer) diff --git a/src/Paramore.Brighter/Subscription.cs b/src/Paramore.Brighter/Subscription.cs index 16b3823e7a..4cb5ab50b3 100644 --- a/src/Paramore.Brighter/Subscription.cs +++ b/src/Paramore.Brighter/Subscription.cs @@ -57,7 +57,7 @@ public class Subscription /// /// How long to pause when there is a channel failure in milliseconds /// - public int ChannelFailureDelay { get; set; } + public TimeSpan ChannelFailureDelay { get; set; } /// /// Gets the type of the that s on the can be translated into. @@ -68,7 +68,7 @@ public class Subscription /// /// How long to pause when a channel is empty in milliseconds /// - public int EmptyChannelDelay { get; set; } + public TimeSpan EmptyChannelDelay { get; set; } /// /// Should we declare infrastructure, or should we just validate that it exists, and assume it is declared elsewhere @@ -110,7 +110,7 @@ public class Subscription /// This increases throughput (although it will no longer throttle use of the resources on the host machine). /// /// true if this instance should use an asynchronous pipeline; otherwise, false - public bool RunAsync { get; } + public MessagePumpType MessagePumpType { get; } /// /// Gets the timeout that we use to infer that nothing could be read from the channel i.e. is empty @@ -137,7 +137,7 @@ public class Subscription /// The number of times you want to requeue a message before dropping it. /// The delay the delivery of a requeue message for. /// The number of unacceptable messages to handle, before stopping reading from the channel. - /// Is this channel read asynchronously + /// Is this channel read asynchronously /// The channel factory to create channels for Consumer. /// Should we make channels if they don't exist, defaults to creating /// How long to pause when a channel is empty in milliseconds @@ -153,11 +153,11 @@ public Subscription( int requeueCount = -1, TimeSpan? requeueDelay = null, int unacceptableMessageLimit = 0, - bool runAsync = false, + MessagePumpType messagePumpType = MessagePumpType.Proactor, IAmAChannelFactory? channelFactory = null, OnMissingChannel makeChannels = OnMissingChannel.Create, - int emptyChannelDelay = 500, - int channelFailureDelay = 1000) + TimeSpan? emptyChannelDelay = null, + TimeSpan? channelFailureDelay = null) { DataType = dataType; Name = name ?? new SubscriptionName(dataType.FullName!); @@ -171,11 +171,11 @@ public Subscription( requeueDelay ??= TimeSpan.Zero; RequeueDelay = requeueDelay.Value; UnacceptableMessageLimit = unacceptableMessageLimit; - RunAsync = runAsync; + MessagePumpType = messagePumpType; ChannelFactory = channelFactory; MakeChannels = makeChannels; - EmptyChannelDelay = emptyChannelDelay; - ChannelFailureDelay = channelFailureDelay; + EmptyChannelDelay = emptyChannelDelay ?? TimeSpan.FromMilliseconds(500); + ChannelFailureDelay = channelFailureDelay ?? TimeSpan.FromMilliseconds(1000); } public void SetNumberOfPerformers(int numberOfPerformers) @@ -199,7 +199,7 @@ public class Subscription : Subscription /// The number of times you want to requeue a message before dropping it. /// The delay the delivery of a requeue message; defaults to 0ms /// The number of unacceptable messages to handle, before stopping reading from the channel. - /// + /// /// The channel factory to create channels for Consumer. /// Should we make channels if they don't exist, defaults to creating /// How long to pause when a channel is empty in milliseconds @@ -214,11 +214,11 @@ public Subscription( int requeueCount = -1, TimeSpan? requeueDelay = null, int unacceptableMessageLimit = 0, - bool runAsync = false, + MessagePumpType messagePumpType = MessagePumpType.Proactor, IAmAChannelFactory? channelFactory = null, OnMissingChannel makeChannels = OnMissingChannel.Create, - int emptyChannelDelay = 500, - int channelFailureDelay = 1000) + TimeSpan? emptyChannelDelay = null, + TimeSpan? channelFailureDelay = null) : base( typeof(T), name, @@ -230,7 +230,7 @@ public Subscription( requeueCount, requeueDelay, unacceptableMessageLimit, - runAsync, + messagePumpType, channelFactory, makeChannels, emptyChannelDelay, diff --git a/src/Paramore.Brighter/Tasks/BoundActionField.cs b/src/Paramore.Brighter/Tasks/BoundActionField.cs new file mode 100644 index 0000000000..f7140264b2 --- /dev/null +++ b/src/Paramore.Brighter/Tasks/BoundActionField.cs @@ -0,0 +1,61 @@ +#region Sources +//copy of Stephen Cleary's Nito Disposables BoundAction.cs +// see https://github.com/StephenCleary/Disposables/blob/main/src/Nito.Disposables/Internals/BoundAction.cs +#endregion + +using System; +using System.Threading; + +namespace Paramore.Brighter.Tasks; + +internal sealed class BoundActionField(Action action, T context) +{ + private BoundAction? _field = new(action, context); + + public bool IsEmpty => Interlocked.CompareExchange(ref _field, null, null) == null; + + public IBoundAction? TryGetAndUnset() + { + return Interlocked.Exchange(ref _field, null); + } + + public bool TryUpdateContext(Func contextUpdater) + { + _ = contextUpdater ?? throw new ArgumentNullException(nameof(contextUpdater)); + while (true) + { + var original = Interlocked.CompareExchange(ref _field, _field, _field); + if (original == null) + return false; + var updatedContext = new BoundAction(original, contextUpdater); + var result = Interlocked.CompareExchange(ref _field, updatedContext, original); + if (ReferenceEquals(original, result)) + return true; + } + } + + public interface IBoundAction + { + void Invoke(); + } + + private sealed class BoundAction : IBoundAction + { + private readonly Action _action; + private readonly T _context; + + public BoundAction(Action action, T context) + { + _action = action; + _context = context; + } + + public BoundAction(BoundAction originalBoundAction, Func contextUpdater) + { + _action = originalBoundAction._action; + _context = contextUpdater(originalBoundAction._context); + } + + public void Invoke() => _action?.Invoke(_context); + } + } diff --git a/src/Paramore.Brighter/Tasks/BrighterSynchronizationContext.cs b/src/Paramore.Brighter/Tasks/BrighterSynchronizationContext.cs new file mode 100644 index 0000000000..502c1dedfd --- /dev/null +++ b/src/Paramore.Brighter/Tasks/BrighterSynchronizationContext.cs @@ -0,0 +1,196 @@ +#region Sources + +// This class is based on Stephen Cleary's AyncContext in https://github.com/StephenCleary/AsyncEx +// The original code is licensed under the MIT License (MIT) class. + /// + /// The synchronization helper. + public BrighterSynchronizationContext(BrighterSynchronizationHelper synchronizationHelper) + { + SynchronizationHelper = synchronizationHelper; + _executionContext = ExecutionContext.Capture(); + } + + /// + /// Creates a copy of the synchronization context. + /// + /// A new object. + public override SynchronizationContext CreateCopy() + { + return new BrighterSynchronizationContext(SynchronizationHelper); + } + + ///inheritdoc /> + public override bool Equals(object? obj) + { + var other = obj as BrighterSynchronizationContext; + if (other == null) + return false; + return (SynchronizationHelper == other.SynchronizationHelper); + } + + ///inheritdoc /> + public override int GetHashCode() + { + return SynchronizationHelper.GetHashCode(); + } + + /// + /// Notifies the context that an operation has completed. + /// + public override void OperationCompleted() + { + Debug.WriteLine(string.Empty); + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationContext: OperationCompleted on thread {Thread.CurrentThread.ManagedThreadId}"); + Debug.WriteLine($"BrighterSynchronizationContext: Parent Task {ParentTaskId}"); + Debug.IndentLevel = 0; + + + SynchronizationHelper.OperationCompleted(); + } + + /// + /// Notifies the context that an operation has started. + /// + public override void OperationStarted() + { + Debug.WriteLine(string.Empty); + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationContext: OperationStarted on thread {Thread.CurrentThread.ManagedThreadId}"); + Debug.WriteLine($"BrighterSynchronizationContext: Parent Task {ParentTaskId}"); + Debug.IndentLevel = 0; + + SynchronizationHelper.OperationStarted(); + } + + /// + /// Dispatches an asynchronous message to the synchronization context. + /// + /// The delegate to call. + /// The object passed to the delegate. + public override void Post(SendOrPostCallback callback, object? state) + { + Debug.WriteLine(string.Empty); + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationContext: Post {callback.Method.Name} on thread {Thread.CurrentThread.ManagedThreadId}"); + Debug.WriteLine($"BrighterSynchronizationContext: Parent Task {ParentTaskId}"); + Debug.IndentLevel = 0; + + if (callback == null) throw new ArgumentNullException(nameof(callback)); + var ctxt = ExecutionContext.Capture(); + bool queued = SynchronizationHelper.Enqueue(new ContextMessage(callback, state, ctxt), true); + + if (queued) return; + + //NOTE: if we got here, something went wrong, we should have been able to queue the message + //mostly this seems to be a problem with the task we are running completing, but work is still being queued to the + //synchronization context. + // If the execution context can help, we might be able to redirect; if not just run immediately on this thread + + var contextCallback = new ContextCallback(callback); + if (ctxt != null && ctxt != _executionContext) + { + Debug.WriteLine(string.Empty); + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationContext: Post Failed to queue {callback.Method.Name} on thread {Thread.CurrentThread.ManagedThreadId}"); + Debug.WriteLine($"BrighterSynchronizationContext: Parent Task {ParentTaskId}"); + Debug.IndentLevel = 0; + SynchronizationHelper.ExecuteOnContext(ctxt, contextCallback, state); + } + else + { + Debug.WriteLine(string.Empty); + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationContext: Post Failed to queue {callback.Method.Name} on thread {Thread.CurrentThread.ManagedThreadId}"); + Debug.WriteLine($"BrighterSynchronizationContext: Parent Task {ParentTaskId}"); + Debug.IndentLevel = 0; + //just execute inline + SynchronizationHelper.ExecuteImmediately(contextCallback, state); + } + Debug.WriteLine(string.Empty); + + } + + /// + /// Dispatches a synchronous message to the synchronization context. + /// + /// The delegate to call. + /// The object passed to the delegate. + public override void Send(SendOrPostCallback callback, object? state) + { + Debug.WriteLine(string.Empty); + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationContext: Send {callback.Method.Name} on thread {Thread.CurrentThread.ManagedThreadId}"); + Debug.WriteLine($"BrighterSynchronizationContext: Parent Task {ParentTaskId}"); + Debug.IndentLevel = 0; + + // current thread already owns the context, so just execute inline to prevent deadlocks + if (BrighterSynchronizationHelper.Current == SynchronizationHelper) + { + callback(state); + } + else + { + var ctxt = ExecutionContext.Capture(); + var task = SynchronizationHelper.MakeTask(new ContextMessage(callback, state, ctxt)); + if (!task.Wait(Timeout)) // Timeout mechanism + throw new TimeoutException("BrighterSynchronizationContext: Send operation timed out."); + } + } + } +} diff --git a/src/Paramore.Brighter/Tasks/BrighterSynchronizationContextScope.cs b/src/Paramore.Brighter/Tasks/BrighterSynchronizationContextScope.cs new file mode 100644 index 0000000000..731c826a5d --- /dev/null +++ b/src/Paramore.Brighter/Tasks/BrighterSynchronizationContextScope.cs @@ -0,0 +1,95 @@ +#region Sources + +// This class is based on Stephen Cleary's AyncContext in https://github.com/StephenCleary/AsyncEx +// The original code is licensed under the MIT License (MIT) struct. + /// + /// The new synchronization context to set. + /// + private BrighterSynchronizationContextScope(BrighterSynchronizationContext newContext, Task parentTask) + : base(new object()) + { + Debug.WriteLine(string.Empty); + Debug.WriteLine("{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{"); + Debug.IndentLevel = 1; + Debug.WriteLine($"Entering BrighterSynchronizationContext on thread {Thread.CurrentThread.ManagedThreadId}"); + Debug.WriteLine($"BrighterSynchronizationContext: Parent Task {parentTask.Id}"); + Debug.IndentLevel = 0; + + + _newContext = newContext; + _newContext.ParentTaskId = parentTask.Id; + + // Save the original synchronization context + _originalContext = SynchronizationContext.Current; + _hasOriginalContext = _originalContext != null; + + // Set the new synchronization context + SynchronizationContext.SetSynchronizationContext(newContext); + } + + /// + /// Restores the original synchronization context. + /// + protected override void Dispose(object context) + { + Debug.IndentLevel = 1; + Debug.WriteLine($"Exiting BrighterSynchronizationContextScope for task {_newContext?.ParentTaskId}"); + Debug.WriteLine($"BrighterSynchronizationContext: Parent Task {_newContext?.ParentTaskId}"); + Debug.IndentLevel = 0; + Debug.WriteLine("}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}"); + + if (_newContext is not null) _newContext.ParentTaskId = 0; + _newContext = null; + + // Restore the original synchronization context + SynchronizationContext.SetSynchronizationContext(_hasOriginalContext ? _originalContext : null); + + } + + /// + /// Executes a method with the specified synchronization context, and then restores the original context. + /// + /// The original synchronization context + /// + /// The action to take within the context + /// If the action passed was null + public static void ApplyContext(BrighterSynchronizationContext? context, Task parentTask, Action action) + { + if (context is null) throw new ArgumentNullException(nameof(context)); + if (action is null) throw new ArgumentNullException(nameof(action)); + + using (new BrighterSynchronizationContextScope(context, parentTask)) + action(); + } +} diff --git a/src/Paramore.Brighter/Tasks/BrighterSynchronizationHelper.cs b/src/Paramore.Brighter/Tasks/BrighterSynchronizationHelper.cs new file mode 100644 index 0000000000..a84b9c1ada --- /dev/null +++ b/src/Paramore.Brighter/Tasks/BrighterSynchronizationHelper.cs @@ -0,0 +1,467 @@ +#region Sources + +// This class is based on Stephen Cleary's AyncContext in https://github.com/StephenCleary/AsyncEx +// The original code is licensed under the MIT License (MIT) class. + /// + public BrighterSynchronizationHelper() + { + _taskScheduler = new BrighterTaskScheduler(this); + _synchronizationContext = new BrighterSynchronizationContext(this); + _taskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.HideScheduler, TaskContinuationOptions.HideScheduler, _taskScheduler); + + _defaultTaskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default); + } + + /// + /// What tasks are currently active? + /// + /// Used for debugging + /// + /// + public IEnumerable ActiveTasks => _activeTasks.Keys; + + /// + /// A default task factory, used to return to the default task factory + /// + internal TaskFactory DefaultTaskFactory => _defaultTaskFactory; + + /// + /// Access the task factory + /// + /// + /// Intended for tests + /// + public TaskFactory Factory => _taskFactory; + + /// + /// This is the same identifier as the context's . Used for testing + /// + public int Id => _taskScheduler.Id; + + /// + /// How many operations are currently outstanding? + /// + /// + /// Intended for debugging + /// + public int OutstandingOperations { get; set; } + + /// + /// Access the task scheduler, + /// + /// + /// Intended for tests + /// + public TaskScheduler TaskScheduler => _taskScheduler; + + /// + /// Access the synchoronization context, intended for tests + /// + public SynchronizationContext? SynchronizationContext => _synchronizationContext; + + /// + /// Disposes the synchronization helper and clears the task queue. + /// + public void Dispose() + { + _taskQueue.CompleteAdding(); + _taskQueue.Dispose(); + } + + /// + /// Gets the current synchronization helper. + /// + public static BrighterSynchronizationHelper? Current + { + get + { + var syncContext = SynchronizationContext.Current as BrighterSynchronizationContext; + return syncContext?.SynchronizationHelper; + } + } + + /// + /// Enqueues a context message for execution. + /// + /// The context message to enqueue. + /// Indicates whether to propagate exceptions. + public bool Enqueue(ContextMessage message, bool propagateExceptions) + { + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Enqueueing message {message.Callback.Method.Name} on thread {Thread.CurrentThread.ManagedThreadId} for BrighterSynchronizationHelper {Id}"); + Debug.IndentLevel = 0; + + return Enqueue(MakeTask(message), propagateExceptions); + } + + /// + /// Enqueues a task for execution. + /// + /// The task to enqueue. + /// Indicates whether to propagate exceptions. + public bool Enqueue(Task task, bool propagateExceptions) + { + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Enqueueing task {task.Id} on thread {Thread.CurrentThread.ManagedThreadId} for BrighterSynchronizationHelper {Id}"); + Debug.IndentLevel = 0; + + OperationStarted(); + task.ContinueWith(_ => OperationCompleted(), CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, _taskScheduler); + if (_taskQueue.TryAdd(task, propagateExceptions)) + { + _activeTasks.TryAdd(task, 0); + return true; + } + return false; + } + + /// + /// Creates a task from a context message. + /// + /// The context message. + /// The created task. + public Task MakeTask(ContextMessage message) + { + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper:Making task for message {message.Callback.Method.Name} on thread {Thread.CurrentThread.ManagedThreadId} for BrighterSynchronizationHelper {Id}"); + Debug.IndentLevel = 0; + + return _taskFactory.StartNew( + () => message.Callback(message.State), + _taskFactory.CancellationToken, + _taskFactory.CreationOptions | TaskCreationOptions.DenyChildAttach, + _taskScheduler); + } + + /// + /// Notifies that an operation has completed. + /// + public void OperationCompleted() + { + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Operation completed on thread {Thread.CurrentThread.ManagedThreadId} for BrighterSynchronizationHelper {Id}"); + + var newCount = Interlocked.Decrement(ref _outstandingOperations); + Debug.WriteLine($"BrighterSynchronizationHelper: Outstanding operations: {newCount}"); + Debug.IndentLevel = 0; + + if (newCount == 0) + _taskQueue.CompleteAdding(); + } + + /// + /// Notifies that an operation has started. + /// + public void OperationStarted() + { + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Operation started on thread {Thread.CurrentThread.ManagedThreadId} for BrighterSynchronizationHelper {Id}"); + + var newCount = Interlocked.Increment(ref _outstandingOperations); + Debug.WriteLine($"BrighterSynchronizationHelper: Outstanding operations: {newCount}"); + Debug.IndentLevel = 0; + } + + /// + /// Runs a void method and returns after all continuations have run. + /// Propagates exceptions. + /// + /// The action to run. + public static void Run(Action action) + { + Debug.WriteLine(string.Empty); + Debug.WriteLine("...................................................................................................................."); + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Running action {action.Method.Name} on thread {Thread.CurrentThread.ManagedThreadId}"); + Debug.IndentLevel = 0; + + if (action == null) + throw new ArgumentNullException(nameof(action)); + + using var synchronizationHelper = new BrighterSynchronizationHelper(); + + var task = synchronizationHelper._taskFactory.StartNew( + action, + synchronizationHelper._taskFactory.CancellationToken, + synchronizationHelper._taskFactory.CreationOptions | TaskCreationOptions.DenyChildAttach, + synchronizationHelper._taskFactory.Scheduler ?? TaskScheduler.Default + ); + + synchronizationHelper.Execute(task); + task.GetAwaiter().GetResult(); + + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Action {action.Method.Name} completed on thread {Thread.CurrentThread.ManagedThreadId}"); + Debug.WriteLine(synchronizationHelper.ActiveTasks.Count()); + Debug.IndentLevel = 0; + Debug.WriteLine("...................................................................................................................."); + } + + + /// + /// Runs a method that returns a result and returns after all continuations have run. + /// Propagates exceptions. + /// + /// The result type of the task. + /// The function to run. + /// The result of the function. + public static TResult Run(Func func) + { + Debug.WriteLine(string.Empty); + Debug.WriteLine("...................................................................................................................."); + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Running function {func.Method.Name} on thread {Thread.CurrentThread.ManagedThreadId}"); + Debug.IndentLevel = 0; + + if (func == null) + throw new ArgumentNullException(nameof(func)); + + using var synchronizationHelper = new BrighterSynchronizationHelper(); + + var task = synchronizationHelper._taskFactory.StartNew( + func, + synchronizationHelper._taskFactory.CancellationToken, + synchronizationHelper._taskFactory.CreationOptions | TaskCreationOptions.DenyChildAttach, + synchronizationHelper._taskFactory.Scheduler ?? TaskScheduler.Default + ); + + synchronizationHelper.Execute(task); + + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Function {func.Method.Name} completed on thread {Thread.CurrentThread.ManagedThreadId}"); + Debug.WriteLine($"BrighterSynchronizationHelper: Active task count: {synchronizationHelper.ActiveTasks.Count()}"); + Debug.WriteLine($"BrighterSynchronizationHelper: Task Status: {task.Status}"); + Debug.IndentLevel = 0; + Debug.WriteLine("...................................................................................................................."); + + return task.GetAwaiter().GetResult(); + + } + + /// + /// Runs an async void method and returns after all continuations have run. + /// Propagates exceptions. + /// + /// The async function to run. + public static void Run(Func func) + { + Debug.WriteLine(string.Empty); + Debug.WriteLine("...................................................................................................................."); + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Running function {func.Method.Name} on thread {Thread.CurrentThread.ManagedThreadId}"); + Debug.IndentLevel = 0; + + if (func == null) + throw new ArgumentNullException(nameof(func)); + + using var synchronizationHelper = new BrighterSynchronizationHelper(); + + synchronizationHelper.OperationStarted(); + + var task = synchronizationHelper._taskFactory.StartNew( + func, + synchronizationHelper._taskFactory.CancellationToken, + synchronizationHelper._taskFactory.CreationOptions | TaskCreationOptions.DenyChildAttach, + synchronizationHelper._taskFactory.Scheduler ?? TaskScheduler.Default + ) + .Unwrap() + .ContinueWith(t => + { + synchronizationHelper.OperationCompleted(); + t.GetAwaiter().GetResult(); + }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, synchronizationHelper._taskScheduler); + + synchronizationHelper.Execute(task); + task.GetAwaiter().GetResult(); + + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Function {func.Method.Name} completed on thread {Thread.CurrentThread.ManagedThreadId}"); + Debug.WriteLine($"BrighterSynchronizationHelper: Active task count: {synchronizationHelper.ActiveTasks.Count()}"); + Debug.WriteLine($"BrighterSynchronizationHelper: Task Status: {task.Status}"); + Debug.IndentLevel = 0; + Debug.WriteLine("...................................................................................................................."); + } + + /// + /// Queues a task for execution and begins executing all tasks in the queue. + /// Returns the result of the task proxy. + /// Propagates exceptions. + /// + /// The result type of the task. + /// The async function to execute. + /// The result of the function. + public static TResult Run(Func> func) + { + Debug.WriteLine(string.Empty); + Debug.WriteLine("...................................................................................................................."); + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Running function {func.Method.Name} on thread {Thread.CurrentThread.ManagedThreadId}"); + Debug.IndentLevel = 0; + + if (func == null) + throw new ArgumentNullException(nameof(func)); + + using var synchronizationHelper = new BrighterSynchronizationHelper(); + + var task = synchronizationHelper._taskFactory.StartNew( + func, + synchronizationHelper._taskFactory.CancellationToken, + synchronizationHelper._taskFactory.CreationOptions | TaskCreationOptions.DenyChildAttach, + synchronizationHelper._taskFactory.Scheduler ?? TaskScheduler.Default + ) + .Unwrap() + .ContinueWith(t => + { + synchronizationHelper.OperationCompleted(); + return t.GetAwaiter().GetResult(); + }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, synchronizationHelper._taskScheduler); + + synchronizationHelper.Execute(task); + + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Function {func.Method.Name} completed on thread {Thread.CurrentThread.ManagedThreadId}"); + Debug.WriteLine($"BrighterSynchronizationHelper: Active task count: {synchronizationHelper.ActiveTasks.Count()}"); + Debug.WriteLine($"BrighterSynchronizationHelper: Task Status: {task.Status}"); + Debug.IndentLevel = 0; + Debug.WriteLine("...................................................................................................................."); + + return task.GetAwaiter().GetResult(); + } + + /// + /// Executes the specified parent task and all tasks in the queue. + /// + /// The parent task to execute. + public void Execute(Task parentTask) + { + Debug.WriteLine(string.Empty); + Debug.WriteLine(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Executing tasks on thread {Thread.CurrentThread.ManagedThreadId} for BrighterSynchronizationHelper {Id}"); + Debug.IndentLevel = 0; + + BrighterSynchronizationContextScope.ApplyContext(_synchronizationContext, parentTask, () => + { + + foreach (var (task, propagateExceptions) in _taskQueue.GetConsumingEnumerable()) + { + _taskScheduler.DoTryExecuteTask(task); + + if (!propagateExceptions) continue; + + task.GetAwaiter().GetResult(); + _activeTasks.TryRemove(task, out _); + } + }); + + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Execution completed on thread {Thread.CurrentThread.ManagedThreadId} for BrighterSynchronizationHelper {Id}"); + Debug.IndentLevel = 0; + Debug.WriteLine("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); + } + + /// + /// Executes a task immediately on the current thread. + /// + /// The task to execute. + /// The state object to pass to the task. + public void ExecuteImmediately(ContextCallback callback, object? state) + { + Debug.WriteLine(string.Empty); + Debug.WriteLine("++++++++++++++++++++++++++++++++++++++++++++++++++++++"); + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Executing task immediately on thread {Thread.CurrentThread.ManagedThreadId} for BrighterSynchronizationHelper {Id}"); + Debug.WriteLine($"BrighterSynchronizationHelper: Task {callback.Method.Name}"); + Debug.IndentLevel = 0; + + try + { + callback.Invoke(state); + } + catch (Exception e) + { + Debug.WriteLine($"BrighterSynchronizationHelper: Execution errored on thread {Thread.CurrentThread.ManagedThreadId} for BrighterSynchronizationHelper {Id} with exception {e.Message}"); + } + + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Execution completed on thread {Thread.CurrentThread.ManagedThreadId} for BrighterSynchronizationHelper {Id}"); + Debug.IndentLevel = 0; + Debug.WriteLine("++++++++++++++++++++++++++++++++++++++++++++++++++++++"); + + } + + /// + /// Executes a task on the specified execution context. + /// + /// The execution context. + /// The context callback to execute. + /// The state object to pass to the callback. + public void ExecuteOnContext(ExecutionContext ctxt, ContextCallback contextCallback, object? state) + { + Debug.WriteLine(string.Empty); + Debug.WriteLine("++++++++++++++++++++++++++++++++++++++++++++++++++++++"); + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Executing task immediately on original execution context for BrighterSynchronizationHelper {Id}"); + Debug.IndentLevel = 0; + + ExecutionContext.Run(ctxt, contextCallback, state); + + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterSynchronizationHelper: Execution completed on original execution context for BrighterSynchronizationHelper {Id}"); + Debug.IndentLevel = 0; + Debug.WriteLine("++++++++++++++++++++++++++++++++++++++++++++++++++++++"); + + } + + /// + /// Gets an enumerable of the tasks currently scheduled. + /// + /// An enumerable of the scheduled tasks. + public IEnumerable GetScheduledTasks() + { + return _taskQueue.GetScheduledTasks(); + } +} diff --git a/src/Paramore.Brighter/Tasks/BrighterTaskQueue.cs b/src/Paramore.Brighter/Tasks/BrighterTaskQueue.cs new file mode 100644 index 0000000000..b731f766c1 --- /dev/null +++ b/src/Paramore.Brighter/Tasks/BrighterTaskQueue.cs @@ -0,0 +1,97 @@ +#region Sources + +// This class is based on Stephen Cleary's AyncContext in https://github.com/StephenCleary/AsyncEx +// The original code is licensed under the MIT License (MIT) The task to be added. + /// Indicates whether to propagate exceptions. + /// True if the task was added successfully; otherwise, false. + public bool TryAdd(Task item, bool propagateExceptions) + { + try + { + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterTaskQueue; Adding task: {item.Id} to queue"); + Debug.IndentLevel = 0; + + return _queue.TryAdd(Tuple.Create(item, propagateExceptions)); + } + catch (InvalidOperationException) + { + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterTaskQueue; TaskQueue is already marked as complete for adding. Failed to add task: {item.Id}"); + Debug.IndentLevel = 0; + + return false; + } + } + + /// + /// Marks the queue as not accepting any more additions. + /// + public void CompleteAdding() + { + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterTaskQueue; Complete adding to queue"); + Debug.IndentLevel = 0; + + _queue.CompleteAdding(); + } + + /// + /// Disposes the task queue and releases all resources. + /// + public void Dispose() + { + _queue.Dispose(); + } +} diff --git a/src/Paramore.Brighter/Tasks/BrighterTaskScheduler.cs b/src/Paramore.Brighter/Tasks/BrighterTaskScheduler.cs new file mode 100644 index 0000000000..58ae28df90 --- /dev/null +++ b/src/Paramore.Brighter/Tasks/BrighterTaskScheduler.cs @@ -0,0 +1,106 @@ +#region Sources + +// This class is based on Stephen Cleary's AyncContext in https://github.com/StephenCleary/AsyncEx +// The original code is licensed under the MIT License (MIT) class. + /// + /// The synchronizationHelper in which tasks should be executed. + public BrighterTaskScheduler(BrighterSynchronizationHelper synchronizationHelper) + { + _synchronizationHelper = synchronizationHelper; + } + + /// + /// Gets an enumerable of the tasks currently scheduled. + /// + /// An enumerable of the scheduled tasks. + protected override IEnumerable GetScheduledTasks() + { + return _synchronizationHelper.GetScheduledTasks(); + } + + /// + /// Queues a task to the scheduler. + /// + /// The task to be queued. + protected override void QueueTask(Task task) + { + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterTaskScheduler: QueueTask on thread {System.Threading.Thread.CurrentThread.ManagedThreadId}"); + Debug.IndentLevel = 0; + + var queued = _synchronizationHelper.Enqueue((Task)task, false); + if (!queued) + { + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterTaskScheduler: QueueTask Failed to queue task {task.ToString()} on {System.Threading.Thread.CurrentThread.ManagedThreadId}"); + Debug.IndentLevel = 0; + } + } + + /// + /// Attempts to execute the specified task on the current thread. + /// + /// The task to be executed. + /// A boolean indicating whether the task was previously queued. + /// True if the task was executed; otherwise, false. + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterTaskScheduler: TryExecuteTaskInline on thread {System.Threading.Thread.CurrentThread.ManagedThreadId}"); + Debug.IndentLevel = 0; + + return (BrighterSynchronizationHelper.Current == _synchronizationHelper) && TryExecuteTask(task); + + } + + /// + /// Gets the maximum concurrency level supported by this scheduler. + /// + public override int MaximumConcurrencyLevel + { + get { return 1; } + } + + /// + /// Attempts to execute the specified task. + /// + /// The task to be executed. + public void DoTryExecuteTask(Task task) + { + Debug.IndentLevel = 1; + Debug.WriteLine($"BrighterTaskScheduler: DoTryExecuteTask on thread {System.Threading.Thread.CurrentThread.ManagedThreadId}"); + Debug.IndentLevel = 0; + + TryExecuteTask(task); + } +} diff --git a/src/Paramore.Brighter/Tasks/ContextMessage.cs b/src/Paramore.Brighter/Tasks/ContextMessage.cs new file mode 100644 index 0000000000..038648bdff --- /dev/null +++ b/src/Paramore.Brighter/Tasks/ContextMessage.cs @@ -0,0 +1,26 @@ +using System.Threading; + +namespace Paramore.Brighter.Tasks; + +/// +/// Represents a context message containing a callback and state. +/// +public struct ContextMessage +{ + public readonly SendOrPostCallback Callback; + public readonly ExecutionContext? Context; + public readonly object? State; + + /// + /// Initializes a new instance of the struct. + /// + /// The callback to execute. + /// The state to pass to the callback. + /// The execution context, mainly intended for debugging purposes + public ContextMessage(SendOrPostCallback callback, object? state, ExecutionContext? ctxt) + { + Callback = callback; + State = state; + Context = ctxt; + } +} diff --git a/src/Paramore.Brighter/Tasks/SingleDisposable.cs b/src/Paramore.Brighter/Tasks/SingleDisposable.cs new file mode 100644 index 0000000000..9273e47312 --- /dev/null +++ b/src/Paramore.Brighter/Tasks/SingleDisposable.cs @@ -0,0 +1,50 @@ +#region Sources +//copy of Stephen Cleary's Nito Disposables SingleDisposable +//see https://github.com/StephenCleary/Disposables/blob/main/src/Nito.Disposables/SingleDisposable.cs +#endregion + +using System; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Tasks; + +internal abstract class SingleDisposable : IDisposable +{ + private readonly BoundActionField _context; + + private readonly TaskCompletionSource _tcs = new TaskCompletionSource(); + + protected SingleDisposable(T context) + { + _context = new BoundActionField(Dispose, context); + } + + private bool IsDisposeStarted => _context.IsEmpty; + + private bool IsDisposed => _tcs.Task.IsCompleted; + + public bool IsDisposing => IsDisposeStarted && !IsDisposed; + + protected abstract void Dispose(T context); + + public void Dispose() + { + var context = _context.TryGetAndUnset(); + if (context == null) + { + _tcs.Task.GetAwaiter().GetResult(); + return; + } + + try + { + context.Invoke(); + } + finally + { + _tcs.TrySetResult(null!); + } + } + + protected bool TryUpdateContext(Func contextUpdater) => _context.TryUpdateContext(contextUpdater); +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages.cs index 33ce5108a2..26ffbd852d 100644 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages.cs +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages.cs @@ -14,15 +14,15 @@ namespace Paramore.Brighter.AWS.Tests.MessagingGateway { [Trait("Category", "AWS")] [Trait("Fragile", "CI")] - public class SQSBufferedConsumerTests : IDisposable + public class SQSBufferedConsumerTests : IDisposable, IAsyncDisposable { private readonly SqsMessageProducer _messageProducer; private readonly SqsMessageConsumer _consumer; private readonly string _topicName; private readonly ChannelFactory _channelFactory; - private const string _contentType = "text\\plain"; - private const int _bufferSize = 3; - private const int _messageCount = 4; + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private const int MessageCount = 4; public SQSBufferedConsumerTests() { @@ -36,17 +36,17 @@ public SQSBufferedConsumerTests() //we need the channel to create the queues and notifications var routingKey = new RoutingKey(_topicName); - var channel = _channelFactory.CreateChannel(new SqsSubscription( + var channel = _channelFactory.CreateSyncChannel(new SqsSubscription( name: new SubscriptionName(channelName), channelName:new ChannelName(channelName), routingKey:routingKey, - bufferSize: _bufferSize, + 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(), routingKey, _bufferSize); + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(), BufferSize); _messageProducer = new SqsMessageProducer(awsConnection, new SnsPublication { @@ -61,25 +61,25 @@ public async Task When_a_message_consumer_reads_multiple_messages() var messageOne = new Message( new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, - correlationId: Guid.NewGuid().ToString(), contentType: _contentType), + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), new MessageBody("test content one") ); var messageTwo= new Message( new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, - correlationId: Guid.NewGuid().ToString(), contentType: _contentType), + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), new MessageBody("test content two") ); var messageThree= new Message( new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, - correlationId: Guid.NewGuid().ToString(), contentType: _contentType), + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), new MessageBody("test content three") ); var messageFour= new Message( new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, - correlationId: Guid.NewGuid().ToString(), contentType: _contentType), + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), new MessageBody("test content four") ); @@ -96,7 +96,7 @@ public async Task When_a_message_consumer_reads_multiple_messages() do { iteration++; - var outstandingMessageCount = _messageCount - messagesReceivedCount; + var outstandingMessageCount = MessageCount - messagesReceivedCount; //retrieve messages var messages = _consumer.Receive(TimeSpan.FromMilliseconds(10000)); @@ -104,7 +104,7 @@ public async Task When_a_message_consumer_reads_multiple_messages() messages.Length.Should().BeLessOrEqualTo(outstandingMessageCount); //should not receive more than buffer in one hit - messages.Length.Should().BeLessOrEqualTo(_bufferSize); + messages.Length.Should().BeLessOrEqualTo(BufferSize); var moreMessages = messages.Where(m => m.Header.MessageType == MessageType.MT_COMMAND); foreach (var message in moreMessages) @@ -117,7 +117,7 @@ public async Task When_a_message_consumer_reads_multiple_messages() await Task.Delay(1000); - } while ((iteration <= 5) && (messagesReceivedCount < _messageCount)); + } while ((iteration <= 5) && (messagesReceivedCount < MessageCount)); messagesReceivedCount.Should().Be(4); @@ -127,8 +127,16 @@ public async Task When_a_message_consumer_reads_multiple_messages() public void Dispose() { //Clean up resources that we have created - _channelFactory.DeleteTopic(); - _channelFactory.DeleteQueue(); + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageProducerAsync) _messageProducer).DisposeAsync(); } } } diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages_async.cs new file mode 100644 index 0000000000..3e06a5ecbe --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages_async.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway +{ + [Trait("Category", "AWS")] + [Trait("Fragile", "CI")] + public class SQSBufferedConsumerTestsAsync : IDisposable, IAsyncDisposable + { + private readonly SqsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _topicName; + private readonly ChannelFactory _channelFactory; + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private const int MessageCount = 4; + + public SQSBufferedConsumerTestsAsync() + { + (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + var awsConnection = new AWSMessagingGatewayConnection(credentials, region); + + _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.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 SqsMessageProducer(awsConnection, + new SnsPublication { MakeChannels = OnMissingChannel.Create }); + } + + [Fact] + public async Task When_a_message_consumer_reads_multiple_messages_async() + { + var routingKey = new RoutingKey(_topicName); + + var messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + ); + + var messageTwo = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content two") + ); + + var messageThree = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content three") + ); + + var messageFour = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content four") + ); + + //send MESSAGE_COUNT messages + await _messageProducer.SendAsync(messageOne); + await _messageProducer.SendAsync(messageTwo); + await _messageProducer.SendAsync(messageThree); + await _messageProducer.SendAsync(messageFour); + + int iteration = 0; + var messagesReceived = new List(); + var messagesReceivedCount = messagesReceived.Count; + do + { + iteration++; + var outstandingMessageCount = MessageCount - messagesReceivedCount; + + //retrieve messages + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(10000)); + + messages.Length.Should().BeLessOrEqualTo(outstandingMessageCount); + + //should not receive more than buffer in one hit + messages.Length.Should().BeLessOrEqualTo(BufferSize); + + var moreMessages = messages.Where(m => m.Header.MessageType == MessageType.MT_COMMAND); + foreach (var message in moreMessages) + { + messagesReceived.Add(message); + await _consumer.AcknowledgeAsync(message); + } + + messagesReceivedCount = messagesReceived.Count; + + await Task.Delay(1000); + + } while ((iteration <= 5) && (messagesReceivedCount < MessageCount)); + + messagesReceivedCount.Should().Be(4); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _messageProducer.DisposeAsync(); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().GetAwaiter().GetResult(); + _channelFactory.DeleteQueueAsync().GetAwaiter().GetResult(); + _messageProducer.DisposeAsync().GetAwaiter().GetResult(); + } + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_customising_aws_client_config.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_customising_aws_client_config.cs index 83daa8c6d7..2ab69a3fd0 100644 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_customising_aws_client_config.cs +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_customising_aws_client_config.cs @@ -12,10 +12,10 @@ namespace Paramore.Brighter.AWS.Tests.MessagingGateway { [Trait("Category", "AWS")] - public class CustomisingAwsClientConfigTests : IDisposable + public class CustomisingAwsClientConfigTests : IDisposable, IAsyncDisposable { private readonly Message _message; - private readonly IAmAChannel _channel; + private readonly IAmAChannelSync _channel; private readonly SqsMessageProducer _messageProducer; private readonly ChannelFactory _channelFactory; @@ -35,6 +35,7 @@ public CustomisingAwsClientConfigTests() SqsSubscription subscription = new( name: new SubscriptionName(channelName), channelName: new ChannelName(channelName), + messagePumpType: MessagePumpType.Reactor, routingKey: routingKey ); @@ -51,7 +52,7 @@ public CustomisingAwsClientConfigTests() }); _channelFactory = new ChannelFactory(subscribeAwsConnection); - _channel = _channelFactory.CreateChannel(subscription); + _channel = _channelFactory.CreateSyncChannel(subscription); var publishAwsConnection = new AWSMessagingGatewayConnection(credentials, region, config => { @@ -81,9 +82,15 @@ public async Task When_customising_aws_client_config() public void Dispose() { - _channelFactory?.DeleteTopic(); - _channelFactory?.DeleteQueue(); - _messageProducer?.Dispose(); + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); } } } diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_customising_aws_client_config_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_customising_aws_client_config_async.cs new file mode 100644 index 0000000000..5f7be4ca28 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_customising_aws_client_config_async.cs @@ -0,0 +1,96 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway +{ + [Trait("Category", "AWS")] + public class CustomisingAwsClientConfigTestsAsync : IDisposable, IAsyncDisposable + { + private readonly Message _message; + private readonly IAmAChannelAsync _channel; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + + private readonly InterceptingDelegatingHandler _publishHttpHandler = new(); + private readonly InterceptingDelegatingHandler _subscribeHttpHandler = new(); + + public CustomisingAwsClientConfigTestsAsync() + { + MyCommand myCommand = new() {Value = "Test"}; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + messagePumpType: MessagePumpType.Proactor, + routingKey: routingKey + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object) myCommand, JsonSerialisationOptions.Options)) + ); + + (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + var subscribeAwsConnection = new AWSMessagingGatewayConnection(credentials, region, config => + { + config.HttpClientFactory = new InterceptingHttpClientFactory(_subscribeHttpHandler); + }); + + _channelFactory = new ChannelFactory(subscribeAwsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + + var publishAwsConnection = new AWSMessagingGatewayConnection(credentials, region, config => + { + config.HttpClientFactory = new InterceptingHttpClientFactory(_publishHttpHandler); + }); + + _messageProducer = new SqsMessageProducer(publishAwsConnection, new SnsPublication{Topic = new RoutingKey(topicName), MakeChannels = OnMissingChannel.Create}); + } + + [Fact] + public async Task When_customising_aws_client_config() + { + //arrange + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var message =await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + //clear the queue + await _channel.AcknowledgeAsync(message); + + //publish_and_subscribe_should_use_custom_http_client_factory + _publishHttpHandler.RequestCount.Should().BeGreaterThan(0); + _subscribeHttpHandler.RequestCount.Should().BeGreaterThan(0); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_assume.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_assume.cs index 0ad39dc054..e0a5f37df0 100644 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_assume.cs +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_assume.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Text.Json; +using System.Threading.Tasks; using Amazon; using Amazon.Runtime; using FluentAssertions; @@ -13,7 +14,7 @@ namespace Paramore.Brighter.AWS.Tests.MessagingGateway { [Trait("Category", "AWS")] [Trait("Fragile", "CI")] - public class AWSAssumeInfrastructureTests : IDisposable + public class AWSAssumeInfrastructureTests : IDisposable, IAsyncDisposable { private readonly Message _message; private readonly SqsMessageConsumer _consumer; private readonly SqsMessageProducer _messageProducer; @@ -34,6 +35,7 @@ public AWSAssumeInfrastructureTests() name: new SubscriptionName(channelName), channelName: new ChannelName(channelName), routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, makeChannels: OnMissingChannel.Create ); @@ -51,19 +53,20 @@ public AWSAssumeInfrastructureTests() //This doesn't look that different from our create tests - this is because we create using the channel factory in //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) _channelFactory = new ChannelFactory(awsConnection); - var channel = _channelFactory.CreateChannel(subscription); + var channel = _channelFactory.CreateSyncChannel(subscription); //Now change the subscription to validate, just check what we made subscription = new( name: new SubscriptionName(channelName), channelName: channel.Name, routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, makeChannels: OnMissingChannel.Assume ); _messageProducer = new SqsMessageProducer(awsConnection, new SnsPublication{MakeChannels = OnMissingChannel.Assume}); - _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(), routingKey); + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName()); } [Fact] @@ -84,12 +87,15 @@ public void When_infastructure_exists_can_assume() public void Dispose() { - _channelFactory.DeleteTopic(); - _channelFactory.DeleteQueue(); - _consumer.Dispose(); - _messageProducer.Dispose(); + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); } - - } } diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_assume_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_assume_async.cs new file mode 100644 index 0000000000..e1d5b3d92c --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_assume_async.cs @@ -0,0 +1,100 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway +{ + [Trait("Category", "AWS")] + [Trait("Fragile", "CI")] + public class AWSAssumeInfrastructureTestsAsync : IDisposable, IAsyncDisposable + { private readonly Message _message; + private readonly SqsMessageConsumer _consumer; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSAssumeInfrastructureTestsAsync() + { + _myCommand = new MyCommand{Value = "Test"}; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object) _myCommand, JsonSerialisationOptions.Options)) + ); + + (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + var awsConnection = new AWSMessagingGatewayConnection(credentials, region); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + //Now change the subscription to validate, just check what we made + subscription = new( + name: new SubscriptionName(channelName), + channelName: channel.Name, + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Assume + ); + + _messageProducer = new SqsMessageProducer(awsConnection, new SnsPublication{MakeChannels = OnMissingChannel.Assume}); + + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName()); + } + + [Fact] + public async Task When_infastructure_exists_can_assume() + { + //arrange + await _messageProducer.SendAsync(_message); + + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + await _consumer.AcknowledgeAsync(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify.cs index e4d1cc602d..1cb5504015 100644 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify.cs +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify.cs @@ -14,9 +14,9 @@ namespace Paramore.Brighter.AWS.Tests.MessagingGateway { [Trait("Category", "AWS")] [Trait("Fragile", "CI")] - public class AWSValidateInfrastructureTests : IDisposable + public class AWSValidateInfrastructureTests : IDisposable, IAsyncDisposable { private readonly Message _message; - private readonly IAmAMessageConsumer _consumer; + private readonly IAmAMessageConsumerSync _consumer; private readonly SqsMessageProducer _messageProducer; private readonly ChannelFactory _channelFactory; private readonly MyCommand _myCommand; @@ -35,6 +35,7 @@ public AWSValidateInfrastructureTests() name: new SubscriptionName(channelName), channelName: new ChannelName(channelName), routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, makeChannels: OnMissingChannel.Create ); @@ -52,7 +53,7 @@ public AWSValidateInfrastructureTests() //This doesn't look that different from our create tests - this is because we create using the channel factory in //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) _channelFactory = new ChannelFactory(awsConnection); - var channel = _channelFactory.CreateChannel(subscription); + var channel = _channelFactory.CreateSyncChannel(subscription); //Now change the subscription to validate, just check what we made subscription = new( @@ -60,6 +61,7 @@ public AWSValidateInfrastructureTests() channelName: channel.Name, routingKey: routingKey, findTopicBy: TopicFindBy.Name, + messagePumpType: MessagePumpType.Reactor, makeChannels: OnMissingChannel.Validate ); @@ -96,10 +98,19 @@ public async Task When_infrastructure_exists_can_verify() public void Dispose() { - _channelFactory.DeleteTopic(); - _channelFactory.DeleteQueue(); + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); _consumer.Dispose(); _messageProducer.Dispose(); } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); + await _messageProducer.DisposeAsync(); + } } } diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify_by_arn.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify_by_arn.cs index b27521204a..0235994a8e 100644 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify_by_arn.cs +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify_by_arn.cs @@ -15,9 +15,9 @@ namespace Paramore.Brighter.AWS.Tests.MessagingGateway { [Trait("Category", "AWS")] [Trait("Fragile", "CI")] - public class AWSValidateInfrastructureByArnTests : IDisposable + public class AWSValidateInfrastructureByArnTests : IDisposable, IAsyncDisposable { private readonly Message _message; - private readonly IAmAMessageConsumer _consumer; + private readonly IAmAMessageConsumerSync _consumer; private readonly SqsMessageProducer _messageProducer; private readonly ChannelFactory _channelFactory; private readonly MyCommand _myCommand; @@ -35,6 +35,7 @@ public AWSValidateInfrastructureByArnTests() name: new SubscriptionName(channelName), channelName: new ChannelName(channelName), routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, makeChannels: OnMissingChannel.Create ); @@ -52,7 +53,7 @@ public AWSValidateInfrastructureByArnTests() //This doesn't look that different from our create tests - this is because we create using the channel factory in //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) _channelFactory = new ChannelFactory(awsConnection); - var channel = _channelFactory.CreateChannel(subscription); + var channel = _channelFactory.CreateSyncChannel(subscription); var topicArn = FindTopicArn(credentials, region, routingKey.Value); var routingKeyArn = new RoutingKey(topicArn); @@ -63,6 +64,7 @@ public AWSValidateInfrastructureByArnTests() channelName: channel.Name, routingKey: routingKeyArn, findTopicBy: TopicFindBy.Arn, + messagePumpType: MessagePumpType.Reactor, makeChannels: OnMissingChannel.Validate ); @@ -99,11 +101,20 @@ public async Task When_infrastructure_exists_can_verify() public void Dispose() { - _channelFactory.DeleteTopic(); - _channelFactory.DeleteQueue(); + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); _consumer.Dispose(); _messageProducer.Dispose(); } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); + await _messageProducer.DisposeAsync(); + } private string FindTopicArn(AWSCredentials credentials, RegionEndpoint region, string topicName) { diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify_by_convention.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify_by_convention.cs index 22b003d0fe..3f3a86ed27 100644 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify_by_convention.cs +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify_by_convention.cs @@ -14,9 +14,9 @@ namespace Paramore.Brighter.AWS.Tests.MessagingGateway { [Trait("Category", "AWS")] [Trait("Fragile", "CI")] - public class AWSValidateInfrastructureByConventionTests : IDisposable + public class AWSValidateInfrastructureByConventionTests : IDisposable, IAsyncDisposable { private readonly Message _message; - private readonly IAmAMessageConsumer _consumer; + private readonly IAmAMessageConsumerSync _consumer; private readonly SqsMessageProducer _messageProducer; private readonly ChannelFactory _channelFactory; private readonly MyCommand _myCommand; @@ -35,6 +35,7 @@ public AWSValidateInfrastructureByConventionTests() name: new SubscriptionName(channelName), channelName: new ChannelName(channelName), routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, makeChannels: OnMissingChannel.Create ); @@ -52,7 +53,7 @@ public AWSValidateInfrastructureByConventionTests() //This doesn't look that different from our create tests - this is because we create using the channel factory in //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) _channelFactory = new ChannelFactory(awsConnection); - var channel = _channelFactory.CreateChannel(subscription); + var channel = _channelFactory.CreateSyncChannel(subscription); //Now change the subscription to validate, just check what we made - will make the SNS Arn to prevent ListTopics call subscription = new( @@ -60,6 +61,7 @@ public AWSValidateInfrastructureByConventionTests() channelName: channel.Name, routingKey: routingKey, findTopicBy: TopicFindBy.Convention, + messagePumpType: MessagePumpType.Reactor, makeChannels: OnMissingChannel.Validate ); @@ -94,12 +96,20 @@ public async Task When_infrastructure_exists_can_verify() public void Dispose() { - _channelFactory.DeleteTopic(); - _channelFactory.DeleteQueue(); + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); _consumer.Dispose(); _messageProducer.Dispose(); } - + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); + await _messageProducer.DisposeAsync(); + } } } diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_async.cs new file mode 100644 index 0000000000..fef735052b --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_async.cs @@ -0,0 +1,109 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway +{ + [Trait("Category", "AWS")] + [Trait("Fragile", "CI")] + public class AWSValidateInfrastructureTestsAsync : IDisposable, IAsyncDisposable + { + private readonly Message _message; + private readonly IAmAMessageConsumerAsync _consumer; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + var awsConnection = new AWSMessagingGatewayConnection(credentials, region); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + subscription = new( + name: new SubscriptionName(channelName), + channelName: channel.Name, + routingKey: routingKey, + findTopicBy: TopicFindBy.Name, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Validate + ); + + _messageProducer = new SqsMessageProducer( + awsConnection, + new SnsPublication + { + FindTopicBy = TopicFindBy.Name, + MakeChannels = OnMissingChannel.Validate, + Topic = new RoutingKey(topicName) + } + ); + + _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify_async() + { + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + await _consumer.AcknowledgeAsync(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + ((IAmAMessageConsumerSync)_consumer).Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _consumer.DisposeAsync(); + await _messageProducer.DisposeAsync(); + } + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_by_arn_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_by_arn_async.cs new file mode 100644 index 0000000000..fcec7e29a2 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_by_arn_async.cs @@ -0,0 +1,118 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using Amazon.SimpleNotificationService; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway +{ + [Trait("Category", "AWS")] + [Trait("Fragile", "CI")] + public class AWSValidateInfrastructureByArnTestsAsync : IAsyncDisposable, IDisposable + { + private readonly Message _message; + private readonly IAmAMessageConsumerAsync _consumer; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureByArnTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey($"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45)); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + var awsConnection = new AWSMessagingGatewayConnection(credentials, region); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + var topicArn = FindTopicArn(credentials, region, routingKey.Value).Result; + var routingKeyArn = new RoutingKey(topicArn); + + subscription = new( + name: new SubscriptionName(channelName), + channelName: channel.Name, + routingKey: routingKeyArn, + findTopicBy: TopicFindBy.Arn, + makeChannels: OnMissingChannel.Validate + ); + + _messageProducer = new SqsMessageProducer( + awsConnection, + new SnsPublication + { + Topic = routingKey, + TopicArn = topicArn, + FindTopicBy = TopicFindBy.Arn, + MakeChannels = OnMissingChannel.Validate + }); + + _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify_async() + { + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + await _consumer.AcknowledgeAsync(message); + } + + private async Task FindTopicArn(AWSCredentials credentials, RegionEndpoint region, string topicName) + { + var snsClient = new AmazonSimpleNotificationServiceClient(credentials, region); + var topicResponse = await snsClient.FindTopicAsync(topicName); + return topicResponse.TopicArn; + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + ((IAmAMessageConsumerSync)_consumer).Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _consumer.DisposeAsync(); + await _messageProducer.DisposeAsync(); + } + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_by_convention.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_by_convention.cs new file mode 100644 index 0000000000..2d07a58097 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_by_convention.cs @@ -0,0 +1,108 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway +{ + [Trait("Category", "AWS")] + [Trait("Fragile", "CI")] + public class AWSValidateInfrastructureByConventionTestsAsync : IAsyncDisposable, IDisposable + { + private readonly Message _message; + private readonly IAmAMessageConsumerAsync _consumer; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureByConventionTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + var awsConnection = new AWSMessagingGatewayConnection(credentials, region); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + subscription = new( + name: new SubscriptionName(channelName), + channelName: channel.Name, + routingKey: routingKey, + findTopicBy: TopicFindBy.Convention, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Validate + ); + + _messageProducer = new SqsMessageProducer( + awsConnection, + new SnsPublication + { + FindTopicBy = TopicFindBy.Convention, + MakeChannels = OnMissingChannel.Validate + } + ); + + _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify_async() + { + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + await _consumer.AcknowledgeAsync(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + ((IAmAMessageConsumerSync)_consumer).Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _consumer.DisposeAsync(); + await _messageProducer.DisposeAsync(); + } + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway.cs index 7e103c136a..b23f62cc1b 100644 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway.cs +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway.cs @@ -12,10 +12,10 @@ namespace Paramore.Brighter.AWS.Tests.MessagingGateway { [Trait("Category", "AWS")] - public class SqsMessageProducerSendTests : IDisposable + public class SqsMessageProducerSendTests : IDisposable, IAsyncDisposable { private readonly Message _message; - private readonly IAmAChannel _channel; + private readonly IAmAChannelSync _channel; private readonly SqsMessageProducer _messageProducer; private readonly ChannelFactory _channelFactory; private readonly MyCommand _myCommand; @@ -38,6 +38,7 @@ public SqsMessageProducerSendTests() name: new SubscriptionName(channelName), channelName: new ChannelName(channelName), routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, rawMessageDelivery: false ); @@ -52,30 +53,19 @@ public SqsMessageProducerSendTests() var awsConnection = new AWSMessagingGatewayConnection(credentials, region); _channelFactory = new ChannelFactory(awsConnection); - _channel = _channelFactory.CreateChannel(subscription); + _channel = _channelFactory.CreateSyncChannel(subscription); _messageProducer = new SqsMessageProducer(awsConnection, new SnsPublication{Topic = new RoutingKey(_topicName), MakeChannels = OnMissingChannel.Create}); } - [Theory] - [InlineData("test subject", true)] - [InlineData(null, true)] - [InlineData("test subject", false)] - [InlineData(null, false)] - public async Task When_posting_a_message_via_the_producer(string subject, bool sendAsync) + [Fact] + public async Task When_posting_a_message_via_the_producer() { //arrange - _message.Header.Subject = subject; - if (sendAsync) - { - await _messageProducer.SendAsync(_message); - } - else - { - _messageProducer.Send(_message); - } + _message.Header.Subject = "test subject"; + _messageProducer.Send(_message); await Task.Delay(1000); @@ -95,7 +85,7 @@ public async Task When_posting_a_message_via_the_producer(string subject, bool s message.Header.ReplyTo.Should().Be(_replyTo); message.Header.ContentType.Should().Be(_contentType); message.Header.HandledCount.Should().Be(0); - message.Header.Subject.Should().Be(subject); + message.Header.Subject.Should().Be(_message.Header.Subject); //allow for clock drift in the following test, more important to have a contemporary timestamp than anything message.Header.TimeStamp.Should().BeAfter(RoundToSeconds(DateTime.UtcNow.AddMinutes(-1))); message.Header.Delayed.Should().Be(TimeSpan.Zero); @@ -105,9 +95,17 @@ public async Task When_posting_a_message_via_the_producer(string subject, bool s public void Dispose() { - _channelFactory?.DeleteTopic(); - _channelFactory?.DeleteQueue(); - _messageProducer?.Dispose(); + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _messageProducer.DisposeAsync(); } private static DateTime RoundToSeconds(DateTime dateTime) diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway_async.cs new file mode 100644 index 0000000000..c25b8f5b88 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway_async.cs @@ -0,0 +1,113 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway +{ + [Trait("Category", "AWS")] + public class SqsMessageProducerSendAsyncTests : IAsyncDisposable, IDisposable + { + private readonly Message _message; + private readonly IAmAChannelAsync _channel; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + private readonly string _correlationId; + private readonly string _replyTo; + private readonly string _contentType; + private readonly string _topicName; + + public SqsMessageProducerSendAsyncTests() + { + _myCommand = new MyCommand { Value = "Test" }; + _correlationId = Guid.NewGuid().ToString(); + _replyTo = "http:\\queueUrl"; + _contentType = "text\\plain"; + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(_topicName); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + rawMessageDelivery: false + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: _correlationId, + replyTo: new RoutingKey(_replyTo), contentType: _contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + var awsConnection = new AWSMessagingGatewayConnection(credentials, region); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + + _messageProducer = new SqsMessageProducer(awsConnection, new SnsPublication { Topic = new RoutingKey(_topicName), MakeChannels = OnMissingChannel.Create }); + } + + [Fact] + public async Task When_posting_a_message_via_the_producer_async() + { + // arrange + _message.Header.Subject = "test subject"; + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + // clear the queue + await _channel.AcknowledgeAsync(message); + + // should_send_the_message_to_aws_sqs + message.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + + message.Id.Should().Be(_myCommand.Id); + message.Redelivered.Should().BeFalse(); + message.Header.MessageId.Should().Be(_myCommand.Id); + message.Header.Topic.Value.Should().Contain(_topicName); + message.Header.CorrelationId.Should().Be(_correlationId); + message.Header.ReplyTo.Should().Be(_replyTo); + message.Header.ContentType.Should().Be(_contentType); + message.Header.HandledCount.Should().Be(0); + message.Header.Subject.Should().Be(_message.Header.Subject); + // allow for clock drift in the following test, more important to have a contemporary timestamp than anything + message.Header.TimeStamp.Should().BeAfter(RoundToSeconds(DateTime.UtcNow.AddMinutes(-1))); + message.Header.Delayed.Should().Be(TimeSpan.Zero); + // {"Id":"cd581ced-c066-4322-aeaf-d40944de8edd","Value":"Test","WasCancelled":false,"TaskCompleted":false} + message.Body.Value.Should().Be(_message.Body.Value); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _messageProducer.DisposeAsync(); + } + + private static DateTime RoundToSeconds(DateTime dateTime) + { + return new DateTime(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond), dateTime.Kind); + } + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_assume_throws.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_assume_throws.cs index 250108d383..54c18811b2 100644 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_assume_throws.cs +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_assume_throws.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Amazon; using Amazon.Runtime; using Amazon.SQS.Model; @@ -10,7 +11,7 @@ namespace Paramore.Brighter.AWS.Tests.MessagingGateway { [Trait("Category", "AWS")] - public class AWSAssumeQueuesTests : IDisposable + public class AWSAssumeQueuesTests : IDisposable, IAsyncDisposable { private readonly ChannelFactory _channelFactory; private readonly SqsMessageConsumer _consumer; @@ -25,6 +26,7 @@ public AWSAssumeQueuesTests() name: new SubscriptionName(channelName), channelName: new ChannelName(channelName), routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, makeChannels: OnMissingChannel.Assume ); @@ -42,10 +44,10 @@ public AWSAssumeQueuesTests() producer.ConfirmTopicExistsAsync(topicName).Wait(); _channelFactory = new ChannelFactory(awsConnection); - var channel = _channelFactory.CreateChannel(subscription); + var channel = _channelFactory.CreateSyncChannel(subscription); //We need to create the topic at least, to check the queues - _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(), routingKey); + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName()); } [Fact] @@ -57,9 +59,13 @@ public void When_queues_missing_assume_throws() public void Dispose() { - _channelFactory.DeleteTopic(); + _channelFactory.DeleteTopicAsync().Wait(); } + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } } } diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_assume_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_assume_throws_async.cs new file mode 100644 index 0000000000..178316874f --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_assume_throws_async.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway +{ + [Trait("Category", "AWS")] + public class AWSAssumeQueuesTestsAsync : IAsyncDisposable, IDisposable + { + private readonly ChannelFactory _channelFactory; + private readonly IAmAMessageConsumerAsync _consumer; + + public AWSAssumeQueuesTestsAsync() + { + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + makeChannels: OnMissingChannel.Assume, + messagePumpType: MessagePumpType.Proactor + ); + + (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + var awsConnection = new AWSMessagingGatewayConnection(credentials, region); + + //create the topic, we want the queue to be the issue + //We need to create the topic at least, to check the queues + var producer = new SqsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create + }); + + producer.ConfirmTopicExistsAsync(topicName).Wait(); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + //We need to create the topic at least, to check the queues + _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); + } + + [Fact] + public async Task When_queues_missing_assume_throws_async() + { + //we will try to get the queue url, and fail because it does not exist + await Assert.ThrowsAsync(async () => await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_verify_throws.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_verify_throws.cs index 3b92e20561..8179fd938f 100644 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_verify_throws.cs +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_verify_throws.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Amazon; using Amazon.Runtime; using Amazon.SQS.Model; @@ -10,7 +11,7 @@ namespace Paramore.Brighter.AWS.Tests.MessagingGateway { [Trait("Category", "AWS")] - public class AWSValidateQueuesTests : IDisposable + public class AWSValidateQueuesTests : IDisposable, IAsyncDisposable { private readonly AWSMessagingGatewayConnection _awsConnection; private readonly SqsSubscription _subscription; @@ -26,6 +27,7 @@ public AWSValidateQueuesTests() name: new SubscriptionName(channelName), channelName: new ChannelName(channelName), routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, makeChannels: OnMissingChannel.Validate ); @@ -48,14 +50,17 @@ public void When_queues_missing_verify_throws() //We have no queues so we should throw //We need to do this manually in a test - will create the channel from subscriber parameters _channelFactory = new ChannelFactory(_awsConnection); - Assert.Throws(() => _channelFactory.CreateChannel(_subscription)); + Assert.Throws(() => _channelFactory.CreateSyncChannel(_subscription)); } public void Dispose() { - _channelFactory.DeleteTopic(); + _channelFactory.DeleteTopicAsync().Wait(); } - + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } } } diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_verify_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_verify_throws_async.cs new file mode 100644 index 0000000000..4fb96ac19c --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_verify_throws_async.cs @@ -0,0 +1,59 @@ +using System; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway +{ + [Trait("Category", "AWS")] + public class AWSValidateQueuesTestsAsync : IAsyncDisposable + { + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private ChannelFactory _channelFactory; + + public AWSValidateQueuesTestsAsync() + { + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + _subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + makeChannels: OnMissingChannel.Validate + ); + + (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + _awsConnection = new AWSMessagingGatewayConnection(credentials, region); + + // We need to create the topic at least, to check the queues + var producer = new SqsMessageProducer(_awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create + }); + producer.ConfirmTopicExistsAsync(topicName).Wait(); + } + + [Fact] + public async Task When_queues_missing_verify_throws_async() + { + // We have no queues so we should throw + // We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(_awsConnection); + await Assert.ThrowsAsync(async () => await _channelFactory.CreateAsyncChannelAsync(_subscription)); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_raw_message_delivery_disabled.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_raw_message_delivery_disabled.cs index f254812e89..0d60c63afb 100644 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_raw_message_delivery_disabled.cs +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_raw_message_delivery_disabled.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Amazon; using Amazon.Runtime; using FluentAssertions; @@ -12,11 +13,11 @@ namespace Paramore.Brighter.AWS.Tests.MessagingGateway { [Trait("Category", "AWS")] [Trait("Fragile", "CI")] - public class SqsRawMessageDeliveryTests : IDisposable + public class SqsRawMessageDeliveryTests : IDisposable, IAsyncDisposable { private readonly SqsMessageProducer _messageProducer; private readonly ChannelFactory _channelFactory; - private readonly IAmAChannel _channel; + private readonly IAmAChannelSync _channel; private readonly RoutingKey _routingKey; public SqsRawMessageDeliveryTests() @@ -31,12 +32,13 @@ public SqsRawMessageDeliveryTests() var bufferSize = 10; //Set rawMessageDelivery to false - _channel = _channelFactory.CreateChannel(new SqsSubscription( + _channel = _channelFactory.CreateSyncChannel(new SqsSubscription( name: new SubscriptionName(channelName), channelName:new ChannelName(channelName), routingKey:_routingKey, bufferSize: bufferSize, makeChannels: OnMissingChannel.Create, + messagePumpType: MessagePumpType.Reactor, rawMessageDelivery: false)); _messageProducer = new SqsMessageProducer(awsConnection, @@ -83,8 +85,14 @@ public void When_raw_message_delivery_disabled() public void Dispose() { - _channelFactory.DeleteTopic(); - _channelFactory.DeleteQueue(); + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); } } } diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_raw_message_delivery_disabled_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_raw_message_delivery_disabled_async.cs new file mode 100644 index 0000000000..ad435b5a46 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_raw_message_delivery_disabled_async.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway +{ + [Trait("Category", "AWS")] + [Trait("Fragile", "CI")] + public class SqsRawMessageDeliveryTestsAsync : IAsyncDisposable, IDisposable + { + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly IAmAChannelAsync _channel; + private readonly RoutingKey _routingKey; + + public SqsRawMessageDeliveryTestsAsync() + { + (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + var awsConnection = new AWSMessagingGatewayConnection(credentials, region); + + _channelFactory = new ChannelFactory(awsConnection); + var channelName = $"Raw-Msg-Delivery-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _routingKey = new RoutingKey($"Raw-Msg-Delivery-Tests-{Guid.NewGuid().ToString()}".Truncate(45)); + + var bufferSize = 10; + + // Set rawMessageDelivery to false + _channel = _channelFactory.CreateAsyncChannel(new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: _routingKey, + bufferSize: bufferSize, + makeChannels: OnMissingChannel.Create, + rawMessageDelivery: false)); + + _messageProducer = new SqsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create + }); + } + + [Fact] + public async Task When_raw_message_delivery_disabled_async() + { + // Arrange + var messageHeader = new MessageHeader( + Guid.NewGuid().ToString(), + _routingKey, + MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), + replyTo: RoutingKey.Empty, + contentType: "text\\plain"); + + var customHeaderItem = new KeyValuePair("custom-header-item", "custom-header-item-value"); + messageHeader.Bag.Add(customHeaderItem.Key, customHeaderItem.Value); + + var messageToSend = new Message(messageHeader, new MessageBody("test content one")); + + // Act + await _messageProducer.SendAsync(messageToSend); + + var messageReceived = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(10000)); + + await _channel.AcknowledgeAsync(messageReceived); + + // Assert + messageReceived.Id.Should().Be(messageToSend.Id); + messageReceived.Header.Topic.Should().Be(messageToSend.Header.Topic); + messageReceived.Header.MessageType.Should().Be(messageToSend.Header.MessageType); + messageReceived.Header.CorrelationId.Should().Be(messageToSend.Header.CorrelationId); + messageReceived.Header.ReplyTo.Should().Be(messageToSend.Header.ReplyTo); + messageReceived.Header.ContentType.Should().Be(messageToSend.Header.ContentType); + messageReceived.Header.Bag.Should().ContainKey(customHeaderItem.Key).And.ContainValue(customHeaderItem.Value); + messageReceived.Body.Value.Should().Be(messageToSend.Body.Value); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_rejecting_a_message_through_gateway_with_requeue.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_rejecting_a_message_through_gateway_with_requeue.cs index 1296cd28f5..644d940496 100644 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_rejecting_a_message_through_gateway_with_requeue.cs +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_rejecting_a_message_through_gateway_with_requeue.cs @@ -16,7 +16,7 @@ namespace Paramore.Brighter.AWS.Tests.MessagingGateway public class SqsMessageConsumerRequeueTests : IDisposable { private readonly Message _message; - private readonly IAmAChannel _channel; + private readonly IAmAChannelSync _channel; private readonly SqsMessageProducer _messageProducer; private readonly ChannelFactory _channelFactory; private readonly MyCommand _myCommand; @@ -34,6 +34,7 @@ public SqsMessageConsumerRequeueTests() SqsSubscription subscription = new( name: new SubscriptionName(channelName), channelName: new ChannelName(channelName), + messagePumpType: MessagePumpType.Reactor, routingKey: routingKey ); @@ -49,7 +50,7 @@ public SqsMessageConsumerRequeueTests() //We need to do this manually in a test - will create the channel from subscriber parameters _channelFactory = new ChannelFactory(awsConnection); - _channel = _channelFactory.CreateChannel(subscription); + _channel = _channelFactory.CreateSyncChannel(subscription); _messageProducer = new SqsMessageProducer(awsConnection, new SnsPublication{MakeChannels = OnMissingChannel.Create}); } @@ -77,9 +78,14 @@ public void When_rejecting_a_message_through_gateway_with_requeue() public void Dispose() { - //Clean up resources that we have created - _channelFactory.DeleteTopic(); - _channelFactory.DeleteQueue(); + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); } } } diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_rejecting_a_message_through_gateway_with_requeue_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_rejecting_a_message_through_gateway_with_requeue_async.cs new file mode 100644 index 0000000000..3af75acf8e --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_rejecting_a_message_through_gateway_with_requeue_async.cs @@ -0,0 +1,90 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway +{ + [Trait("Category", "AWS")] + [Trait("Fragile", "CI")] + public class SqsMessageConsumerRequeueTestsAsync : IDisposable, IAsyncDisposable + { + private readonly Message _message; + private readonly IAmAChannelAsync _channel; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public SqsMessageConsumerRequeueTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + var awsConnection = new AWSMessagingGatewayConnection(credentials, region); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + + _messageProducer = new SqsMessageProducer(awsConnection, new SnsPublication { MakeChannels = OnMissingChannel.Create }); + } + + [Fact] + public async Task When_rejecting_a_message_through_gateway_with_requeue_async() + { + await _messageProducer.SendAsync(_message); + + var message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + await _channel.RejectAsync(message); + + // Let the timeout change + await Task.Delay(TimeSpan.FromMilliseconds(3000)); + + // should requeue_the_message + message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + // clear the queue + await _channel.AcknowledgeAsync(message); + + message.Id.Should().Be(_myCommand.Id); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_a_message.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_a_message.cs index 1c4b3c540e..4f28d6c3db 100644 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_a_message.cs +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_a_message.cs @@ -1,5 +1,6 @@ using System; using System.Text.Json; +using System.Threading.Tasks; using Amazon; using Amazon.Runtime; using Amazon.Runtime.CredentialManagement; @@ -12,12 +13,12 @@ namespace Paramore.Brighter.AWS.Tests.MessagingGateway { [Trait("Category", "AWS")] - public class SqsMessageProducerRequeueTests : IDisposable + public class SqsMessageProducerRequeueTests : IDisposable, IAsyncDisposable { private readonly IAmAMessageProducerSync _sender; private Message _requeuedMessage; private Message _receivedMessage; - private readonly IAmAChannel _channel; + private readonly IAmAChannelSync _channel; private readonly ChannelFactory _channelFactory; private readonly Message _message; @@ -53,7 +54,7 @@ public SqsMessageProducerRequeueTests() //We need to do this manually in a test - will create the channel from subscriber parameters _channelFactory = new ChannelFactory(awsConnection); - _channel = _channelFactory.CreateChannel(subscription); + _channel = _channelFactory.CreateSyncChannel(subscription); } [Fact] @@ -73,8 +74,14 @@ public void When_requeueing_a_message() public void Dispose() { - _channelFactory.DeleteTopic(); - _channelFactory.DeleteQueue(); + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); } } } diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_a_message_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_a_message_async.cs new file mode 100644 index 0000000000..faa37a7128 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_a_message_async.cs @@ -0,0 +1,86 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using Amazon.Runtime.CredentialManagement; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway +{ + [Trait("Category", "AWS")] + public class SqsMessageProducerRequeueTestsAsync : IDisposable, IAsyncDisposable + { + private readonly IAmAMessageProducerAsync _sender; + private Message _requeuedMessage; + private Message _receivedMessage; + private readonly IAmAChannelAsync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + + public SqsMessageProducerRequeueTestsAsync() + { + MyCommand myCommand = new MyCommand { Value = "Test" }; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + new CredentialProfileStoreChain(); + + (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + var awsConnection = new AWSMessagingGatewayConnection(credentials, region); + + _sender = new SqsMessageProducer(awsConnection, new SnsPublication { MakeChannels = OnMissingChannel.Create }); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + } + + [Fact] + public async Task When_requeueing_a_message_async() + { + await _sender.SendAsync(_message); + _receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(_receivedMessage); + + _requeuedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + await _channel.AcknowledgeAsync(_requeuedMessage); + + _requeuedMessage.Body.Value.Should().Be(_receivedMessage.Body.Value); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_redrives_to_the_dlq.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_redrives_to_the_dlq.cs index e15b3cd29d..200cfc9810 100644 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_redrives_to_the_dlq.cs +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_redrives_to_the_dlq.cs @@ -17,10 +17,10 @@ namespace Paramore.Brighter.AWS.Tests.MessagingGateway { [Trait("Category", "AWS")] [Trait("Fragile", "CI")] - public class SqsMessageProducerDlqTests : IDisposable + public class SqsMessageProducerDlqTests : IDisposable, IAsyncDisposable { private readonly SqsMessageProducer _sender; - private readonly IAmAChannel _channel; + private readonly IAmAChannelSync _channel; private readonly ChannelFactory _channelFactory; private readonly Message _message; private readonly AWSMessagingGatewayConnection _awsConnection; @@ -60,7 +60,7 @@ public SqsMessageProducerDlqTests () //We need to do this manually in a test - will create the channel from subscriber parameters _channelFactory = new ChannelFactory(_awsConnection); - _channel = _channelFactory.CreateChannel(subscription); + _channel = _channelFactory.CreateSyncChannel(subscription); } [Fact] @@ -83,12 +83,6 @@ public void When_requeueing_redrives_to_the_queue() GetDLQCount(_dlqChannelName).Should().Be(1); } - public void Dispose() - { - _channelFactory.DeleteTopic(); - _channelFactory.DeleteQueue(); - } - public int GetDLQCount(string queueName) { using var sqsClient = new AmazonSQSClient(_awsConnection.Credentials, _awsConnection.Region); @@ -107,6 +101,18 @@ public int GetDLQCount(string queueName) return response.Messages.Count; } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } } } diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_redrives_to_the_dlq_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_redrives_to_the_dlq_async.cs new file mode 100644 index 0000000000..65d1fc3706 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_redrives_to_the_dlq_async.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway +{ + [Trait("Category", "AWS")] + [Trait("Fragile", "CI")] + public class SqsMessageProducerDlqTestsAsync : IDisposable, IAsyncDisposable + { + private readonly SqsMessageProducer _sender; + private readonly IAmAChannelAsync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly string _dlqChannelName; + + public SqsMessageProducerDlqTestsAsync() + { + MyCommand myCommand = new MyCommand { Value = "Test" }; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _dlqChannelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + SqsSubscription subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + redrivePolicy: new RedrivePolicy(_dlqChannelName, 2) + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + _awsConnection = new AWSMessagingGatewayConnection(credentials, region); + + _sender = new SqsMessageProducer(_awsConnection, new SnsPublication { MakeChannels = OnMissingChannel.Create }); + + _sender.ConfirmTopicExistsAsync(topicName).Wait(); + + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + } + + [Fact] + public async Task When_requeueing_redrives_to_the_queue_async() + { + await _sender.SendAsync(_message); + var receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(receivedMessage); + + receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(receivedMessage); + + receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(receivedMessage); + + await Task.Delay(5000); + + int dlqCount = await GetDLQCountAsync(_dlqChannelName); + dlqCount.Should().Be(1); + } + + public async Task GetDLQCountAsync(string queueName) + { + using var sqsClient = new AmazonSQSClient(_awsConnection.Credentials, _awsConnection.Region); + var queueUrlResponse = await sqsClient.GetQueueUrlAsync(queueName); + var response = await sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageAttributeNames = new List { "All", "ApproximateReceiveCount" } + }); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException($"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_throwing_defer_action_respect_redrive.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_throwing_defer_action_respect_redrive.cs index c1895016db..76e4731a02 100644 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_throwing_defer_action_respect_redrive.cs +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_throwing_defer_action_respect_redrive.cs @@ -24,10 +24,11 @@ public class SnsReDrivePolicySDlqTests private readonly IAmAMessagePump _messagePump; private readonly Message _message; private readonly string _dlqChannelName; - private readonly IAmAChannel _channel; + private readonly IAmAChannelSync _channel; private readonly SqsMessageProducer _sender; private readonly AWSMessagingGatewayConnection _awsConnection; private readonly SqsSubscription _subscription; + private readonly ChannelFactory _channelFactory; public SnsReDrivePolicySDlqTests() { @@ -48,6 +49,7 @@ public SnsReDrivePolicySDlqTests() requeueCount: -1, //delay before requeuing requeueDelay: TimeSpan.FromMilliseconds(50), + messagePumpType: MessagePumpType.Reactor, //we want our SNS subscription to manage requeue limits using the DLQ for 'too many requeues' redrivePolicy: new RedrivePolicy ( @@ -79,8 +81,8 @@ public SnsReDrivePolicySDlqTests() ); //We need to do this manually in a test - will create the channel from subscriber parameters - ChannelFactory channelFactory = new(_awsConnection); - _channel = channelFactory.CreateChannel(_subscription); + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateSyncChannel(_subscription); //how do we handle a command IHandleRequests handler = new MyDeferredCommandHandler(); @@ -105,7 +107,7 @@ public SnsReDrivePolicySDlqTests() messageMapperRegistry.Register(); //pump messages from a channel to a handler - in essence we are building our own dispatcher in this test - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, + _messagePump = new Reactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) { Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 @@ -155,5 +157,17 @@ public async Task When_throwing_defer_action_respect_redrive() //inspect the dlq GetDLQCount(_dlqChannelName).Should().Be(1); } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } } } diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_throwing_defer_action_respect_redrive_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_throwing_defer_action_respect_redrive_async.cs new file mode 100644 index 0000000000..f7445b49b7 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_throwing_defer_action_respect_redrive_async.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.ServiceActivator; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway +{ + [Trait("Category", "AWS")] + [Trait("Fragile", "CI")] + public class SnsReDrivePolicySDlqTestsAsync : IDisposable, IAsyncDisposable + { + private readonly IAmAMessagePump _messagePump; + private readonly Message _message; + private readonly string _dlqChannelName; + private readonly IAmAChannelAsync _channel; + private readonly SqsMessageProducer _sender; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private readonly ChannelFactory _channelFactory; + + public SnsReDrivePolicySDlqTestsAsync() + { + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _dlqChannelName = $"Redrive-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + _subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + requeueCount: -1, + requeueDelay: TimeSpan.FromMilliseconds(50), + messagePumpType: MessagePumpType.Proactor, + redrivePolicy: new RedrivePolicy(new ChannelName(_dlqChannelName), 2) + ); + + var myCommand = new MyDeferredCommand { Value = "Hello Redrive" }; + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + _awsConnection = new AWSMessagingGatewayConnection(credentials, region); + + _sender = new SqsMessageProducer( + _awsConnection, + new SnsPublication + { + Topic = routingKey, + RequestType = typeof(MyDeferredCommand), + MakeChannels = OnMissingChannel.Create + } + ); + + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateAsyncChannel(_subscription); + + IHandleRequestsAsync handler = new MyDeferredCommandHandlerAsync(); + + var subscriberRegistry = new SubscriberRegistry(); + subscriberRegistry.RegisterAsync(); + + IAmACommandProcessor commandProcessor = new CommandProcessor( + subscriberRegistry: subscriberRegistry, + handlerFactory: new QuickHandlerFactoryAsync(() => handler), + requestContextFactory: new InMemoryRequestContextFactory(), + policyRegistry: new PolicyRegistry() + ); + var provider = new CommandProcessorProvider(commandProcessor); + + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory(_ => new MyDeferredCommandMessageMapper()), + null + ); + messageMapperRegistry.Register(); + + _messagePump = new Proactor(provider, messageMapperRegistry, + new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), _channel) + { + Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 + }; + } + + public async Task GetDLQCountAsync(string queueName) + { + using var sqsClient = new AmazonSQSClient(_awsConnection.Credentials, _awsConnection.Region); + var queueUrlResponse = await sqsClient.GetQueueUrlAsync(queueName); + var response = await sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageSystemAttributeNames = new List { "ApproximateReceiveCount" }, + MessageAttributeNames = new List { "All" } + }); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException($"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + [Fact(Skip = "Failing async tests caused by task scheduler issues")] + public async Task When_throwing_defer_action_respect_redrive_async() + { + await _sender.SendAsync(_message); + + var task = Task.Factory.StartNew(() => _messagePump.Run(), TaskCreationOptions.LongRunning); + await Task.Delay(5000); + + var quitMessage = MessageFactory.CreateQuitMessage(_subscription.RoutingKey); + _channel.Enqueue(quitMessage); + + await Task.WhenAll(task); + + await Task.Delay(5000); + + int dlqCount = await GetDLQCountAsync(_dlqChannelName); + dlqCount.Should().Be(1); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_topic_missing_verify_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_topic_missing_verify_throws_async.cs new file mode 100644 index 0000000000..dc3102fc53 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_topic_missing_verify_throws_async.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway +{ + [Trait("Category", "AWS")] + public class AWSValidateMissingTopicTestsAsync + { + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly RoutingKey _routingKey; + + public AWSValidateMissingTopicTestsAsync() + { + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _routingKey = new RoutingKey(topicName); + + (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + _awsConnection = new AWSMessagingGatewayConnection(credentials, region); + + // Because we don't use channel factory to create the infrastructure - it won't exist + } + + [Fact] + public async Task When_topic_missing_verify_throws_async() + { + // arrange + var producer = new SqsMessageProducer(_awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Validate + }); + + // act & assert + await Assert.ThrowsAsync(async () => + await producer.SendAsync(new Message( + new MessageHeader("", _routingKey, MessageType.MT_EVENT, type: "plain/text"), + new MessageBody("Test")))); + } + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/TestDoubles/MyDeferredCommandHandlerAsync.cs b/tests/Paramore.Brighter.AWS.Tests/TestDoubles/MyDeferredCommandHandlerAsync.cs new file mode 100644 index 0000000000..a6228a8a5f --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/TestDoubles/MyDeferredCommandHandlerAsync.cs @@ -0,0 +1,22 @@ +using System.Threading; +using System.Threading.Tasks; +using Paramore.Brighter; +using Paramore.Brighter.Actions; +using Paramore.Brighter.AWS.Tests.TestDoubles; + +internal class MyDeferredCommandHandlerAsync : RequestHandlerAsync +{ + public override async Task HandleAsync(MyDeferredCommand command, CancellationToken cancellationToken = default) + { + // Simulate some asynchronous work + await Task.Delay(100, cancellationToken); + + // Logic to handle the command + if (command.Value == "Hello Redrive") + { + throw new DeferMessageAction(); + } + + return await base.HandleAsync(command, cancellationToken); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/TestDoubles/QuickHandlerFactoryAsync.cs b/tests/Paramore.Brighter.AWS.Tests/TestDoubles/QuickHandlerFactoryAsync.cs new file mode 100644 index 0000000000..e2a2cc74a5 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/TestDoubles/QuickHandlerFactoryAsync.cs @@ -0,0 +1,22 @@ +using System; +using Paramore.Brighter; + +public class QuickHandlerFactoryAsync : IAmAHandlerFactoryAsync +{ + private readonly Func _handlerFactory; + + public QuickHandlerFactoryAsync(Func handlerFactory) + { + _handlerFactory = handlerFactory; + } + + public IHandleRequestsAsync Create(Type handlerType) + { + return _handlerFactory(); + } + + public void Release(IHandleRequestsAsync handler) + { + // Implement any necessary cleanup logic here + } +} diff --git a/tests/Paramore.Brighter.AzureServiceBus.Tests/AzureServiceBusChannelFactoryTests.cs b/tests/Paramore.Brighter.AzureServiceBus.Tests/AzureServiceBusChannelFactoryTests.cs index 9bac72c130..a615bfd1d6 100644 --- a/tests/Paramore.Brighter.AzureServiceBus.Tests/AzureServiceBusChannelFactoryTests.cs +++ b/tests/Paramore.Brighter.AzureServiceBus.Tests/AzureServiceBusChannelFactoryTests.cs @@ -1,23 +1,48 @@ using System; +using System.Threading.Tasks; using Xunit; using Paramore.Brighter.MessagingGateway.AzureServiceBus; -namespace Paramore.Brighter.AzureServiceBus.Tests +namespace Paramore.Brighter.AzureServiceBus.Tests; + +public class AzureServiceBusChannelFactoryTests { - - public class AzureServiceBusChannelFactoryTests + [Fact] + public void When_the_timeout_is_below_400_ms_it_should_throw_an_exception() + { + var factory = new AzureServiceBusChannelFactory(new AzureServiceBusConsumerFactory(new AzureServiceBusConfiguration("Endpoint=sb://someString.servicebus.windows.net;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=oUWJw7777s7ydjdafqFqhk9O7TOs="))); + + var subscription = new AzureServiceBusSubscription(typeof(object), new SubscriptionName("name"), new ChannelName("name"), new RoutingKey("name"), + 1, 1, timeOut: TimeSpan.FromMilliseconds(399)); + + ArgumentException exception = Assert.Throws(() => factory.CreateSyncChannel(subscription)); + + Assert.Equal("The minimum allowed timeout is 400 milliseconds", exception.Message); + } + + [Fact] + public void When_the_timeout_is_below_400_ms_it_should_throw_an_exception_async_channel() + { + var factory = new AzureServiceBusChannelFactory(new AzureServiceBusConsumerFactory(new AzureServiceBusConfiguration("Endpoint=sb://someString.servicebus.windows.net;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=oUWJw7777s7ydjdafqFqhk9O7TOs="))); + + var subscription = new AzureServiceBusSubscription(typeof(object), new SubscriptionName("name"), new ChannelName("name"), new RoutingKey("name"), + 1, 1, timeOut: TimeSpan.FromMilliseconds(399)); + + ArgumentException exception = Assert.Throws(() => factory.CreateAsyncChannel(subscription)); + + Assert.Equal("The minimum allowed timeout is 400 milliseconds", exception.Message); + } + + [Fact] + public async Task When_the_timeout_is_below_400_ms_it_should_throw_an_exception_async_channel_async() { - [Fact] - public void When_the_timeout_is_below_400_ms_it_should_throw_an_exception() - { - var factory = new AzureServiceBusChannelFactory(new AzureServiceBusConsumerFactory(new AzureServiceBusConfiguration("Endpoint=sb://someString.servicebus.windows.net;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=oUWJw7777s7ydjdafqFqhk9O7TOs="))); + var factory = new AzureServiceBusChannelFactory(new AzureServiceBusConsumerFactory(new AzureServiceBusConfiguration("Endpoint=sb://someString.servicebus.windows.net;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=oUWJw7777s7ydjdafqFqhk9O7TOs="))); - var subscription = new AzureServiceBusSubscription(typeof(object), new SubscriptionName("name"), new ChannelName("name"), new RoutingKey("name"), - 1, 1, timeOut: TimeSpan.FromMilliseconds(399)); + var subscription = new AzureServiceBusSubscription(typeof(object), new SubscriptionName("name"), new ChannelName("name"), new RoutingKey("name"), + 1, 1, timeOut: TimeSpan.FromMilliseconds(399)); - ArgumentException exception = Assert.Throws(() => factory.CreateChannel(subscription)); + ArgumentException exception = await Assert.ThrowsAsync(async () => await factory.CreateAsyncChannelAsync(subscription)); - Assert.Equal("The minimum allowed timeout is 400 milliseconds", exception.Message); - } + Assert.Equal("The minimum allowed timeout is 400 milliseconds", exception.Message); } } diff --git a/tests/Paramore.Brighter.AzureServiceBus.Tests/AzureServiceBusConsumerTests.cs b/tests/Paramore.Brighter.AzureServiceBus.Tests/AzureServiceBusConsumerTests.cs index a43f73cf60..18e155163d 100644 --- a/tests/Paramore.Brighter.AzureServiceBus.Tests/AzureServiceBusConsumerTests.cs +++ b/tests/Paramore.Brighter.AzureServiceBus.Tests/AzureServiceBusConsumerTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading.Tasks; using Azure.Messaging.ServiceBus; using Paramore.Brighter.AzureServiceBus.Tests.Fakes; using Paramore.Brighter.AzureServiceBus.Tests.TestDoubles; @@ -9,485 +10,484 @@ using Paramore.Brighter.MessagingGateway.AzureServiceBus.AzureServiceBusWrappers; using Xunit; -namespace Paramore.Brighter.AzureServiceBus.Tests +namespace Paramore.Brighter.AzureServiceBus.Tests; + +public class AzureServiceBusConsumerTests { - public class AzureServiceBusConsumerTests - { - private readonly FakeAdministrationClient _nameSpaceManagerWrapper; - private readonly AzureServiceBusConsumer _azureServiceBusConsumer; - private readonly FakeServiceBusReceiverWrapper _messageReceiver; - private readonly FakeMessageProducer _fakeMessageProducer; - private readonly FakeServiceBusReceiverProvider _fakeMessageReceiver; + private readonly FakeAdministrationClient _nameSpaceManagerWrapper; + private readonly AzureServiceBusConsumer _azureServiceBusConsumer; + private readonly FakeServiceBusReceiverWrapper _messageReceiver; + private readonly FakeMessageProducer _fakeMessageProducer; + private readonly FakeServiceBusReceiverProvider _fakeMessageReceiver; - private readonly AzureServiceBusSubscriptionConfiguration _subConfig = new(); + private readonly AzureServiceBusSubscriptionConfiguration _subConfig = new(); - public AzureServiceBusConsumerTests() - { - _nameSpaceManagerWrapper = new FakeAdministrationClient(); - _fakeMessageProducer = new FakeMessageProducer(); - _messageReceiver = new FakeServiceBusReceiverWrapper(); - _fakeMessageReceiver = new FakeServiceBusReceiverProvider(_messageReceiver); + public AzureServiceBusConsumerTests() + { + _nameSpaceManagerWrapper = new FakeAdministrationClient(); + _fakeMessageProducer = new FakeMessageProducer(); + _messageReceiver = new FakeServiceBusReceiverWrapper(); + _fakeMessageReceiver = new FakeServiceBusReceiverProvider(_messageReceiver); - var sub = new AzureServiceBusSubscription(routingKey: new RoutingKey("topic"), channelName: new ChannelName("subscription") + var sub = new AzureServiceBusSubscription(routingKey: new RoutingKey("topic"), channelName: new ChannelName("subscription") ,makeChannels: OnMissingChannel.Create, bufferSize: 10, subscriptionConfiguration: _subConfig); - _azureServiceBusConsumer = new AzureServiceBusTopicConsumer(sub, _fakeMessageProducer, - _nameSpaceManagerWrapper, _fakeMessageReceiver); - } + _azureServiceBusConsumer = new AzureServiceBusTopicConsumer(sub, _fakeMessageProducer, + _nameSpaceManagerWrapper, _fakeMessageReceiver); + } - [Fact] - public void When_a_subscription_exists_and_messages_are_in_the_queue_the_messages_are_returned() + [Fact] + public void When_a_subscription_exists_and_messages_are_in_the_queue_the_messages_are_returned() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); + + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); - - var brokeredMessageList = new List(); - var message1 = new BrokeredMessage() - { - MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), - ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } - }; + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } + }; - var message2 = new BrokeredMessage() - { - MessageBodyValue = Encoding.UTF8.GetBytes("somebody2"), - ApplicationProperties = new Dictionary { { "MessageType", "MT_DOCUMENT" } } - }; + var message2 = new BrokeredMessage() + { + MessageBodyValue = Encoding.UTF8.GetBytes("somebody2"), + ApplicationProperties = new Dictionary { { "MessageType", "MT_DOCUMENT" } } + }; - brokeredMessageList.Add(message1); - brokeredMessageList.Add(message2); + brokeredMessageList.Add(message1); + brokeredMessageList.Add(message2); - _messageReceiver.MessageQueue = brokeredMessageList; + _messageReceiver.MessageQueue = brokeredMessageList; - Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); - Assert.Equal("somebody", result[0].Body.Value); - Assert.Equal("topic", result[0].Header.Topic); - Assert.Equal(MessageType.MT_EVENT, result[0].Header.MessageType); + Assert.Equal("somebody", result[0].Body.Value); + Assert.Equal("topic", result[0].Header.Topic); + Assert.Equal(MessageType.MT_EVENT, result[0].Header.MessageType); - Assert.Equal("somebody2", result[1].Body.Value); - Assert.Equal("topic", result[1].Header.Topic); - Assert.Equal(MessageType.MT_DOCUMENT, result[1].Header.MessageType); - } + Assert.Equal("somebody2", result[1].Body.Value); + Assert.Equal("topic", result[1].Header.Topic); + Assert.Equal(MessageType.MT_DOCUMENT, result[1].Header.MessageType); + } - [Fact] - public void When_a_subscription_does_not_exist_and_messages_are_in_the_queue_then_the_subscription_is_created_and_messages_are_returned() + [Fact] + public async Task When_a_subscription_does_not_exist_and_messages_are_in_the_queue_then_the_subscription_is_created_and_messages_are_returned() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.Topics.Add("topic", new ()); - var brokeredMessageList = new List(); - var message1 = new BrokeredMessage() - { - MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), - ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } - }; - brokeredMessageList.Add(message1); - - _messageReceiver.MessageQueue = brokeredMessageList; - - Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } + }; + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; + + Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); - _nameSpaceManagerWrapper.SubscriptionExists("topic", "subscription"); - //A.CallTo(() => _nameSpaceManagerWrapper.f => f.CreateSubscription("topic", "subscription", _subConfig)).MustHaveHappened(); - Assert.Equal("somebody", result[0].Body.Value); - } + await _nameSpaceManagerWrapper.SubscriptionExistsAsync("topic", "subscription"); + //A.CallTo(() => _nameSpaceManagerWrapper.f => f.CreateSubscription("topic", "subscription", _subConfig)).MustHaveHappened(); + Assert.Equal("somebody", result[0].Body.Value); + } - [Fact] - public void When_a_message_is_a_command_type_then_the_message_type_is_set_correctly() + [Fact] + public void When_a_message_is_a_command_type_then_the_message_type_is_set_correctly() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); + + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); - - var brokeredMessageList = new List(); - var message1 = new BrokeredMessage() - { - MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), - ApplicationProperties = new Dictionary { { "MessageType", "MT_COMMAND" } } - }; - brokeredMessageList.Add(message1); + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary { { "MessageType", "MT_COMMAND" } } + }; + brokeredMessageList.Add(message1); - _messageReceiver.MessageQueue = brokeredMessageList; + _messageReceiver.MessageQueue = brokeredMessageList; + + Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); - Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + Assert.Equal("somebody", result[0].Body.Value); + Assert.Equal("topic", result[0].Header.Topic); + Assert.Equal(MessageType.MT_COMMAND, result[0].Header.MessageType); + } - Assert.Equal("somebody", result[0].Body.Value); - Assert.Equal("topic", result[0].Header.Topic); - Assert.Equal(MessageType.MT_COMMAND, result[0].Header.MessageType); - } + [Fact] + public void When_a_message_is_a_command_type_and_it_is_specified_in_funny_casing_then_the_message_type_is_set_correctly() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); - [Fact] - public void When_a_message_is_a_command_type_and_it_is_specified_in_funny_casing_then_the_message_type_is_set_correctly() + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary { { "MessageType", "MT_COmmAND" } } + }; + brokeredMessageList.Add(message1); - var brokeredMessageList = new List(); - var message1 = new BrokeredMessage() - { - MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), - ApplicationProperties = new Dictionary { { "MessageType", "MT_COmmAND" } } - }; - brokeredMessageList.Add(message1); + _messageReceiver.MessageQueue = brokeredMessageList; - _messageReceiver.MessageQueue = brokeredMessageList; + Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); - Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + Assert.Equal("somebody", result[0].Body.Value); + Assert.Equal("topic", result[0].Header.Topic); + Assert.Equal(MessageType.MT_COMMAND, result[0].Header.MessageType); + } - Assert.Equal("somebody", result[0].Body.Value); - Assert.Equal("topic", result[0].Header.Topic); - Assert.Equal(MessageType.MT_COMMAND, result[0].Header.MessageType); - } + [Fact] + public void When_the_specified_message_type_is_unknown_then_it_should_default_to_MT_EVENT() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); - [Fact] - public void When_the_specified_message_type_is_unknown_then_it_should_default_to_MT_EVENT() + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary { { "MessageType", "wrong_message_type" } } + }; + brokeredMessageList.Add(message1); - var brokeredMessageList = new List(); - var message1 = new BrokeredMessage() - { - MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), - ApplicationProperties = new Dictionary { { "MessageType", "wrong_message_type" } } - }; - brokeredMessageList.Add(message1); + _messageReceiver.MessageQueue = brokeredMessageList; - _messageReceiver.MessageQueue = brokeredMessageList; + Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); - Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + Assert.Equal(MessageType.MT_EVENT, result[0].Header.MessageType); + } - Assert.Equal(MessageType.MT_EVENT, result[0].Header.MessageType); - } + [Fact] + public void When_the_message_type_is_not_specified_it_should_default_to_MT_EVENT() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); - [Fact] - public void When_the_message_type_is_not_specified_it_should_default_to_MT_EVENT() + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary() + }; + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; - var brokeredMessageList = new List(); - var message1 = new BrokeredMessage() - { - MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), - ApplicationProperties = new Dictionary() - }; - brokeredMessageList.Add(message1); + Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); - _messageReceiver.MessageQueue = brokeredMessageList; + Assert.Equal("somebody", result[0].Body.Value); + Assert.Equal("topic", result[0].Header.Topic); + Assert.Equal(MessageType.MT_EVENT, result[0].Header.MessageType); + } - Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + [Fact] + public void When_the_user_properties_on_the_azure_sb_message_is_null_it_should_default_to_message_type_to_MT_EVENT() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); - Assert.Equal("somebody", result[0].Body.Value); - Assert.Equal("topic", result[0].Header.Topic); - Assert.Equal(MessageType.MT_EVENT, result[0].Header.MessageType); - } - [Fact] - public void When_the_user_properties_on_the_azure_sb_message_is_null_it_should_default_to_message_type_to_MT_EVENT() + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); - + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary() + }; + brokeredMessageList.Add(message1); - var brokeredMessageList = new List(); - var message1 = new BrokeredMessage() - { - MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), - ApplicationProperties = new Dictionary() - }; - brokeredMessageList.Add(message1); + _messageReceiver.MessageQueue = brokeredMessageList; - _messageReceiver.MessageQueue = brokeredMessageList; + Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); - Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + Assert.Equal("somebody", result[0].Body.Value); + Assert.Equal("topic", result[0].Header.Topic); + Assert.Equal(MessageType.MT_EVENT, result[0].Header.MessageType); + } - Assert.Equal("somebody", result[0].Body.Value); - Assert.Equal("topic", result[0].Header.Topic); - Assert.Equal(MessageType.MT_EVENT, result[0].Header.MessageType); - } + [Fact] + public void When_there_are_no_messages_then_it_returns_an_empty_array() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); + var brokeredMessageList = new List(); - [Fact] - public void When_there_are_no_messages_then_it_returns_an_empty_array() - { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); - var brokeredMessageList = new List(); + _messageReceiver.MessageQueue = brokeredMessageList; - _messageReceiver.MessageQueue = brokeredMessageList; + Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + Assert.Empty(result); + } - Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); - Assert.Empty(result); - } + [Fact] + public void When_trying_to_create_a_subscription_which_was_already_created_by_another_thread_it_should_ignore_the_error() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.CreateSubscriptionException = + new ServiceBusException("whatever", ServiceBusFailureReason.MessagingEntityAlreadyExists); - [Fact] - public void When_trying_to_create_a_subscription_which_was_already_created_by_another_thread_it_should_ignore_the_error() + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.CreateSubscriptionException = - new ServiceBusException("whatever", ServiceBusFailureReason.MessagingEntityAlreadyExists); - - var brokeredMessageList = new List(); - var message1 = new BrokeredMessage() - { - MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), - ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } - }; - brokeredMessageList.Add(message1); + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } + }; + brokeredMessageList.Add(message1); - _messageReceiver.MessageQueue = brokeredMessageList; + _messageReceiver.MessageQueue = brokeredMessageList; - Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); - Assert.Equal("somebody", result[0].Body.Value); - } + Assert.Equal("somebody", result[0].Body.Value); + } - [Fact] - public void When_dispose_is_called_the_close_method_is_called() - { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.Topics.Add("topic", new ()); - _azureServiceBusConsumer.Receive(TimeSpan.Zero); - _azureServiceBusConsumer.Dispose(); + [Fact] + public void When_dispose_is_called_the_close_method_is_called() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + _azureServiceBusConsumer.Receive(TimeSpan.Zero); + _azureServiceBusConsumer.Dispose(); - Assert.True(_messageReceiver.IsClosedOrClosing); - } + Assert.True(_messageReceiver.IsClosedOrClosing); + } - [Fact] - public void When_requeue_is_called_and_the_delay_is_zero_the_send_method_is_called() - { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.Topics.Add("topic", new ()); - _fakeMessageProducer.SentMessages.Clear(); - var messageLockTokenOne = Guid.NewGuid(); - var messageHeader = new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("topic"), MessageType.MT_EVENT); - var message = new Message(messageHeader, new MessageBody("body")); - message.Header.Bag.Add("LockToken", messageLockTokenOne); + [Fact] + public void When_requeue_is_called_and_the_delay_is_zero_the_send_method_is_called() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + _fakeMessageProducer.SentMessages.Clear(); + var messageLockTokenOne = Guid.NewGuid(); + var messageHeader = new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("topic"), MessageType.MT_EVENT); + var message = new Message(messageHeader, new MessageBody("body")); + message.Header.Bag.Add("LockToken", messageLockTokenOne); - _azureServiceBusConsumer.Requeue(message, TimeSpan.Zero); + _azureServiceBusConsumer.Requeue(message, TimeSpan.Zero); - Assert.Single(_fakeMessageProducer.SentMessages); - } + Assert.Single(_fakeMessageProducer.SentMessages); + } - [Fact] - public void When_requeue_is_called_and_the_delay_is_more_than_zero_the_sendWithDelay_method_is_called() - { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.Topics.Add("topic", new ()); - _fakeMessageProducer.SentMessages.Clear(); + [Fact] + public void When_requeue_is_called_and_the_delay_is_more_than_zero_the_sendWithDelay_method_is_called() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + _fakeMessageProducer.SentMessages.Clear(); - var messageLockTokenOne = Guid.NewGuid(); - var messageHeader = new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("topic"), MessageType.MT_EVENT); - var message = new Message(messageHeader, new MessageBody("body")); - message.Header.Bag.Add("LockToken", messageLockTokenOne); + var messageLockTokenOne = Guid.NewGuid(); + var messageHeader = new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("topic"), MessageType.MT_EVENT); + var message = new Message(messageHeader, new MessageBody("body")); + message.Header.Bag.Add("LockToken", messageLockTokenOne); + + _azureServiceBusConsumer.Requeue(message, TimeSpan.FromMilliseconds(100)); - _azureServiceBusConsumer.Requeue(message, TimeSpan.FromMilliseconds(100)); + Assert.Single(_fakeMessageProducer.SentMessages); + } - Assert.Single(_fakeMessageProducer.SentMessages); - } + [Fact] + public void + When_there_is_an_error_talking_to_servicebus_when_checking_if_subscription_exist_then_a_ChannelFailureException_is_raised() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.CreateSubscriptionException = new Exception(); - [Fact] - public void - When_there_is_an_error_talking_to_servicebus_when_checking_if_subscription_exist_then_a_ChannelFailureException_is_raised() - { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.CreateSubscriptionException = new Exception(); + Assert.Throws(() => _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400))); + } - Assert.Throws(() => _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400))); - } + [Fact] + public void When_there_is_an_error_talking_to_servicebus_when_creating_the_subscription_then_a_ChannelFailureException_is_raised_and_ManagementClientWrapper_is_reinitilised() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.CreateSubscriptionException = new Exception(); - [Fact] - public void When_there_is_an_error_talking_to_servicebus_when_creating_the_subscription_then_a_ChannelFailureException_is_raised_and_ManagementClientWrapper_is_reinitilised() - { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.CreateSubscriptionException = new Exception(); - - Assert.Throws(() => _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400))); - Assert.Equal(1, _nameSpaceManagerWrapper.ResetCount); - } - - /// - /// TODO: review - /// - [Fact] - public void When_there_is_an_error_talking_to_servicebus_when_receiving_then_a_ChannelFailureException_is_raised_and_the_messageReceiver_is_recreated() - { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); + Assert.Throws(() => _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400))); + Assert.Equal(1, _nameSpaceManagerWrapper.ResetCount); + } - _messageReceiver.MessageQueue.Clear(); - _messageReceiver.ReceiveException = new Exception(); + /// + /// TODO: review + /// + [Fact] + public void When_there_is_an_error_talking_to_servicebus_when_receiving_then_a_ChannelFailureException_is_raised_and_the_messageReceiver_is_recreated() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); - Assert.Throws(() => _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400))); - Assert.Equal(2, _fakeMessageReceiver.CreationCount); - } + _messageReceiver.MessageQueue.Clear(); + _messageReceiver.ReceiveException = new Exception(); + + Assert.Throws(() => _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400))); + Assert.Equal(2, _fakeMessageReceiver.CreationCount); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void Once_the_subscription_is_created_or_exits_it_does_not_check_if_it_exists_every_time(bool subscriptionExists) + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Once_the_subscription_is_created_or_exits_it_does_not_check_if_it_exists_every_time(bool subscriptionExists) + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + _messageReceiver.MessageQueue.Clear(); + if (subscriptionExists) await _nameSpaceManagerWrapper.CreateSubscriptionAsync("topic", "subscription", new()); + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.Topics.Add("topic", new ()); - _messageReceiver.MessageQueue.Clear(); - if (subscriptionExists) _nameSpaceManagerWrapper.CreateSubscription("topic", "subscription", new()); - var brokeredMessageList = new List(); - var message1 = new BrokeredMessage() - { - MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), - ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } - }; - brokeredMessageList.Add(message1); - - _messageReceiver.MessageQueue = brokeredMessageList; - - _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); - _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } + }; + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; + + _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); - //Subscription is only created once - Assert.Equal(1, _nameSpaceManagerWrapper.Topics["topic"].Count(s => s.Equals("subscription"))); + //Subscription is only created once + Assert.Equal(1, _nameSpaceManagerWrapper.Topics["topic"].Count(s => s.Equals("subscription"))); - Assert.Equal(1, _nameSpaceManagerWrapper.ExistCount); - } + Assert.Equal(1, _nameSpaceManagerWrapper.ExistCount); + } - [Fact] - public void When_MessagingEntityAlreadyExistsException_does_not_check_if_subscription_exists() - { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.Topics.Add("topic", new ()); - _nameSpaceManagerWrapper.CreateSubscriptionException = - new ServiceBusException("whatever", ServiceBusFailureReason.MessagingEntityAlreadyExists); - _messageReceiver.MessageQueue.Clear(); + [Fact] + public void When_MessagingEntityAlreadyExistsException_does_not_check_if_subscription_exists() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + _nameSpaceManagerWrapper.CreateSubscriptionException = + new ServiceBusException("whatever", ServiceBusFailureReason.MessagingEntityAlreadyExists); + _messageReceiver.MessageQueue.Clear(); - var brokeredMessageList = new List(); - var message1 = new BrokeredMessage() - { - MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), - ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } - }; - brokeredMessageList.Add(message1); - - _messageReceiver.MessageQueue = brokeredMessageList; - - Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); - _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() + { + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } + }; + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; + + Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); - Assert.Equal("somebody", result[0].Body.Value); + Assert.Equal("somebody", result[0].Body.Value); - Assert.Equal(1, _nameSpaceManagerWrapper.ExistCount); - } + Assert.Equal(1, _nameSpaceManagerWrapper.ExistCount); + } - [Fact] - public void When_a_message_contains_a_null_body_message_is_still_processed() - { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + [Fact] + public void When_a_message_contains_a_null_body_message_is_still_processed() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); - _messageReceiver.MessageQueue.Clear(); + _messageReceiver.MessageQueue.Clear(); - var brokeredMessageList = new List(); - var message1 = new BrokeredMessage() - { - MessageBodyValue = null, - ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } - }; + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() + { + MessageBodyValue = null, + ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } + }; - brokeredMessageList.Add(message1); + brokeredMessageList.Add(message1); - _messageReceiver.MessageQueue = brokeredMessageList; + _messageReceiver.MessageQueue = brokeredMessageList; - Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); - Assert.Equal(string.Empty, result[0].Body.Value); - } + Assert.Equal(string.Empty, result[0].Body.Value); + } - [Fact] - public void When_receiving_messages_and_the_receiver_is_closing_a_MT_QUIT_message_is_sent() - { - _nameSpaceManagerWrapper.Topics.Add("topic", new ()); - _messageReceiver.Close(); + [Fact] + public void When_receiving_messages_and_the_receiver_is_closing_a_MT_QUIT_message_is_sent() + { + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + _messageReceiver.Close(); - Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); - Assert.Equal(MessageType.MT_QUIT, result[0].Header.MessageType); + Assert.Equal(MessageType.MT_QUIT, result[0].Header.MessageType); - } + } - [Fact] - public void When_a_subscription_does_not_exist_and_Missing_is_set_to_Validate_a_Channel_Failure_is_Raised() - { - _nameSpaceManagerWrapper.ResetState(); + [Fact] + public void When_a_subscription_does_not_exist_and_Missing_is_set_to_Validate_a_Channel_Failure_is_Raised() + { + _nameSpaceManagerWrapper.ResetState(); - var sub = new AzureServiceBusSubscription(routingKey: new RoutingKey("topic"), channelName: new ChannelName("subscription") - ,makeChannels: OnMissingChannel.Validate, subscriptionConfiguration: _subConfig); + var sub = new AzureServiceBusSubscription(routingKey: new RoutingKey("topic"), channelName: new ChannelName("subscription") + ,makeChannels: OnMissingChannel.Validate, subscriptionConfiguration: _subConfig); - var azureServiceBusConsumerValidate = new AzureServiceBusTopicConsumer(sub, _fakeMessageProducer, - _nameSpaceManagerWrapper, _fakeMessageReceiver); + var azureServiceBusConsumerValidate = new AzureServiceBusTopicConsumer(sub, _fakeMessageProducer, + _nameSpaceManagerWrapper, _fakeMessageReceiver); - Assert.Throws(() => azureServiceBusConsumerValidate.Receive(TimeSpan.FromMilliseconds(400))); - } + Assert.Throws(() => azureServiceBusConsumerValidate.Receive(TimeSpan.FromMilliseconds(400))); + } - [Fact] - public void When_ackOnRead_is_Set_and_ack_fails_then_exception_is_thrown() + [Fact] + public void When_ackOnRead_is_Set_and_ack_fails_then_exception_is_thrown() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + _messageReceiver.MessageQueue.Clear(); + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.Topics.Add("topic", new ()); - _messageReceiver.MessageQueue.Clear(); - var brokeredMessageList = new List(); - var message1 = new BrokeredMessage() - { - MessageBodyValue = null, - ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } }, - LockToken = Guid.NewGuid().ToString() - }; - - brokeredMessageList.Add(message1); - - _messageReceiver.MessageQueue = brokeredMessageList; - _messageReceiver.CompleteException = new Exception(); - - var sub = new AzureServiceBusSubscription(routingKey: new RoutingKey("topic"), channelName: new ChannelName("subscription") - ,makeChannels: OnMissingChannel.Create, bufferSize: 10, subscriptionConfiguration: _subConfig); + MessageBodyValue = null, + ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } }, + LockToken = Guid.NewGuid().ToString() + }; + + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; + _messageReceiver.CompleteException = new Exception(); + + var sub = new AzureServiceBusSubscription(routingKey: new RoutingKey("topic"), channelName: new ChannelName("subscription") + ,makeChannels: OnMissingChannel.Create, bufferSize: 10, subscriptionConfiguration: _subConfig); - var azureServiceBusConsumer = new AzureServiceBusTopicConsumer(sub, _fakeMessageProducer, - _nameSpaceManagerWrapper, _fakeMessageReceiver); + var azureServiceBusConsumer = new AzureServiceBusTopicConsumer(sub, _fakeMessageProducer, + _nameSpaceManagerWrapper, _fakeMessageReceiver); - Message[] result = azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + Message[] result = azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); - var msg = result.First(); + var msg = result.First(); - Assert.Throws(() => azureServiceBusConsumer.Acknowledge(msg)); - } + Assert.Throws(() => azureServiceBusConsumer.Acknowledge(msg)); + } - [Fact] - public void When_ackOnRead_is_Set_and_DeadLetter_fails_then_exception_is_thrown() + [Fact] + public void When_ackOnRead_is_Set_and_DeadLetter_fails_then_exception_is_thrown() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() { - _nameSpaceManagerWrapper.ResetState(); - _nameSpaceManagerWrapper.Topics.Add("topic", new ()); - var brokeredMessageList = new List(); - var message1 = new BrokeredMessage() - { - MessageBodyValue = null, - ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } }, - LockToken = Guid.NewGuid().ToString() - }; - - brokeredMessageList.Add(message1); - - _messageReceiver.MessageQueue = brokeredMessageList; - _messageReceiver.DeadLetterException = new Exception(); - - var sub = new AzureServiceBusSubscription(routingKey: new RoutingKey("topic"), channelName: new ChannelName("subscription") - ,makeChannels: OnMissingChannel.Create, bufferSize: 10, subscriptionConfiguration: _subConfig); + MessageBodyValue = null, + ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } }, + LockToken = Guid.NewGuid().ToString() + }; + + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; + _messageReceiver.DeadLetterException = new Exception(); + + var sub = new AzureServiceBusSubscription(routingKey: new RoutingKey("topic"), channelName: new ChannelName("subscription") + ,makeChannels: OnMissingChannel.Create, bufferSize: 10, subscriptionConfiguration: _subConfig); - var azureServiceBusConsumer = new AzureServiceBusTopicConsumer(sub, _fakeMessageProducer, - _nameSpaceManagerWrapper, _fakeMessageReceiver); + var azureServiceBusConsumer = new AzureServiceBusTopicConsumer(sub, _fakeMessageProducer, + _nameSpaceManagerWrapper, _fakeMessageReceiver); - Message[] result = azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + Message[] result = azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); - var msg = result.First(); + var msg = result.First(); - Assert.Throws(() => azureServiceBusConsumer.Reject(msg)); - } + Assert.Throws(() => azureServiceBusConsumer.Reject(msg)); } } diff --git a/tests/Paramore.Brighter.AzureServiceBus.Tests/AzureServiceBusConsumerTestsAsync.cs b/tests/Paramore.Brighter.AzureServiceBus.Tests/AzureServiceBusConsumerTestsAsync.cs new file mode 100644 index 0000000000..5e67ef3db3 --- /dev/null +++ b/tests/Paramore.Brighter.AzureServiceBus.Tests/AzureServiceBusConsumerTestsAsync.cs @@ -0,0 +1,493 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using Paramore.Brighter.AzureServiceBus.Tests.Fakes; +using Paramore.Brighter.AzureServiceBus.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AzureServiceBus; +using Paramore.Brighter.MessagingGateway.AzureServiceBus.AzureServiceBusWrappers; +using Xunit; + +namespace Paramore.Brighter.AzureServiceBus.Tests; + +public class AzureServiceBusConsumerTestsAsync +{ + private readonly FakeAdministrationClient _nameSpaceManagerWrapper; + private readonly AzureServiceBusConsumer _azureServiceBusConsumer; + private readonly FakeServiceBusReceiverWrapper _messageReceiver; + private readonly FakeMessageProducer _fakeMessageProducer; + private readonly FakeServiceBusReceiverProvider _fakeMessageReceiver; + + private readonly AzureServiceBusSubscriptionConfiguration _subConfig = new(); + + public AzureServiceBusConsumerTestsAsync() + { + _nameSpaceManagerWrapper = new FakeAdministrationClient(); + _fakeMessageProducer = new FakeMessageProducer(); + _messageReceiver = new FakeServiceBusReceiverWrapper(); + _fakeMessageReceiver = new FakeServiceBusReceiverProvider(_messageReceiver); + + + var sub = new AzureServiceBusSubscription(routingKey: new RoutingKey("topic"), channelName: new ChannelName("subscription") + ,makeChannels: OnMissingChannel.Create, bufferSize: 10, subscriptionConfiguration: _subConfig); + + _azureServiceBusConsumer = new AzureServiceBusTopicConsumer(sub, _fakeMessageProducer, + _nameSpaceManagerWrapper, _fakeMessageReceiver); + } + + [Fact] + public async Task When_a_subscription_exists_and_messages_are_in_the_queue_the_messages_are_returned() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); + + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() + { + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } + }; + + var message2 = new BrokeredMessage() + { + MessageBodyValue = Encoding.UTF8.GetBytes("somebody2"), + ApplicationProperties = new Dictionary { { "MessageType", "MT_DOCUMENT" } } + }; + + brokeredMessageList.Add(message1); + brokeredMessageList.Add(message2); + + _messageReceiver.MessageQueue = brokeredMessageList; + + Message[] result = await _azureServiceBusConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(400)); + + Assert.Equal("somebody", result[0].Body.Value); + Assert.Equal("topic", result[0].Header.Topic); + Assert.Equal(MessageType.MT_EVENT, result[0].Header.MessageType); + + Assert.Equal("somebody2", result[1].Body.Value); + Assert.Equal("topic", result[1].Header.Topic); + Assert.Equal(MessageType.MT_DOCUMENT, result[1].Header.MessageType); + } + + [Fact] + public async Task When_a_subscription_does_not_exist_and_messages_are_in_the_queue_then_the_subscription_is_created_and_messages_are_returned() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() + { + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } + }; + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; + + Message[] result =await _azureServiceBusConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(400)); + + await _nameSpaceManagerWrapper.SubscriptionExistsAsync("topic", "subscription"); + //A.CallTo(() => _nameSpaceManagerWrapper.f => f.CreateSubscription("topic", "subscription", _subConfig)).MustHaveHappened(); + Assert.Equal("somebody", result[0].Body.Value); + } + + [Fact] + public async Task When_a_message_is_a_command_type_then_the_message_type_is_set_correctly() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); + + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() + { + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary { { "MessageType", "MT_COMMAND" } } + }; + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; + + Message[] result =await _azureServiceBusConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(400)); + + Assert.Equal("somebody", result[0].Body.Value); + Assert.Equal("topic", result[0].Header.Topic); + Assert.Equal(MessageType.MT_COMMAND, result[0].Header.MessageType); + } + + [Fact] + public async Task When_a_message_is_a_command_type_and_it_is_specified_in_funny_casing_then_the_message_type_is_set_correctly() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); + + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() + { + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary { { "MessageType", "MT_COmmAND" } } + }; + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; + + Message[] result = await _azureServiceBusConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(400)); + + Assert.Equal("somebody", result[0].Body.Value); + Assert.Equal("topic", result[0].Header.Topic); + Assert.Equal(MessageType.MT_COMMAND, result[0].Header.MessageType); + } + + [Fact] + public async Task When_the_specified_message_type_is_unknown_then_it_should_default_to_MT_EVENT() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); + + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() + { + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary { { "MessageType", "wrong_message_type" } } + }; + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; + + Message[] result = await _azureServiceBusConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(400)); + + Assert.Equal(MessageType.MT_EVENT, result[0].Header.MessageType); + } + + [Fact] + public async Task When_the_message_type_is_not_specified_it_should_default_to_MT_EVENT() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); + + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() + { + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary() + }; + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; + + Message[] result = await _azureServiceBusConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(400)); + + Assert.Equal("somebody", result[0].Body.Value); + Assert.Equal("topic", result[0].Header.Topic); + Assert.Equal(MessageType.MT_EVENT, result[0].Header.MessageType); + } + + [Fact] + public async Task When_the_user_properties_on_the_azure_sb_message_is_null_it_should_default_to_message_type_to_MT_EVENT() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); + + + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() + { + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary() + }; + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; + + Message[] result = await _azureServiceBusConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(400)); + + Assert.Equal("somebody", result[0].Body.Value); + Assert.Equal("topic", result[0].Header.Topic); + Assert.Equal(MessageType.MT_EVENT, result[0].Header.MessageType); + } + + [Fact] + public async Task When_there_are_no_messages_then_it_returns_an_empty_array() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); + var brokeredMessageList = new List(); + + _messageReceiver.MessageQueue = brokeredMessageList; + + Message[] result = await _azureServiceBusConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(400)); + Assert.Empty(result); + } + + [Fact] + public async Task When_trying_to_create_a_subscription_which_was_already_created_by_another_thread_it_should_ignore_the_error() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.CreateSubscriptionException = + new ServiceBusException("whatever", ServiceBusFailureReason.MessagingEntityAlreadyExists); + + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() + { + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } + }; + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; + + Message[] result = await _azureServiceBusConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(400)); + + Assert.Equal("somebody", result[0].Body.Value); + } + + [Fact] + public async Task When_dispose_is_called_the_close_method_is_called() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + await _azureServiceBusConsumer.ReceiveAsync(TimeSpan.Zero); + await _azureServiceBusConsumer.DisposeAsync(); + + Assert.True(_messageReceiver.IsClosedOrClosing); + } + + [Fact] + public async Task When_requeue_is_called_and_the_delay_is_zero_the_send_method_is_called() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + _fakeMessageProducer.SentMessages.Clear(); + var messageLockTokenOne = Guid.NewGuid(); + var messageHeader = new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("topic"), MessageType.MT_EVENT); + var message = new Message(messageHeader, new MessageBody("body")); + message.Header.Bag.Add("LockToken", messageLockTokenOne); + + await _azureServiceBusConsumer.RequeueAsync(message, TimeSpan.Zero); + + Assert.Single(_fakeMessageProducer.SentMessages); + } + + [Fact] + public void When_requeue_is_called_and_the_delay_is_more_than_zero_the_sendWithDelay_method_is_called() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + _fakeMessageProducer.SentMessages.Clear(); + + var messageLockTokenOne = Guid.NewGuid(); + var messageHeader = new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("topic"), MessageType.MT_EVENT); + var message = new Message(messageHeader, new MessageBody("body")); + message.Header.Bag.Add("LockToken", messageLockTokenOne); + + _azureServiceBusConsumer.Requeue(message, TimeSpan.FromMilliseconds(100)); + + Assert.Single(_fakeMessageProducer.SentMessages); + } + + [Fact] + public void + When_there_is_an_error_talking_to_servicebus_when_checking_if_subscription_exist_then_a_ChannelFailureException_is_raised() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.CreateSubscriptionException = new Exception(); + + Assert.Throws(() => _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400))); + } + + [Fact] + public void When_there_is_an_error_talking_to_servicebus_when_creating_the_subscription_then_a_ChannelFailureException_is_raised_and_ManagementClientWrapper_is_reinitilised() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.CreateSubscriptionException = new Exception(); + + Assert.Throws(() => _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400))); + Assert.Equal(1, _nameSpaceManagerWrapper.ResetCount); + } + + /// + /// TODO: review + /// + [Fact] + public void When_there_is_an_error_talking_to_servicebus_when_receiving_then_a_ChannelFailureException_is_raised_and_the_messageReceiver_is_recreated() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", ["subscription"]); + + _messageReceiver.MessageQueue.Clear(); + _messageReceiver.ReceiveException = new Exception(); + + Assert.Throws(() => _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400))); + Assert.Equal(2, _fakeMessageReceiver.CreationCount); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Once_the_subscription_is_created_or_exits_it_does_not_check_if_it_exists_every_time(bool subscriptionExists) + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + _messageReceiver.MessageQueue.Clear(); + if (subscriptionExists) await _nameSpaceManagerWrapper.CreateSubscriptionAsync("topic", "subscription", new()); + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() + { + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } + }; + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; + + _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + + //Subscription is only created once + Assert.Equal(1, _nameSpaceManagerWrapper.Topics["topic"].Count(s => s.Equals("subscription"))); + + Assert.Equal(1, _nameSpaceManagerWrapper.ExistCount); + } + + [Fact] + public void When_MessagingEntityAlreadyExistsException_does_not_check_if_subscription_exists() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + _nameSpaceManagerWrapper.CreateSubscriptionException = + new ServiceBusException("whatever", ServiceBusFailureReason.MessagingEntityAlreadyExists); + _messageReceiver.MessageQueue.Clear(); + + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() + { + MessageBodyValue = Encoding.UTF8.GetBytes("somebody"), + ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } + }; + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; + + Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + + Assert.Equal("somebody", result[0].Body.Value); + + Assert.Equal(1, _nameSpaceManagerWrapper.ExistCount); + } + + [Fact] + public void When_a_message_contains_a_null_body_message_is_still_processed() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + + _messageReceiver.MessageQueue.Clear(); + + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() + { + MessageBodyValue = null, + ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } } + }; + + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; + + Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + + Assert.Equal(string.Empty, result[0].Body.Value); + } + + [Fact] + public void When_receiving_messages_and_the_receiver_is_closing_a_MT_QUIT_message_is_sent() + { + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + _messageReceiver.Close(); + + Message[] result = _azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + + Assert.Equal(MessageType.MT_QUIT, result[0].Header.MessageType); + + } + + [Fact] + public void When_a_subscription_does_not_exist_and_Missing_is_set_to_Validate_a_Channel_Failure_is_Raised() + { + _nameSpaceManagerWrapper.ResetState(); + + var sub = new AzureServiceBusSubscription(routingKey: new RoutingKey("topic"), channelName: new ChannelName("subscription") + ,makeChannels: OnMissingChannel.Validate, subscriptionConfiguration: _subConfig); + + var azureServiceBusConsumerValidate = new AzureServiceBusTopicConsumer(sub, _fakeMessageProducer, + _nameSpaceManagerWrapper, _fakeMessageReceiver); + + Assert.Throws(() => azureServiceBusConsumerValidate.Receive(TimeSpan.FromMilliseconds(400))); + } + + [Fact] + public void When_ackOnRead_is_Set_and_ack_fails_then_exception_is_thrown() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + _messageReceiver.MessageQueue.Clear(); + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() + { + MessageBodyValue = null, + ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } }, + LockToken = Guid.NewGuid().ToString() + }; + + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; + _messageReceiver.CompleteException = new Exception(); + + var sub = new AzureServiceBusSubscription(routingKey: new RoutingKey("topic"), channelName: new ChannelName("subscription") + ,makeChannels: OnMissingChannel.Create, bufferSize: 10, subscriptionConfiguration: _subConfig); + + var azureServiceBusConsumer = new AzureServiceBusTopicConsumer(sub, _fakeMessageProducer, + _nameSpaceManagerWrapper, _fakeMessageReceiver); + + Message[] result = azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + + var msg = result.First(); + + Assert.Throws(() => azureServiceBusConsumer.Acknowledge(msg)); + } + + [Fact] + public void When_ackOnRead_is_Set_and_DeadLetter_fails_then_exception_is_thrown() + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", new ()); + var brokeredMessageList = new List(); + var message1 = new BrokeredMessage() + { + MessageBodyValue = null, + ApplicationProperties = new Dictionary { { "MessageType", "MT_EVENT" } }, + LockToken = Guid.NewGuid().ToString() + }; + + brokeredMessageList.Add(message1); + + _messageReceiver.MessageQueue = brokeredMessageList; + _messageReceiver.DeadLetterException = new Exception(); + + var sub = new AzureServiceBusSubscription(routingKey: new RoutingKey("topic"), channelName: new ChannelName("subscription") + ,makeChannels: OnMissingChannel.Create, bufferSize: 10, subscriptionConfiguration: _subConfig); + + var azureServiceBusConsumer = new AzureServiceBusTopicConsumer(sub, _fakeMessageProducer, + _nameSpaceManagerWrapper, _fakeMessageReceiver); + + Message[] result = azureServiceBusConsumer.Receive(TimeSpan.FromMilliseconds(400)); + + var msg = result.First(); + + Assert.Throws(() => azureServiceBusConsumer.Reject(msg)); + } +} diff --git a/tests/Paramore.Brighter.AzureServiceBus.Tests/AzureServiceBusMessageProducerTestsAsync.cs b/tests/Paramore.Brighter.AzureServiceBus.Tests/AzureServiceBusMessageProducerTestsAsync.cs new file mode 100644 index 0000000000..906df4e432 --- /dev/null +++ b/tests/Paramore.Brighter.AzureServiceBus.Tests/AzureServiceBusMessageProducerTestsAsync.cs @@ -0,0 +1,299 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using Paramore.Brighter.AzureServiceBus.Tests.Fakes; +using Paramore.Brighter.MessagingGateway.AzureServiceBus; +using Xunit; + +namespace Paramore.Brighter.AzureServiceBus.Tests +{ + public class AzureServiceBusMessageProducerTestsAsync + { + private readonly FakeAdministrationClient _nameSpaceManagerWrapper; + private readonly FakeServiceBusSenderProvider _topicClientProvider; + private readonly FakeServiceBusSenderWrapper _topicClient; + private readonly AzureServiceBusMessageProducer _producer; + private readonly AzureServiceBusMessageProducer _queueProducer; + + public AzureServiceBusMessageProducerTestsAsync() + { + _nameSpaceManagerWrapper = new FakeAdministrationClient(); + _topicClient = new FakeServiceBusSenderWrapper(); + _topicClientProvider = new FakeServiceBusSenderProvider(_topicClient); + + + _producer = new AzureServiceBusTopicMessageProducer( + _nameSpaceManagerWrapper, + _topicClientProvider, + new AzureServiceBusPublication{MakeChannels = OnMissingChannel.Create} + ); + + _queueProducer = new AzureServiceBusQueueMessageProducer( + _nameSpaceManagerWrapper, + _topicClientProvider, + new AzureServiceBusPublication{MakeChannels = OnMissingChannel.Create} + ); + } + + [Fact] + public async Task When_the_topic_exists_and_sending_a_message_with_no_delay_it_should_send_the_message_to_the_correct_topicclient() + { + var messageBody = Encoding.UTF8.GetBytes("A message body"); + + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", []); + + await _producer.SendAsync(new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("topic"), MessageType.MT_EVENT), + new MessageBody(messageBody, "JSON")) + ); + + ServiceBusMessage sentMessage = _topicClient.SentMessages.First(); + + Assert.Equal(messageBody, sentMessage.Body.ToArray()); + Assert.Equal("MT_EVENT", sentMessage.ApplicationProperties["MessageType"]); + Assert.Equal(1, _topicClient.ClosedCount); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task When_sending_a_command_message_type_message_with_no_delay_it_should_set_the_correct_messagetype_property(bool useQueues) + { + var messageBody = Encoding.UTF8.GetBytes("A message body"); + + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", []); + _nameSpaceManagerWrapper.Queues.Add("topic"); + + var producer = useQueues ? _queueProducer : _producer; + + await producer.SendAsync(new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("topic"), MessageType.MT_COMMAND), + new MessageBody(messageBody, "JSON")) + ); + + ServiceBusMessage sentMessage = _topicClient.SentMessages.First(); + + Assert.Equal(messageBody, sentMessage.Body.ToArray()); + Assert.Equal("MT_COMMAND", sentMessage.ApplicationProperties["MessageType"]); + Assert.Equal(1, _topicClient.ClosedCount); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task When_the_topic_does_not_exist_it_should_be_created_and_the_message_is_sent_to_the_correct_topicclient(bool useQueues) + { + var messageBody = Encoding.UTF8.GetBytes("A message body"); + + _nameSpaceManagerWrapper.ResetState(); + + var producer = useQueues ? _queueProducer : _producer; + + await producer.SendAsync(new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("topic"), MessageType.MT_NONE), + new MessageBody(messageBody, "JSON"))); + + ServiceBusMessage sentMessage = _topicClient.SentMessages.First(); + + Assert.Equal(1, _nameSpaceManagerWrapper.CreateCount); + Assert.Equal(messageBody, sentMessage.Body.ToArray()); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task When_a_message_is_send_and_an_exception_occurs_close_is_still_called(bool useQueues) + { + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", []); + _nameSpaceManagerWrapper.Queues.Add("topic"); + + _topicClient.SendException = new Exception("Failed"); + + try + { + var producer = useQueues ? _queueProducer : _producer; + + await producer.SendAsync(new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("topic"), MessageType.MT_NONE), + new MessageBody("Message", "JSON"))); + } + catch (Exception) + { + // ignored + } + + Assert.Equal(1, _topicClient.ClosedCount); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task When_the_topic_exists_and_sending_a_message_with_a_delay_it_should_send_the_message_to_the_correct_topicclient(bool useQueues) + { + var messageBody = Encoding.UTF8.GetBytes("A message body"); + + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", []); + _nameSpaceManagerWrapper.Queues.Add("topic"); + + var producer = useQueues ? _queueProducer : _producer; + + await producer.SendWithDelayAsync( + new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("topic"), MessageType.MT_EVENT), + new MessageBody(messageBody, "JSON")), TimeSpan.FromSeconds(1)); + + ServiceBusMessage sentMessage = _topicClient.SentMessages.First(); + + Assert.Equal(messageBody, sentMessage.Body.ToArray()); + Assert.Equal("MT_EVENT", sentMessage.ApplicationProperties["MessageType"]); + Assert.Equal(1, _topicClient.ClosedCount); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task When_sending_a_command_message_type_message_with_delay_it_should_set_the_correct_messagetype_property(bool useQueues) + { + var messageBody = Encoding.UTF8.GetBytes("A message body"); + + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", []); + _nameSpaceManagerWrapper.Queues.Add("topic"); + + var producer = useQueues ? _queueProducer : _producer; + + await producer.SendWithDelayAsync(new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("topic"), MessageType.MT_COMMAND), + new MessageBody(messageBody, "JSON")), TimeSpan.FromSeconds(1)); + + ServiceBusMessage sentMessage = _topicClient.SentMessages.First(); + + Assert.Equal(messageBody, sentMessage.Body.ToArray()); + Assert.Equal("MT_COMMAND", sentMessage.ApplicationProperties["MessageType"]); + Assert.Equal(1, _topicClient.ClosedCount); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task When_the_topic_does_not_exist_and_sending_a_message_with_a_delay_it_should_send_the_message_to_the_correct_topicclient(bool useQueues) + { + var messageBody = Encoding.UTF8.GetBytes("A message body"); + + _nameSpaceManagerWrapper.ResetState(); + + var producer = useQueues ? _queueProducer : _producer; + + await producer.SendWithDelayAsync(new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("topic"), MessageType.MT_NONE), + new MessageBody(messageBody, "JSON")), TimeSpan.FromSeconds(1)); + + ServiceBusMessage sentMessage = _topicClient.SentMessages.First(); + + Assert.Equal(1, _nameSpaceManagerWrapper.CreateCount); + + Assert.Equal(messageBody, sentMessage.Body.ToArray()); + Assert.Equal(1, _topicClient.ClosedCount); + } + + [Theory] + [InlineData(true, true)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(false, false)] + public async Task Once_the_topic_is_created_it_then_does_not_check_if_it_exists_every_time(bool topicExists, bool useQueues) + { + var messageBody = Encoding.UTF8.GetBytes("A message body"); + + _nameSpaceManagerWrapper.ResetState(); + if (topicExists) + { + _nameSpaceManagerWrapper.Topics.Add("topic", []); + _nameSpaceManagerWrapper.Queues.Add("topic"); + } + + var producer = useQueues ? _queueProducer : _producer; + + var routingKey = new RoutingKey("topic"); + + await producer.SendWithDelayAsync(new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_NONE), + new MessageBody(messageBody, "JSON")), TimeSpan.FromSeconds(1)); + await producer.SendWithDelayAsync(new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_NONE), + new MessageBody(messageBody, "JSON")), TimeSpan.FromSeconds(1)); + + if (topicExists == false) + Assert.Equal(1, _nameSpaceManagerWrapper.CreateCount); + + Assert.Equal(1, _nameSpaceManagerWrapper.ExistCount); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task When_there_is_an_error_talking_to_servicebus_when_creating_the_topic_the_ManagementClientWrapper_is_reinitilised(bool useQueues) + { + var messageBody = Encoding.UTF8.GetBytes("A message body"); + + _nameSpaceManagerWrapper.ExistsException = new Exception(); + + var producer = useQueues ? _queueProducer : _producer; + + await Assert.ThrowsAsync(() => producer.SendWithDelayAsync( + new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("topic"), MessageType.MT_NONE), + new MessageBody(messageBody, "JSON")), TimeSpan.FromSeconds(1)) + ); + Assert.Equal(1, _nameSpaceManagerWrapper.ResetCount); + } + + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void When_there_is_an_error_getting_a_topic_client_the_connection_for_topic_client_is_retried(bool useQueues) + { + var messageBody = Encoding.UTF8.GetBytes("A message body"); + + _nameSpaceManagerWrapper.ResetState(); + _nameSpaceManagerWrapper.Topics.Add("topic", []); + _nameSpaceManagerWrapper.Queues.Add("topic"); + + _topicClientProvider.SingleThrowGetException = new Exception(); + + var producer = useQueues ? _queueProducer : _producer; + + producer.SendWithDelay(new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("topic"), MessageType.MT_NONE), + new MessageBody(messageBody, "JSON")) + ); + + Assert.Single(_topicClient.SentMessages); + } + + [Fact] + public async Task When_the_topic_does_not_exist_and_Missing_is_set_to_Validate_an_exception_is_raised() + { + var messageBody = Encoding.UTF8.GetBytes("A message body"); + + var producerValidate = new AzureServiceBusTopicMessageProducer( + _nameSpaceManagerWrapper, + _topicClientProvider, + new AzureServiceBusPublication{MakeChannels = OnMissingChannel.Validate}) + ; + + await Assert.ThrowsAsync(() => producerValidate.SendAsync( + new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("topic"), MessageType.MT_NONE), + new MessageBody(messageBody, "JSON"))) + ); + } + } +} diff --git a/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeAdministrationClient.cs b/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeAdministrationClient.cs index 14cf438fd1..8a56ab421e 100644 --- a/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeAdministrationClient.cs +++ b/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeAdministrationClient.cs @@ -23,32 +23,34 @@ public class FakeAdministrationClient : IAdministrationClientWrapper public Exception ExistsException { get; set; } = null; public int CreateCount { get; private set; } = 0; - public bool TopicExists(string topicName) + public Task TopicExistsAsync(string topicName) { ExistCount++; if (ExistsException != null) throw ExistsException; - return Topics.Keys.Any(t => t.Equals(topicName, StringComparison.InvariantCultureIgnoreCase)); + return Task.FromResult(Topics.Keys.Any(t => t.Equals(topicName, StringComparison.InvariantCultureIgnoreCase))); } - public bool QueueExists(string queueName) + public Task QueueExistsAsync(string queueName) { ExistCount++; if (ExistsException != null) throw ExistsException; - return Queues.Any(q => q.Equals(queueName, StringComparison.InvariantCultureIgnoreCase)); + return Task.FromResult(Queues.Any(q => q.Equals(queueName, StringComparison.InvariantCultureIgnoreCase))); } - public void CreateQueue(string queueName, TimeSpan? autoDeleteOnIdle = null) + public Task CreateQueueAsync(string queueName, TimeSpan? autoDeleteOnIdle = null) { CreateCount++; Queues.Add(queueName); + return Task.CompletedTask; } - public void CreateTopic(string topicName, TimeSpan? autoDeleteOnIdle = null) + public Task CreateTopicAsync(string topicName, TimeSpan? autoDeleteOnIdle = null) { CreateCount++; Topics.Add(topicName, []); + return Task.CompletedTask; } public Task DeleteQueueAsync(string queueName) @@ -63,17 +65,18 @@ public Task DeleteTopicAsync(string topicName) return Task.CompletedTask; } - public bool SubscriptionExists(string topicName, string subscriptionName) + public Task SubscriptionExistsAsync(string topicName, string subscriptionName) { ExistCount++; - return Topics.ContainsKey(topicName) && Topics[topicName].Contains(subscriptionName); + return Task.FromResult(Topics.ContainsKey(topicName) && Topics[topicName].Contains(subscriptionName)); } - public void CreateSubscription(string topicName, string subscriptionName, + public Task CreateSubscriptionAsync(string topicName, string subscriptionName, AzureServiceBusSubscriptionConfiguration subscriptionConfiguration) { if (CreateSubscriptionException != null) throw CreateSubscriptionException; Topics[topicName].Add(subscriptionName); + return Task.CompletedTask; } public void Reset() diff --git a/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeMessageProducer.cs b/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeMessageProducer.cs index dbc33facfd..df947fb31f 100644 --- a/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeMessageProducer.cs +++ b/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeMessageProducer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Threading; using System.Threading.Tasks; namespace Paramore.Brighter.AzureServiceBus.Tests.Fakes; @@ -10,12 +11,15 @@ public class FakeMessageProducer : IAmAMessageProducerAsync, IAmAMessageProducer public List SentMessages { get; } = new List(); public Publication Publication { get; } public Activity Span { get; set; } - public Task SendAsync(Message message) + public Task SendAsync(Message message, CancellationToken cancellationToken = default) { Send(message); return Task.CompletedTask; } + public async Task SendWithDelayAsync(Message message, TimeSpan? delay, CancellationToken cancellationToken = default) + => await SendAsync(message, cancellationToken); + public void Send(Message message) => SentMessages.Add(message); @@ -24,6 +28,10 @@ public void SendWithDelay(Message message, TimeSpan? delay = null) public void Dispose() { - // TODO release managed resources here + } + + public ValueTask DisposeAsync() + { + return new ValueTask(Task.CompletedTask); } } diff --git a/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeServiceBusReceiverProvider.cs b/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeServiceBusReceiverProvider.cs index ffc51f2d58..89ee6dab92 100644 --- a/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeServiceBusReceiverProvider.cs +++ b/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeServiceBusReceiverProvider.cs @@ -1,19 +1,20 @@ +using System.Threading.Tasks; using Paramore.Brighter.MessagingGateway.AzureServiceBus.AzureServiceBusWrappers; namespace Paramore.Brighter.AzureServiceBus.Tests.Fakes; -public class FakeServiceBusReceiverProvider(IServiceBusReceiverWrapper receiver) : IServiceBusReceiverProvider +public class FakeServiceBusReceiverProvider(IServiceBusReceiverWrapper? receiver) : IServiceBusReceiverProvider { public int CreationCount { get; private set; } = 0; - public IServiceBusReceiverWrapper Get(string queueName, bool sessionEnabled) + public Task GetAsync(string queueName, bool sessionEnabled) { CreationCount++; - return receiver; + return Task.FromResult(receiver); } - public IServiceBusReceiverWrapper Get(string topicName, string subscriptionName, bool sessionEnabled) + public Task GetAsync(string topicName, string subscriptionName, bool sessionEnabled) { CreationCount++; - return receiver; + return Task.FromResult(receiver); } } diff --git a/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeServiceBusReceiverWrapper.cs b/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeServiceBusReceiverWrapper.cs index d2a6b3105e..c89a16634c 100644 --- a/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeServiceBusReceiverWrapper.cs +++ b/tests/Paramore.Brighter.AzureServiceBus.Tests/Fakes/FakeServiceBusReceiverWrapper.cs @@ -14,7 +14,7 @@ public class FakeServiceBusReceiverWrapper : IServiceBusReceiverWrapper public Exception CompleteException = null; public Exception ReceiveException = null; - public Task> Receive(int batchSize, TimeSpan serverWaitTime) + public Task> ReceiveAsync(int batchSize, TimeSpan serverWaitTime) { if (IsClosedOrClosing) throw new Exception("Connection not Open"); @@ -33,7 +33,7 @@ public Task> Receive(int batchSize, TimeSpa return Task.FromResult(messages.AsEnumerable()); } - public Task Complete(string lockToken) + public Task CompleteAsync(string lockToken) { if (CompleteException != null) throw CompleteException; @@ -41,7 +41,7 @@ public Task Complete(string lockToken) return Task.CompletedTask; } - public Task DeadLetter(string lockToken) + public Task DeadLetterAsync(string lockToken) { if (DeadLetterException != null) throw DeadLetterException; @@ -52,5 +52,11 @@ public Task DeadLetter(string lockToken) public void Close() => IsClosedOrClosing = true; + public Task CloseAsync() + { + Close(); + return Task.CompletedTask; + } + public bool IsClosedOrClosing { get; private set; } = false; } diff --git a/tests/Paramore.Brighter.AzureServiceBus.Tests/MessagingGateway/When_consuming_a_message_via_the_consumer.cs b/tests/Paramore.Brighter.AzureServiceBus.Tests/MessagingGateway/When_consuming_a_message_via_the_consumer.cs index 10a7d29f00..0bb2f16a1f 100644 --- a/tests/Paramore.Brighter.AzureServiceBus.Tests/MessagingGateway/When_consuming_a_message_via_the_consumer.cs +++ b/tests/Paramore.Brighter.AzureServiceBus.Tests/MessagingGateway/When_consuming_a_message_via_the_consumer.cs @@ -16,7 +16,7 @@ namespace Paramore.Brighter.AzureServiceBus.Tests.MessagingGateway public class ASBConsumerTests : IDisposable { private readonly Message _message; - private readonly IAmAChannel _channel; + private readonly IAmAChannelSync _channel; private readonly IAmAProducerRegistry _producerRegistry; private readonly string _correlationId; private readonly string _contentType; @@ -65,13 +65,15 @@ public ASBConsumerTests() var clientProvider = ASBCreds.ASBClientProvider; _administrationClient = new AdministrationClientWrapper(clientProvider); - _administrationClient.CreateSubscription(_topicName, _channelName, _subscriptionConfiguration); + _administrationClient.CreateSubscriptionAsync(_topicName, _channelName, _subscriptionConfiguration) + .GetAwaiter() + .GetResult(); _serviceBusClient = clientProvider.GetServiceBusClient(); var channelFactory = new AzureServiceBusChannelFactory(new AzureServiceBusConsumerFactory(clientProvider)); - _channel = channelFactory.CreateChannel(subscription); + _channel = channelFactory.CreateSyncChannel(subscription); _producerRegistry = new AzureServiceBusProducerRegistryFactory( clientProvider, diff --git a/tests/Paramore.Brighter.AzureServiceBus.Tests/MessagingGateway/When_posting_a_message_via_the_producer.cs b/tests/Paramore.Brighter.AzureServiceBus.Tests/MessagingGateway/When_posting_a_message_via_the_producer.cs index 8b596d3bf8..948692094f 100644 --- a/tests/Paramore.Brighter.AzureServiceBus.Tests/MessagingGateway/When_posting_a_message_via_the_producer.cs +++ b/tests/Paramore.Brighter.AzureServiceBus.Tests/MessagingGateway/When_posting_a_message_via_the_producer.cs @@ -13,8 +13,8 @@ namespace Paramore.Brighter.AzureServiceBus.Tests.MessagingGateway [Trait("Fragile", "CI")] public class ASBProducerTests : IDisposable { - private readonly IAmAChannel _topicChannel; - private readonly IAmAChannel _queueChannel; + private readonly IAmAChannelSync _topicChannel; + private readonly IAmAChannelSync _queueChannel; private readonly IAmAProducerRegistry _producerRegistry; private readonly ASBTestCommand _command; private readonly string _correlationId; @@ -61,12 +61,14 @@ public ASBProducerTests() var clientProvider = ASBCreds.ASBClientProvider; _administrationClient = new AdministrationClientWrapper(clientProvider); - _administrationClient.CreateSubscription(_topicName, channelName, new AzureServiceBusSubscriptionConfiguration()); + _administrationClient.CreateSubscriptionAsync(_topicName, channelName, new AzureServiceBusSubscriptionConfiguration()) + .GetAwaiter() + .GetResult(); var channelFactory = new AzureServiceBusChannelFactory(new AzureServiceBusConsumerFactory(clientProvider)); - _topicChannel = channelFactory.CreateChannel(subscription); - _queueChannel = channelFactory.CreateChannel(queueSubscription); + _topicChannel = channelFactory.CreateSyncChannel(subscription); + _queueChannel = channelFactory.CreateSyncChannel(queueSubscription); _producerRegistry = new AzureServiceBusProducerRegistryFactory( clientProvider, diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Call/When_Calling_A_Server_Via_The_Command_Processor.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Call/When_Calling_A_Server_Via_The_Command_Processor.cs index 3eb18c74e9..147e17211b 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Call/When_Calling_A_Server_Via_The_Command_Processor.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Call/When_Calling_A_Server_Via_The_Command_Processor.cs @@ -112,7 +112,7 @@ public void When_Calling_A_Server_Via_The_Command_Processor() new InMemoryMessageConsumer(_routingKey, _bus, TimeProvider.System, TimeSpan.FromMilliseconds(1000)) ); - var messagePump = new MessagePumpBlocking(provider, _messageMapperRegistry, + var messagePump = new Reactor(provider, _messageMapperRegistry, new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), channel) { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000) }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_channel_failure_exception_is_thrown_for_command_should_retry_until_connection_re_established_async.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_channel_failure_exception_is_thrown_for_command_should_retry_until_connection_re_established_async.cs new file mode 100644 index 0000000000..df2be8b895 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_channel_failure_exception_is_thrown_for_command_should_retry_until_connection_re_established_async.cs @@ -0,0 +1,96 @@ +#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.Linq; +using System.Text.Json; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; +using Paramore.Brighter.ServiceActivator; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor +{ + public class MessagePumpRetryCommandOnConnectionFailureTestsAsync + { + private const string ChannelName = "myChannel"; + private readonly RoutingKey _routingKey = new("MyTopic"); + private readonly InternalBus _bus = new(); + private readonly FakeTimeProvider _timeProvider = new(); + private readonly IAmAMessagePump _messagePump; + private readonly SpyCommandProcessor _commandProcessor; + + public MessagePumpRetryCommandOnConnectionFailureTestsAsync() + { + _commandProcessor = new SpyCommandProcessor(); + var provider = new CommandProcessorProvider(_commandProcessor); + var channel = new FailingChannelAsync( + new ChannelName(ChannelName), _routingKey, + new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000)), + 2) + { + NumberOfRetries = 1 + }; + var messageMapperRegistry = new MessageMapperRegistry( + null, + new SimpleMessageMapperFactoryAsync(_ => new MyCommandMessageMapperAsync())); + messageMapperRegistry.RegisterAsync(); + _messagePump = new Proactor(provider, messageMapperRegistry, new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), channel) + { + Channel = channel, TimeOut = TimeSpan.FromMilliseconds(500), RequeueCount = -1 + }; + + var command = new MyCommand(); + + //two command, will be received when subscription restored + var message1 = new Message( + new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), + new MessageBody(JsonSerializer.Serialize(command, JsonSerialisationOptions.Options)) + ); + var message2 = new Message( + new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), + new MessageBody(JsonSerializer.Serialize(command, JsonSerialisationOptions.Options)) + ); + channel.Enqueue(message1); + channel.Enqueue(message2); + + //end the pump + var quitMessage = MessageFactory.CreateQuitMessage(_routingKey); + channel.Enqueue(quitMessage); + } + + [Fact] + public void When_A_Channel_Failure_Exception_Is_Thrown_For_Command_Should_Retry_Until_Connection_Re_established() + { + _messagePump.Run(); + + //_should_send_the_message_via_the_command_processor + _commandProcessor.Commands.Count().Should().Be(2); + _commandProcessor.Commands[0].Should().Be(CommandType.SendAsync); + _commandProcessor.Commands[1].Should().Be(CommandType.SendAsync); + } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_channel_failure_exception_is_thrown_for_event_should_retry_until_connection_re_established_async.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_channel_failure_exception_is_thrown_for_event_should_retry_until_connection_re_established_async.cs new file mode 100644 index 0000000000..dc8afe1a0e --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_channel_failure_exception_is_thrown_for_event_should_retry_until_connection_re_established_async.cs @@ -0,0 +1,98 @@ +#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.Linq; +using System.Text.Json; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; +using Paramore.Brighter.ServiceActivator; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor +{ + public class MessagePumpRetryEventConnectionFailureTestsAsync + { + private readonly RoutingKey _routingKey = new("MyTopic"); + private readonly InternalBus _bus = new(); + private readonly FakeTimeProvider _timeProvider = new(); + private readonly IAmAMessagePump _messagePump; + private readonly SpyCommandProcessor _commandProcessor; + + public MessagePumpRetryEventConnectionFailureTestsAsync() + { + _commandProcessor = new SpyCommandProcessor(); + var provider = new CommandProcessorProvider(_commandProcessor); + var channel = new FailingChannelAsync( + new ChannelName("myChannel"), _routingKey, + new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000)), + 2) + { + NumberOfRetries = 1 + }; + + var messageMapperRegistry = new MessageMapperRegistry( + null, + new SimpleMessageMapperFactoryAsync(_ => new MyEventMessageMapperAsync())); + messageMapperRegistry.RegisterAsync(); + + _messagePump = new Proactor(provider, messageMapperRegistry, new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), channel) + { + Channel = channel, TimeOut = TimeSpan.FromMilliseconds(500), RequeueCount = -1 + }; + + var @event = new MyEvent(); + + //Two events will be received when channel fixed + var message1 = new Message( + new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_EVENT), + new MessageBody(JsonSerializer.Serialize(@event, JsonSerialisationOptions.Options)) + ); + var message2 = new Message( + new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_EVENT), + new MessageBody(JsonSerializer.Serialize(@event, JsonSerialisationOptions.Options)) + ); + channel.Enqueue(message1); + channel.Enqueue(message2); + + //Quit the message pump + var quitMessage = MessageFactory.CreateQuitMessage(_routingKey); + channel.Enqueue(quitMessage); + } + + [Fact] + public void When_A_Channel_Failure_Exception_Is_Thrown_For_Event_Should_Retry_Until_Connection_Re_established() + { + _messagePump.Run(); + + //_should_publish_the_message_via_the_command_processor + _commandProcessor.Commands.Count().Should().Be(2); + _commandProcessor.Commands[0].Should().Be(CommandType.PublishAsync); + _commandProcessor.Commands[1].Should().Be(CommandType.PublishAsync); + } + + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_command_handler_throws_a_defer_message_Then_message_is_requeued_until_rejectedAsync.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_command_handler_throws_a_defer_message_Then_message_is_requeued_until_rejected_async.cs similarity index 89% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_command_handler_throws_a_defer_message_Then_message_is_requeued_until_rejectedAsync.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_command_handler_throws_a_defer_message_Then_message_is_requeued_until_rejected_async.cs index 551a6000a7..e1cd10b1b6 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_command_handler_throws_a_defer_message_Then_message_is_requeued_until_rejectedAsync.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_command_handler_throws_a_defer_message_Then_message_is_requeued_until_rejected_async.cs @@ -27,17 +27,17 @@ THE SOFTWARE. */ using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor { public class MessagePumpCommandProcessingDeferMessageActionTestsAsync { private const string Topic = "MyCommand"; private const string ChannelName = "myChannel"; private readonly IAmAMessagePump _messagePump; - private readonly Channel _channel; + private readonly ChannelAsync _channel; private readonly int _requeueCount = 5; private readonly InternalBus _bus = new(); private readonly RoutingKey _routingKey = new(Topic); @@ -48,14 +48,14 @@ public MessagePumpCommandProcessingDeferMessageActionTestsAsync() SpyRequeueCommandProcessor commandProcessor = new(); var commandProcessorProvider = new CommandProcessorProvider(commandProcessor); - _channel = new Channel(new(ChannelName),_routingKey, new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000))); + _channel = new ChannelAsync(new(ChannelName),_routingKey, new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000))); var messageMapperRegistry = new MessageMapperRegistry( null, new SimpleMessageMapperFactoryAsync(_ => new MyCommandMessageMapperAsync())); messageMapperRegistry.RegisterAsync(); - _messagePump = new MessagePumpAsync(commandProcessorProvider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) + _messagePump = new Proactor(commandProcessorProvider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) { Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = _requeueCount }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_command_handler_throws_unhandled_exception_Then_message_is_acked_async.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_command_handler_throws_unhandled_exception_Then_message_is_acked_async.cs similarity index 91% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_command_handler_throws_unhandled_exception_Then_message_is_acked_async.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_command_handler_throws_unhandled_exception_Then_message_is_acked_async.cs index 7c0393a33a..d498efb739 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_command_handler_throws_unhandled_exception_Then_message_is_acked_async.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_command_handler_throws_unhandled_exception_Then_message_is_acked_async.cs @@ -28,17 +28,17 @@ THE SOFTWARE. */ using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; using Serilog.Events; using Serilog.Sinks.TestCorrelator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor { public class MessagePumpCommandProcessingExceptionTestsAsync { private readonly IAmAMessagePump _messagePump; - private readonly Channel _channel; + private readonly ChannelAsync _channel; private readonly int _requeueCount = 5; private readonly RoutingKey _routingKey = new("MyCommand"); private readonly FakeTimeProvider _timeProvider = new(); @@ -50,14 +50,14 @@ public MessagePumpCommandProcessingExceptionTestsAsync() InternalBus bus = new(); - _channel = new Channel(new("myChannel"),_routingKey, new InMemoryMessageConsumer(_routingKey, bus, _timeProvider, TimeSpan.FromMilliseconds(1000))); + _channel = new ChannelAsync(new("myChannel"),_routingKey, new InMemoryMessageConsumer(_routingKey, bus, _timeProvider, TimeSpan.FromMilliseconds(1000))); var messageMapperRegistry = new MessageMapperRegistry( null, new SimpleMessageMapperFactoryAsync(_ => new MyCommandMessageMapperAsync())); messageMapperRegistry.RegisterAsync(); - _messagePump = new MessagePumpAsync(commandProcessorProvider, messageMapperRegistry, + _messagePump = new Proactor(commandProcessorProvider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel ) { diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_Is_asked_to_connect_a_channel_and_handler_async.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_Is_asked_to_connect_a_channel_and_handler_async.cs similarity index 96% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_Is_asked_to_connect_a_channel_and_handler_async.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_Is_asked_to_connect_a_channel_and_handler_async.cs index 1a306b3806..2709aa2888 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_Is_asked_to_connect_a_channel_and_handler_async.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_Is_asked_to_connect_a_channel_and_handler_async.cs @@ -8,7 +8,7 @@ using Paramore.Brighter.ServiceActivator; using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor { [Collection("CommandProcessor")] @@ -39,7 +39,7 @@ public MessageDispatcherRoutingAsyncTests() channelFactory: new InMemoryChannelFactory(_bus, _timeProvider), channelName: new ChannelName(ChannelName), routingKey: _routingKey, - runAsync: true + messagePumpType: MessagePumpType.Proactor ); _dispatcher = new Dispatcher( diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_has_a_new_connection_added_while_running_async.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_has_a_new_connection_added_while_running_async.cs new file mode 100644 index 0000000000..66a286475f --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_has_a_new_connection_added_while_running_async.cs @@ -0,0 +1,103 @@ +#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.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; +using Paramore.Brighter.ServiceActivator; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor +{ + [Collection("CommandProcessor")] + public class DispatcherAddNewConnectionTestsAsync : IDisposable + { + private readonly Dispatcher _dispatcher; + private readonly Subscription _newSubscription; + private readonly InternalBus _bus; + private readonly RoutingKey _routingKey = new("MyEvent"); + private readonly RoutingKey _routingKeyTwo = new("OtherEvent"); + + public DispatcherAddNewConnectionTestsAsync() + { + _bus = new InternalBus(); + + IAmACommandProcessor commandProcessor = new SpyCommandProcessor(); + + var messageMapperRegistry = new MessageMapperRegistry( + null, + new SimpleMessageMapperFactoryAsync((_) => new MyEventMessageMapperAsync())); + messageMapperRegistry.RegisterAsync(); + + Subscription subscription = new Subscription( + new SubscriptionName("test"), noOfPerformers: 1, timeOut: TimeSpan.FromMilliseconds(1000), + channelFactory: new InMemoryChannelFactory(_bus, TimeProvider.System), channelName: new ChannelName("fakeChannel"), + messagePumpType: MessagePumpType.Proactor, routingKey: _routingKey + ); + + _newSubscription = new Subscription( + new SubscriptionName("newTest"), noOfPerformers: 1, timeOut: TimeSpan.FromMilliseconds(1000), + channelFactory: new InMemoryChannelFactory(_bus, TimeProvider.System), + channelName: new ChannelName("fakeChannelTwo"), messagePumpType: MessagePumpType.Proactor, routingKey: _routingKeyTwo); + _dispatcher = new Dispatcher(commandProcessor, new List { subscription }, messageMapperRegistryAsync: messageMapperRegistry); + + var @event = new MyEvent(); + var message = new MyEventMessageMapperAsync() + .MapToMessageAsync(@event, new Publication{Topic = _routingKey}) + .GetAwaiter() + .GetResult(); + _bus.Enqueue(message); + + _dispatcher.State.Should().Be(DispatcherState.DS_AWAITING); + _dispatcher.Receive(); + } + + [Fact] + public async Task When_A_Message_Dispatcher_Has_A_New_Connection_Added_While_Running() + { + _dispatcher.Open(_newSubscription); + + var @event = new MyEvent(); + var message = await new MyEventMessageMapperAsync().MapToMessageAsync(@event, new Publication{Topic = _routingKeyTwo}); + _bus.Enqueue(message); + + await Task.Delay(1000); + + _bus.Stream(_routingKey).Count().Should().Be(0); + _dispatcher.State.Should().Be(DispatcherState.DS_RUNNING); + _dispatcher.Consumers.Should().HaveCount(2); + _dispatcher.Subscriptions.Should().HaveCount(2); + } + + public void Dispose() + { + if (_dispatcher?.State == DispatcherState.DS_RUNNING) + _dispatcher.End().Wait(); + } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_restarts_a_connection.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_restarts_a_connection.cs new file mode 100644 index 0000000000..ffd76e7a92 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_restarts_a_connection.cs @@ -0,0 +1,111 @@ +#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.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; +using Paramore.Brighter.ServiceActivator; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor +{ + [Collection("CommandProcessor")] + public class MessageDispatcherResetConnectionAsync : IDisposable + { + private readonly Dispatcher _dispatcher; + private readonly Subscription _subscription; + private readonly Publication _publication; + private readonly InternalBus _bus = new(); + private readonly RoutingKey _routingKey = new("myTopic"); + private readonly FakeTimeProvider _timeProvider = new(); + + public MessageDispatcherResetConnectionAsync() + { + IAmACommandProcessor commandProcessor = new SpyCommandProcessor(); + + var messageMapperRegistry = new MessageMapperRegistry( + null, + new SimpleMessageMapperFactoryAsync((_) => new MyEventMessageMapperAsync())); + messageMapperRegistry.RegisterAsync(); + + _subscription = new Subscription( + new SubscriptionName("test"), + noOfPerformers: 1, + timeOut: TimeSpan.FromMilliseconds(1000), + channelFactory: new InMemoryChannelFactory(_bus, _timeProvider), + channelName: new ChannelName("myChannel"), + messagePumpType: MessagePumpType.Proactor, + routingKey: _routingKey + ); + + _publication = new Publication{Topic = _subscription.RoutingKey, RequestType = typeof(MyEvent)}; + + _dispatcher = new Dispatcher(commandProcessor, new List { _subscription }, messageMapperRegistryAsync:messageMapperRegistry); + + var @event = new MyEvent(); + var message = new MyEventMessageMapperAsync() + .MapToMessageAsync(@event, _publication) + .GetAwaiter() + .GetResult(); + + _bus.Enqueue(message); + + _dispatcher.State.Should().Be(DispatcherState.DS_AWAITING); + _dispatcher.Receive(); + Task.Delay(1000).Wait(); + _dispatcher.Shut(_subscription); + } + +#pragma warning disable xUnit1031 + [Fact] + public async Task When_A_Message_Dispatcher_Restarts_A_Connection() + { + _dispatcher.Open(_subscription); + + var @event = new MyEvent(); + var message = await new MyEventMessageMapperAsync().MapToMessageAsync(@event, _publication); + _bus.Enqueue(message); + + await Task.Delay(1000); + + _timeProvider.Advance(TimeSpan.FromSeconds(2)); //This will trigger requeue of not acked/rejected messages + + await _dispatcher.End(); + + Assert.Empty(_bus.Stream(_routingKey)); + _dispatcher.State.Should().Be(DispatcherState.DS_STOPPED); + } +#pragma warning restore xUnit1031 + + public void Dispose() + { + if (_dispatcher?.State == DispatcherState.DS_RUNNING) + _dispatcher.End().Wait(); + } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_restarts_a_connection_after_all_connections_have_stopped_async.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_restarts_a_connection_after_all_connections_have_stopped_async.cs new file mode 100644 index 0000000000..c13c1b8924 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_restarts_a_connection_after_all_connections_have_stopped_async.cs @@ -0,0 +1,124 @@ +#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.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; +using Paramore.Brighter.ServiceActivator; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor +{ + [Collection("CommandProcessor")] + public class DispatcherRestartConnectionTestsAsync : IDisposable + { + private const string ChannelName = "fakeChannel"; + private readonly Dispatcher _dispatcher; + private readonly Publication _publication; + private readonly RoutingKey _routingKey = new("fakekey"); + private readonly ChannelName _channelName = new(ChannelName); + private readonly InternalBus _bus = new(); + private readonly FakeTimeProvider _timeProvider = new(); + + public DispatcherRestartConnectionTestsAsync() + { + IAmACommandProcessor commandProcessor = new SpyCommandProcessor(); + + var messageMapperRegistry = new MessageMapperRegistry( + null, + new SimpleMessageMapperFactoryAsync((_) => new MyEventMessageMapperAsync())); + messageMapperRegistry.RegisterAsync (); + + Subscription subscription = new Subscription( + new SubscriptionName("test"), + noOfPerformers: 1, + timeOut: TimeSpan.FromMilliseconds(100), + channelFactory: new InMemoryChannelFactory(_bus, _timeProvider), + channelName: _channelName, + messagePumpType: MessagePumpType.Proactor, + routingKey: _routingKey + ); + + Subscription newSubscription = new Subscription( + new SubscriptionName("newTest"), + noOfPerformers: 1, timeOut: TimeSpan.FromMilliseconds(100), + channelFactory: new InMemoryChannelFactory(_bus, _timeProvider), + channelName: _channelName, + messagePumpType: MessagePumpType.Proactor, + routingKey: _routingKey + ); + + _publication = new Publication{Topic = subscription.RoutingKey}; + + _dispatcher = new Dispatcher( + commandProcessor, + new List { subscription, newSubscription }, + messageMapperRegistryAsync: messageMapperRegistry) + ; + + var @event = new MyEvent(); + var message = new MyEventMessageMapperAsync() + .MapToMessageAsync(@event, _publication ) + .GetAwaiter() + .GetResult(); + + _bus.Enqueue(message); + + _dispatcher.State.Should().Be(DispatcherState.DS_AWAITING); + _dispatcher.Receive(); + Task.Delay(250).Wait(); + _dispatcher.Shut(subscription.Name); + _dispatcher.Shut(newSubscription.Name); + Task.Delay(1000).Wait(); + _dispatcher.Consumers.Should().HaveCount(0); + } + + [Fact] + public async Task When_A_Message_Dispatcher_Restarts_A_Connection_After_All_Connections_Have_Stopped() + { + _dispatcher.Open(new SubscriptionName("newTest")); + var @event = new MyEvent(); + var message = await new MyEventMessageMapperAsync().MapToMessageAsync(@event, _publication); + _bus.Enqueue(message); + + await Task.Delay(500); + _timeProvider.Advance(TimeSpan.FromSeconds(2)); //This will trigger requeue of not acked/rejected messages + + Assert.Empty(_bus.Stream(_routingKey)); + _dispatcher.State.Should().Be(DispatcherState.DS_RUNNING); + _dispatcher.Consumers.Should().HaveCount(1); + _dispatcher.Subscriptions.Should().HaveCount(2); + } + + public void Dispose() + { + if (_dispatcher?.State == DispatcherState.DS_RUNNING) + _dispatcher.End().Wait(); + } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_shuts_a_connection_async.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_shuts_a_connection_async.cs new file mode 100644 index 0000000000..9e077efebc --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_shuts_a_connection_async.cs @@ -0,0 +1,99 @@ +#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.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; +using Paramore.Brighter.ServiceActivator; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor +{ + [Collection("CommandProcessor")] + public class MessageDispatcherShutConnectionTests : IDisposable + { + private const string Topic = "fakekey"; + private const string ChannelName = "fakeChannel"; + private readonly Dispatcher _dispatcher; + private readonly Subscription _subscription; + private readonly RoutingKey _routingKey = new(Topic); + private readonly FakeTimeProvider _timeProvider = new(); + + public MessageDispatcherShutConnectionTests() + { + InternalBus bus = new(); + + IAmACommandProcessor commandProcessor = new SpyCommandProcessor(); + + var messageMapperRegistry = new MessageMapperRegistry( + null, + new SimpleMessageMapperFactoryAsync((_) => new MyEventMessageMapperAsync())); + messageMapperRegistry.RegisterAsync(); + + _subscription = new Subscription( + new SubscriptionName("test"), + noOfPerformers: 3, + timeOut: TimeSpan.FromMilliseconds(1000), + channelFactory: new InMemoryChannelFactory(bus, _timeProvider), + channelName: new ChannelName(ChannelName), + messagePumpType: MessagePumpType.Proactor, + routingKey: _routingKey + ); + _dispatcher = new Dispatcher(commandProcessor, new List { _subscription }, messageMapperRegistryAsync: messageMapperRegistry); + + var @event = new MyEvent(); + var message = new MyEventMessageMapperAsync().MapToMessageAsync(@event, new Publication{ Topic = _subscription.RoutingKey}) + .GetAwaiter() + .GetResult(); + + for (var i = 0; i < 6; i++) + bus.Enqueue(message); + + _dispatcher.State.Should().Be(DispatcherState.DS_AWAITING); + _dispatcher.Receive(); + } + + [Fact] + public async Task When_A_Message_Dispatcher_Shuts_A_Connection() + { + await Task.Delay(1000); + _dispatcher.Shut(_subscription); + await _dispatcher.End(); + + _dispatcher.Consumers.Should().NotContain(consumer => consumer.Name == _subscription.Name && consumer.State == ConsumerState.Open); + _dispatcher.State.Should().Be(DispatcherState.DS_STOPPED); + _dispatcher.Consumers.Should().BeEmpty(); + } + + public void Dispose() + { + if (_dispatcher?.State == DispatcherState.DS_RUNNING) + _dispatcher.End().Wait(); + } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_starts_different_types_of_performers.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_starts_different_types_of_performers.cs new file mode 100644 index 0000000000..c012fca9cd --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_starts_different_types_of_performers.cs @@ -0,0 +1,130 @@ +#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.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.ServiceActivator; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor +{ + [Collection("CommandProcessor")] + public class MessageDispatcherMultipleConnectionTestsAsync : IDisposable + { + private readonly Dispatcher _dispatcher; + private int _numberOfConsumers; + private readonly InternalBus _bus = new(); + private readonly FakeTimeProvider _timeProvider = new(); + private readonly RoutingKey _commandRoutingKey = new("myCommand"); + private readonly RoutingKey _eventRoutingKey = new("myEvent"); + + public MessageDispatcherMultipleConnectionTestsAsync() + { + var commandProcessor = new SpyCommandProcessor(); + + var container = new ServiceCollection(); + container.AddTransient(); + container.AddTransient(); + + var messageMapperRegistry = new MessageMapperRegistry( + null, + new ServiceProviderMapperFactoryAsync(container.BuildServiceProvider()) + ); + messageMapperRegistry.RegisterAsync(); + messageMapperRegistry.RegisterAsync(); + + var myEventConnection = new Subscription( + new SubscriptionName("test"), + noOfPerformers: 1, + timeOut: TimeSpan.FromMilliseconds(1000), + channelFactory: new InMemoryChannelFactory(_bus, _timeProvider), + messagePumpType: MessagePumpType.Proactor, + channelName: new ChannelName("fakeEventChannel"), + routingKey: _eventRoutingKey + ); + var myCommandConnection = new Subscription( + new SubscriptionName("anothertest"), + noOfPerformers: 1, + timeOut: TimeSpan.FromMilliseconds(1000), + channelFactory: new InMemoryChannelFactory(_bus, _timeProvider), + channelName: new ChannelName("fakeCommandChannel"), + messagePumpType: MessagePumpType.Proactor, + routingKey: _commandRoutingKey + ); + _dispatcher = new Dispatcher(commandProcessor, new List { myEventConnection, myCommandConnection }, messageMapperRegistryAsync: messageMapperRegistry); + + var @event = new MyEvent(); + var eventMessage = new MyEventMessageMapperAsync().MapToMessageAsync(@event, new Publication{Topic = _eventRoutingKey}) + .GetAwaiter() + .GetResult(); + + _bus.Enqueue(eventMessage); + + var command = new MyCommand(); + var commandMessage = new MyCommandMessageMapperAsync().MapToMessageAsync(command, new Publication{Topic = _commandRoutingKey}) + .GetAwaiter() + .GetResult(); + + _bus.Enqueue(commandMessage); + + _dispatcher.State.Should().Be(DispatcherState.DS_AWAITING); + _dispatcher.Receive(); + } + + + [Fact] + public async Task When_A_Message_Dispatcher_Starts_Different_Types_Of_Performers() + { + await Task.Delay(1000); + + _numberOfConsumers = _dispatcher.Consumers.Count(); + + _timeProvider.Advance(TimeSpan.FromSeconds(2)); //This will trigger requeue of not acked/rejected messages + + await _dispatcher.End(); + + Assert.Empty(_bus.Stream(_eventRoutingKey)); + Assert.Empty(_bus.Stream(_commandRoutingKey)); + _dispatcher.State.Should().Be(DispatcherState.DS_STOPPED); + _dispatcher.Consumers.Should().BeEmpty(); + _numberOfConsumers.Should().Be(2); + } + + public void Dispose() + { + if (_dispatcher?.State == DispatcherState.DS_RUNNING) + _dispatcher.End().Wait(); + } + + } + +} diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_starts_multiple_performers_async.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_starts_multiple_performers_async.cs new file mode 100644 index 0000000000..268479d8e1 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_dispatcher_starts_multiple_performers_async.cs @@ -0,0 +1,94 @@ +#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.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; +using Paramore.Brighter.ServiceActivator; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor +{ + + public class MessageDispatcherMultiplePerformerTestsAsync + { + private const string Topic = "myTopic"; + private const string ChannelName = "myChannel"; + private readonly Dispatcher _dispatcher; + private readonly InternalBus _bus; + + public MessageDispatcherMultiplePerformerTestsAsync() + { + var routingKey = new RoutingKey(Topic); + _bus = new InternalBus(); + var consumer = new InMemoryMessageConsumer(routingKey, _bus, TimeProvider.System, TimeSpan.FromMilliseconds(1000)); + + IAmAChannelSync channel = new Channel(new (ChannelName), new(Topic), consumer, 6); + IAmACommandProcessor commandProcessor = new SpyCommandProcessor(); + + var messageMapperRegistry = new MessageMapperRegistry( + null, + new SimpleMessageMapperFactoryAsync((_) => new MyEventMessageMapperAsync())); + messageMapperRegistry.RegisterAsync(); + + var connection = new Subscription( + new SubscriptionName("test"), + noOfPerformers: 3, + timeOut: TimeSpan.FromMilliseconds(100), + channelFactory: new InMemoryChannelFactory(_bus, TimeProvider.System), + channelName: new ChannelName("fakeChannel"), + messagePumpType: MessagePumpType.Proactor, + routingKey: routingKey + ); + _dispatcher = new Dispatcher(commandProcessor, new List { connection }, messageMapperRegistryAsync: messageMapperRegistry); + + var @event = new MyEvent(); + var message = new MyEventMessageMapperAsync().MapToMessageAsync(@event, new Publication{Topic = connection.RoutingKey}) + .GetAwaiter() + .GetResult(); + + for (var i = 0; i < 6; i++) + channel.Enqueue(message); + + _dispatcher.State.Should().Be(DispatcherState.DS_AWAITING); + _dispatcher.Receive(); + } + + [Fact] + public async Task WhenAMessageDispatcherStartsMultiplePerformers() + { + _dispatcher.State.Should().Be(DispatcherState.DS_RUNNING); + _dispatcher.Consumers.Count().Should().Be(3); + + await _dispatcher.End(); + + _bus.Stream(new RoutingKey(Topic)).Count().Should().Be(0); + _dispatcher.State.Should().Be(DispatcherState.DS_STOPPED); + } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_fails_to_be_mapped_to_a_request_and_the_unacceptable_message_limit_is_reached_async.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_fails_to_be_mapped_to_a_request_and_the_unacceptable_message_limit_is_reached_async.cs similarity index 93% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_fails_to_be_mapped_to_a_request_and_the_unacceptable_message_limit_is_reached_async.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_fails_to_be_mapped_to_a_request_and_the_unacceptable_message_limit_is_reached_async.cs index 9a6dd9dca3..1fa89c187e 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_fails_to_be_mapped_to_a_request_and_the_unacceptable_message_limit_is_reached_async.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_fails_to_be_mapped_to_a_request_and_the_unacceptable_message_limit_is_reached_async.cs @@ -26,10 +26,10 @@ THE SOFTWARE. */ using System.Threading.Tasks; using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor { public class MessagePumpUnacceptableMessageLimitTestsAsync { @@ -43,7 +43,7 @@ public MessagePumpUnacceptableMessageLimitTestsAsync() { SpyRequeueCommandProcessor commandProcessor = new(); var provider = new CommandProcessorProvider(commandProcessor); - Channel channel = new( + var channel = new ChannelAsync( new (Channel), _routingKey, new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000)), 2 @@ -53,7 +53,7 @@ public MessagePumpUnacceptableMessageLimitTestsAsync() new SimpleMessageMapperFactoryAsync(_ => new FailingEventMessageMapperAsync())); messageMapperRegistry.RegisterAsync(); - _messagePump = new MessagePumpAsync(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), channel) + _messagePump = new Proactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), channel) { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3, UnacceptableMessageLimit = 3 }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_fails_to_be_mapped_to_a_request_async.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_fails_to_be_mapped_to_a_request_async.cs similarity index 87% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_fails_to_be_mapped_to_a_request_async.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_fails_to_be_mapped_to_a_request_async.cs index 201532f47d..902268e4dc 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_fails_to_be_mapped_to_a_request_async.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_fails_to_be_mapped_to_a_request_async.cs @@ -2,10 +2,10 @@ using System.Threading.Tasks; using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor { public class MessagePumpFailingMessageTranslationTestsAsync @@ -15,13 +15,13 @@ public class MessagePumpFailingMessageTranslationTestsAsync private readonly InternalBus _bus = new(); private readonly FakeTimeProvider _timeProvider = new(); private readonly IAmAMessagePump _messagePump; - private readonly Channel _channel; + private readonly ChannelAsync _channel; public MessagePumpFailingMessageTranslationTestsAsync() { SpyRequeueCommandProcessor commandProcessor = new(); var provider = new CommandProcessorProvider(commandProcessor); - _channel = new Channel( + _channel = new ChannelAsync( new(ChannelName), _routingKey, new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000)) ); @@ -30,7 +30,7 @@ public MessagePumpFailingMessageTranslationTestsAsync() new SimpleMessageMapperFactoryAsync(_ => new FailingEventMessageMapperAsync())); messageMapperRegistry.RegisterAsync(); - _messagePump = new MessagePumpAsync(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) + _messagePump = new Proactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) { Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3, UnacceptableMessageLimit = 3 }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_is_dispatched_it_should_reach_a_handler_async.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_is_dispatched_it_should_reach_a_handler_async.cs similarity index 86% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_is_dispatched_it_should_reach_a_handler_async.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_is_dispatched_it_should_reach_a_handler_async.cs index a886c044ef..03249b7577 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_is_dispatched_it_should_reach_a_handler_async.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_message_is_dispatched_it_should_reach_a_handler_async.cs @@ -3,11 +3,11 @@ using FluentAssertions; using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; using Polly.Registry; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor { public class MessagePumpDispatchAsyncTests { @@ -34,13 +34,13 @@ public MessagePumpDispatchAsyncTests() PipelineBuilder.ClearPipelineCache(); - var channel = new Channel(new(ChannelName), _routingKey, new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000))); + var channel = new ChannelAsync(new(ChannelName), _routingKey, new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000))); var messageMapperRegistry = new MessageMapperRegistry( null, new SimpleMessageMapperFactoryAsync(_ => new MyEventMessageMapperAsync())); messageMapperRegistry.RegisterAsync(); - _messagePump = new MessagePumpAsync(commandProcessorProvider, messageMapperRegistry, new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), channel) + _messagePump = new Proactor(commandProcessorProvider, messageMapperRegistry, new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), channel) { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000) }; var message = new Message(new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_EVENT), new MessageBody(JsonSerializer.Serialize(_myEvent))); diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_requeue_count_threshold_for_commands_has_been_reached.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_requeue_count_threshold_for_commands_has_been_reached.cs new file mode 100644 index 0000000000..a8ecafeaa4 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_requeue_count_threshold_for_commands_has_been_reached.cs @@ -0,0 +1,92 @@ +#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.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; +using Paramore.Brighter.ServiceActivator; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor +{ + public class MessagePumpCommandRequeueCountThresholdTestsAsync + { + private const string Channel = "MyChannel"; + private readonly RoutingKey _routingKey = new("MyTopic"); + private readonly InternalBus _bus = new(); + private readonly FakeTimeProvider _timeProvider = new(); + private readonly IAmAMessagePump _messagePump; + private readonly ChannelAsync _channel; + private readonly SpyRequeueCommandProcessor _commandProcessor; + + public MessagePumpCommandRequeueCountThresholdTestsAsync() + { + _commandProcessor = new SpyRequeueCommandProcessor(); + var provider = new CommandProcessorProvider(_commandProcessor); + _channel = new ChannelAsync(new(Channel) ,_routingKey, new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000))); + + var messageMapperRegistry = new MessageMapperRegistry( + null, + new SimpleMessageMapperFactoryAsync(_ => new MyCommandMessageMapperAsync())); + messageMapperRegistry.RegisterAsync(); + + _messagePump = new Proactor(provider, messageMapperRegistry, new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), _channel) + { Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 }; + + var message1 = new Message(new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), + new MessageBody(JsonSerializer.Serialize((MyCommand)new(), JsonSerialisationOptions.Options)) + ); + var message2 = new Message(new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), + new MessageBody(JsonSerializer.Serialize((MyCommand)new(), JsonSerialisationOptions.Options)) + ); + _bus.Enqueue(message1); + _bus.Enqueue(message2); + } + + [Fact] + public async Task When_A_Requeue_Count_Threshold_For_Commands_Has_Been_Reached() + { + var task = Task.Factory.StartNew(() => _messagePump.Run(), TaskCreationOptions.LongRunning); + await Task.Delay(1000); + + _timeProvider.Advance(TimeSpan.FromSeconds(2)); //This will trigger requeue of not acked/rejected messages + + var quitMessage = MessageFactory.CreateQuitMessage(new RoutingKey("MyTopic")); + _channel.Enqueue(quitMessage); + + await Task.WhenAll(task); + + _commandProcessor.Commands[0].Should().Be(CommandType.SendAsync); + _commandProcessor.SendCount.Should().Be(6); + + Assert.Empty(_bus.Stream(_routingKey)); + + //TODO: How can we observe that the channel has been closed? Observability? + } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_requeue_count_threshold_for_events_has_been_reached.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_requeue_count_threshold_for_events_has_been_reached.cs new file mode 100644 index 0000000000..fb8c6f2033 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_requeue_count_threshold_for_events_has_been_reached.cs @@ -0,0 +1,94 @@ +#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.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; +using Paramore.Brighter.ServiceActivator; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor +{ + public class MessagePumpEventRequeueCountThresholdTestsAsync + { + private const string Channel = "MyChannel"; + private readonly RoutingKey _routingKey = new("MyTopic"); + private readonly InternalBus _bus = new(); + private readonly FakeTimeProvider _timeProvider = new(); + private readonly IAmAMessagePump _messagePump; + private readonly ChannelAsync _channel; + private readonly SpyRequeueCommandProcessor _commandProcessor; + + public MessagePumpEventRequeueCountThresholdTestsAsync() + { + _commandProcessor = new SpyRequeueCommandProcessor(); + var provider = new CommandProcessorProvider(_commandProcessor); + _channel = new ChannelAsync(new(Channel), _routingKey, new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000))); + + var messageMapperRegistry = new MessageMapperRegistry( + null, + new SimpleMessageMapperFactoryAsync(_ => new MyEventMessageMapperAsync())); + messageMapperRegistry.RegisterAsync(); + + _messagePump = new Proactor(provider, messageMapperRegistry, new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), _channel) + { Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 }; + + var message1 = new Message( + new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_EVENT), + new MessageBody(JsonSerializer.Serialize((MyEvent)new(), JsonSerialisationOptions.Options)) + ); + var message2 = new Message( + new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_EVENT), + new MessageBody(JsonSerializer.Serialize((MyEvent)new(), JsonSerialisationOptions.Options)) + ); + _bus.Enqueue(message1); + _bus.Enqueue(message2); + } + + [Fact] + public async Task When_A_Requeue_Count_Threshold_For_Events_Has_Been_Reached() + { + var task = Task.Factory.StartNew(() => _messagePump.Run(), TaskCreationOptions.LongRunning); + await Task.Delay(1000); + + _timeProvider.Advance(TimeSpan.FromSeconds(2)); //This will trigger requeue of not acked/rejected messages + + var quitMessage = MessageFactory.CreateQuitMessage(_routingKey); + _channel.Enqueue(quitMessage); + + await Task.WhenAll(task); + + _commandProcessor.Commands[0].Should().Be(CommandType.PublishAsync); + _commandProcessor.PublishCount.Should().Be(6); + + Assert.Empty(_bus.Stream(_routingKey)); + + //TODO: How do we assert that the channel was closed? Observability? + } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_requeue_of_command_exception_is_thrown.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_requeue_of_command_exception_is_thrown.cs new file mode 100644 index 0000000000..791a905ae6 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_requeue_of_command_exception_is_thrown.cs @@ -0,0 +1,90 @@ +#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.Linq; +using System.Text.Json; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; +using Paramore.Brighter.ServiceActivator; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor +{ + public class MessagePumpCommandRequeueTestsAsync + { + private const string Channel = "MyChannel"; + private readonly RoutingKey _routingKey = new("MyTopic"); + private readonly InternalBus _bus = new(); + private readonly FakeTimeProvider _timeProvider = new(); + private readonly IAmAMessagePump _messagePump; + private readonly SpyCommandProcessor _commandProcessor; + private readonly MyCommand _command = new(); + + public MessagePumpCommandRequeueTestsAsync() + { + _commandProcessor = new SpyRequeueCommandProcessor(); + var provider = new CommandProcessorProvider(_commandProcessor); + ChannelAsync channel = new(new(Channel), _routingKey, new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000)), 2); + + var messageMapperRegistry = new MessageMapperRegistry( + null, + new SimpleMessageMapperFactoryAsync(_ => new MyCommandMessageMapperAsync())); + messageMapperRegistry.RegisterAsync(); + + _messagePump = new Proactor(provider, messageMapperRegistry, new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), channel) + { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = -1 }; + + var message1 = new Message( + new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), + new MessageBody(JsonSerializer.Serialize(_command, JsonSerialisationOptions.Options)) + ); + + var message2 = new Message( + new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), + new MessageBody(JsonSerializer.Serialize(_command, JsonSerialisationOptions.Options)) + ); + + channel.Enqueue(message1); + channel.Enqueue(message2); + var quitMessage = new Message( + new MessageHeader(string.Empty, RoutingKey.Empty, MessageType.MT_QUIT), + new MessageBody("") + ); + channel.Enqueue(quitMessage); + } + + [Fact] + public void When_A_Requeue_Of_Command_Exception_Is_Thrown() + { + _messagePump.Run(); + + _commandProcessor.Commands[0].Should().Be(CommandType.SendAsync); + + Assert.Equal(2, _bus.Stream(_routingKey).Count()); + } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_requeue_of_event_exception_is_thrown.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_requeue_of_event_exception_is_thrown.cs new file mode 100644 index 0000000000..7f61bc0ecc --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_a_requeue_of_event_exception_is_thrown.cs @@ -0,0 +1,95 @@ +#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.Linq; +using System.Text.Json; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; +using Paramore.Brighter.ServiceActivator; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor +{ + public class MessagePumpEventRequeueTestsAsync + { + private const string Channel = "MyChannel"; + private readonly RoutingKey _routingKey = new("MyTopic"); + private readonly InternalBus _bus = new(); + private readonly FakeTimeProvider _timeProvider = new(); + private readonly IAmAMessagePump _messagePump; + private readonly SpyCommandProcessor _commandProcessor; + + public MessagePumpEventRequeueTestsAsync() + { + _commandProcessor = new SpyRequeueCommandProcessor(); + var provider = new CommandProcessorProvider(_commandProcessor); + ChannelAsync channel = new( + new(Channel), _routingKey, + new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000)), + 2 + ); + + var messageMapperRegistry = new MessageMapperRegistry( + null, + new SimpleMessageMapperFactoryAsync(_ => new MyEventMessageMapperAsync())); + messageMapperRegistry.RegisterAsync(); + + _messagePump = new Proactor(provider, messageMapperRegistry, new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), channel) + { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = -1 }; + + var message1 = new Message( + new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_EVENT), + new MessageBody(JsonSerializer.Serialize((MyEvent)new(), JsonSerialisationOptions.Options)) + ); + var message2 = new Message( + new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_EVENT), + new MessageBody(JsonSerializer.Serialize((MyEvent)new(), JsonSerialisationOptions.Options)) + ); + + channel.Enqueue(message1); + channel.Enqueue(message2); + var quitMessage = MessageFactory.CreateQuitMessage(new RoutingKey("MyTopic")); + channel.Enqueue(quitMessage); + } + + [Fact] + public void When_A_Requeue_Of_Event_Exception_Is_Thrown() + { + _messagePump.Run(); + + _timeProvider.Advance(TimeSpan.FromSeconds(2)); //This will trigger requeue of not acked/rejected messages + + //_should_publish_the_message_via_the_command_processor + _commandProcessor.Commands[0].Should().Be(CommandType.PublishAsync); + + //_should_requeue_the_messages + Assert.Equal(2, _bus.Stream(_routingKey).Count()); + + //TODO: How do we know that the channel has been disposed? Observability + } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_event_handler_throws_a_defer_message_Then_message_is_requeued_until_rejectedAsync.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_an_event_handler_throws_a_defer_message_Then_message_is_requeued_until_rejectedAsync.cs similarity index 88% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_event_handler_throws_a_defer_message_Then_message_is_requeued_until_rejectedAsync.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_an_event_handler_throws_a_defer_message_Then_message_is_requeued_until_rejectedAsync.cs index 87607ddfb2..5145e08882 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_event_handler_throws_a_defer_message_Then_message_is_requeued_until_rejectedAsync.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_an_event_handler_throws_a_defer_message_Then_message_is_requeued_until_rejectedAsync.cs @@ -27,10 +27,10 @@ THE SOFTWARE. */ using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor { public class MessagePumpEventProcessingDeferMessageActionTestsAsync { @@ -41,7 +41,7 @@ public class MessagePumpEventProcessingDeferMessageActionTestsAsync private readonly RoutingKey _routingKey = new(Topic); private readonly FakeTimeProvider _timeProvider = new(); private readonly InternalBus _bus; - private readonly Channel _channel; + private readonly ChannelAsync _channel; public MessagePumpEventProcessingDeferMessageActionTestsAsync() { @@ -49,14 +49,14 @@ public MessagePumpEventProcessingDeferMessageActionTestsAsync() var commandProcessorProvider = new CommandProcessorProvider(commandProcessor); _bus = new InternalBus(); - _channel = new Channel(new (Channel), _routingKey, new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000))); + _channel = new ChannelAsync(new (Channel), _routingKey, new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000))); var messageMapperRegistry = new MessageMapperRegistry( null, new SimpleMessageMapperFactoryAsync(_ => new MyEventMessageMapperAsync())); messageMapperRegistry.RegisterAsync(); - _messagePump = new MessagePumpAsync(commandProcessorProvider, messageMapperRegistry, new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), _channel) + _messagePump = new Proactor(commandProcessorProvider, messageMapperRegistry, new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), _channel) { Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = _requeueCount }; var msg = new TransformPipelineBuilderAsync(messageMapperRegistry, null) diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_event_handler_throws_unhandled_exception_Then_message_is_acked_async.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_an_event_handler_throws_unhandled_exception_Then_message_is_acked_async.cs similarity index 90% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_event_handler_throws_unhandled_exception_Then_message_is_acked_async.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_an_event_handler_throws_unhandled_exception_Then_message_is_acked_async.cs index 7b5641576b..591b8766d5 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_event_handler_throws_unhandled_exception_Then_message_is_acked_async.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_an_event_handler_throws_unhandled_exception_Then_message_is_acked_async.cs @@ -28,19 +28,19 @@ THE SOFTWARE. */ using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; using Serilog.Events; using Serilog.Sinks.TestCorrelator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor { public class MessagePumpEventProcessingExceptionTestsAsync { private const string Topic = "MyEvent"; private const string Channel = "MyChannel"; private readonly IAmAMessagePump _messagePump; - private readonly Channel _channel; + private readonly ChannelAsync _channel; private readonly int _requeueCount = 5; private readonly RoutingKey _routingKey = new(Topic); private readonly FakeTimeProvider _timeProvider = new(); @@ -52,14 +52,14 @@ public MessagePumpEventProcessingExceptionTestsAsync() var bus = new InternalBus(); - _channel = new Channel(new (Channel), _routingKey, new InMemoryMessageConsumer(_routingKey, bus, _timeProvider, TimeSpan.FromMilliseconds(1000))); + _channel = new ChannelAsync(new (Channel), _routingKey, new InMemoryMessageConsumer(_routingKey, bus, _timeProvider, TimeSpan.FromMilliseconds(1000))); var messageMapperRegistry = new MessageMapperRegistry( null, new SimpleMessageMapperFactoryAsync(_ => new MyEventMessageMapperAsync())); messageMapperRegistry.RegisterAsync(); - _messagePump = new MessagePumpAsync(commandProcessorProvider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) + _messagePump = new Proactor(commandProcessorProvider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) { Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = _requeueCount }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_unacceptable_message_is_recieved_async.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_an_unacceptable_message_is_recieved_async.cs similarity index 92% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_unacceptable_message_is_recieved_async.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_an_unacceptable_message_is_recieved_async.cs index 3c9a01f1e2..e18490fd86 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_unacceptable_message_is_recieved_async.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_an_unacceptable_message_is_recieved_async.cs @@ -28,16 +28,16 @@ THE SOFTWARE. */ using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor { public class AsyncMessagePumpUnacceptableMessageTests { private const string Channel = "MyChannel"; private readonly IAmAMessagePump _messagePump; - private readonly Channel _channel; + private readonly ChannelAsync _channel; private readonly InternalBus _bus; private readonly RoutingKey _routingKey = new("MyTopic"); private readonly FakeTimeProvider _timeProvider = new FakeTimeProvider(); @@ -49,7 +49,7 @@ public AsyncMessagePumpUnacceptableMessageTests() _bus = new InternalBus(); - _channel = new Channel( + _channel = new ChannelAsync( new(Channel), _routingKey, new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000)) ); @@ -59,7 +59,7 @@ public AsyncMessagePumpUnacceptableMessageTests() new SimpleMessageMapperFactoryAsync(_ => new MyEventMessageMapperAsync())); messageMapperRegistry.RegisterAsync(); - _messagePump = new MessagePumpAsync(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) + _messagePump = new Proactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) { Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_unacceptable_message_limit_is_reached_async.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_an_unacceptable_message_limit_is_reached_async.cs similarity index 91% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_unacceptable_message_limit_is_reached_async.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_an_unacceptable_message_limit_is_reached_async.cs index 9138a47cfa..fd7a5a9a86 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_unacceptable_message_limit_is_reached_async.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_an_unacceptable_message_limit_is_reached_async.cs @@ -27,10 +27,10 @@ THE SOFTWARE. */ using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor { public class MessagePumpUnacceptableMessageLimitBreachedAsyncTests { @@ -44,14 +44,14 @@ public MessagePumpUnacceptableMessageLimitBreachedAsyncTests() SpyRequeueCommandProcessor commandProcessor = new(); var provider = new CommandProcessorProvider(commandProcessor); - Channel channel = new(new("MyChannel"), _routingKey, new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000)), 3); + var channel = new ChannelAsync(new("MyChannel"), _routingKey, new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000)), 3); var messageMapperRegistry = new MessageMapperRegistry( null, new SimpleMessageMapperFactoryAsync(_ => new MyEventMessageMapperAsync())); messageMapperRegistry.RegisterAsync(); - _messagePump = new MessagePumpAsync(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), channel) + _messagePump = new Proactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), channel) { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3, UnacceptableMessageLimit = 3 }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_reading_a_message_from_a_channel_pump_out_to_command_processor_async.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_reading_a_message_from_a_channel_pump_out_to_command_processor_async.cs similarity index 93% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_reading_a_message_from_a_channel_pump_out_to_command_processor_async.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_reading_a_message_from_a_channel_pump_out_to_command_processor_async.cs index 789ea86c79..a95753f876 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_reading_a_message_from_a_channel_pump_out_to_command_processor_async.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_reading_a_message_from_a_channel_pump_out_to_command_processor_async.cs @@ -23,15 +23,15 @@ THE SOFTWARE. */ #endregion using System; +using System.Text.Json; using FluentAssertions; +using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; -using System.Text.Json; -using Microsoft.Extensions.Time.Testing; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor { public class MessagePumpToCommandProcessorTestsAsync { @@ -47,7 +47,7 @@ public MessagePumpToCommandProcessorTestsAsync() { _commandProcessor = new SpyCommandProcessor(); var provider = new CommandProcessorProvider(_commandProcessor); - Channel channel = new( + ChannelAsync channel = new( new(Channel), _routingKey, new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000)) ); @@ -56,7 +56,7 @@ public MessagePumpToCommandProcessorTestsAsync() new SimpleMessageMapperFactoryAsync(_ => new MyEventMessageMapperAsync())); messagerMapperRegistry.RegisterAsync(); - _messagePump = new MessagePumpAsync(provider, messagerMapperRegistry, new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), channel) + _messagePump = new Proactor(provider, messagerMapperRegistry, new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), channel) { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000) }; _event = new MyEvent(); diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_running_a_message_pump_on_a_thread_should_be_able_to_stop_async.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_running_a_message_pump_on_a_thread_should_be_able_to_stop_async.cs similarity index 92% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_running_a_message_pump_on_a_thread_should_be_able_to_stop_async.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_running_a_message_pump_on_a_thread_should_be_able_to_stop_async.cs index 894128e699..5524cab749 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_running_a_message_pump_on_a_thread_should_be_able_to_stop_async.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/When_running_a_message_pump_on_a_thread_should_be_able_to_stop_async.cs @@ -23,16 +23,16 @@ THE SOFTWARE. */ #endregion using System; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; +using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; -using System.Text.Json; -using Microsoft.Extensions.Time.Testing; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Proactor { public class PerformerCanStopTestsAsync { @@ -46,7 +46,7 @@ public PerformerCanStopTestsAsync() { SpyCommandProcessor commandProcessor = new(); var provider = new CommandProcessorProvider(commandProcessor); - Channel channel = new( + ChannelAsync channel = new( new(Channel), _routingKey, new InMemoryMessageConsumer(_routingKey, _bus, _timeProvider, TimeSpan.FromMilliseconds(1000)) ); @@ -56,7 +56,7 @@ public PerformerCanStopTestsAsync() new SimpleMessageMapperFactoryAsync(_ => new MyEventMessageMapperAsync())); messageMapperRegistry.RegisterAsync(); - var messagePump = new MessagePumpAsync(provider, messageMapperRegistry, new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), channel); + var messagePump = new Proactor(provider, messageMapperRegistry, new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), channel); messagePump.Channel = channel; messagePump.TimeOut = TimeSpan.FromMilliseconds(5000); diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_channel_failure_exception_is_thrown_for_command_should_retry_until_connection_re_established.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_channel_failure_exception_is_thrown_for_command_should_retry_until_connection_re_established.cs similarity index 94% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_channel_failure_exception_is_thrown_for_command_should_retry_until_connection_re_established.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_channel_failure_exception_is_thrown_for_command_should_retry_until_connection_re_established.cs index 524ffdd0bc..6461926068 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_channel_failure_exception_is_thrown_for_command_should_retry_until_connection_re_established.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_channel_failure_exception_is_thrown_for_command_should_retry_until_connection_re_established.cs @@ -24,15 +24,15 @@ THE SOFTWARE. */ using System; using System.Linq; +using System.Text.Json; using FluentAssertions; +using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; -using System.Text.Json; -using Microsoft.Extensions.Time.Testing; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class MessagePumpRetryCommandOnConnectionFailureTests { @@ -58,7 +58,7 @@ public MessagePumpRetryCommandOnConnectionFailureTests() new SimpleMessageMapperFactory(_ => new MyCommandMessageMapper()), null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), channel) + _messagePump = new Reactor(provider, messageMapperRegistry, new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), channel) { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(500), RequeueCount = -1 }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_channel_failure_exception_is_thrown_for_event_should_retry_until_connection_re_established.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_channel_failure_exception_is_thrown_for_event_should_retry_until_connection_re_established.cs similarity index 94% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_channel_failure_exception_is_thrown_for_event_should_retry_until_connection_re_established.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_channel_failure_exception_is_thrown_for_event_should_retry_until_connection_re_established.cs index ff1681b224..2b644fc56e 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_channel_failure_exception_is_thrown_for_event_should_retry_until_connection_re_established.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_channel_failure_exception_is_thrown_for_event_should_retry_until_connection_re_established.cs @@ -24,15 +24,15 @@ THE SOFTWARE. */ using System; using System.Linq; +using System.Text.Json; using FluentAssertions; +using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; -using System.Text.Json; -using Microsoft.Extensions.Time.Testing; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class MessagePumpRetryEventConnectionFailureTests { @@ -59,7 +59,7 @@ public MessagePumpRetryEventConnectionFailureTests() null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), channel) + _messagePump = new Reactor(provider, messageMapperRegistry, new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), channel) { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(500), RequeueCount = -1 }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_command_handler_throws_a_defer_message_Then_message_is_requeued_until_rejected.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_command_handler_throws_a_defer_message_Then_message_is_requeued_until_rejected.cs similarity index 94% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_command_handler_throws_a_defer_message_Then_message_is_requeued_until_rejected.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_command_handler_throws_a_defer_message_Then_message_is_requeued_until_rejected.cs index 62a139cb4b..f37d603fb4 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_command_handler_throws_a_defer_message_Then_message_is_requeued_until_rejected.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_command_handler_throws_a_defer_message_Then_message_is_requeued_until_rejected.cs @@ -27,10 +27,10 @@ THE SOFTWARE. */ using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class MessagePumpCommandProcessingDeferMessageActionTests { @@ -53,7 +53,7 @@ public MessagePumpCommandProcessingDeferMessageActionTests() null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) + _messagePump = new Reactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) { Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = _requeueCount }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_command_handler_throws_unhandled_exception_Then_message_is_acked.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_command_handler_throws_unhandled_exception_Then_message_is_acked.cs similarity index 95% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_command_handler_throws_unhandled_exception_Then_message_is_acked.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_command_handler_throws_unhandled_exception_Then_message_is_acked.cs index 40ce5d2131..661428fea6 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_command_handler_throws_unhandled_exception_Then_message_is_acked.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_command_handler_throws_unhandled_exception_Then_message_is_acked.cs @@ -28,12 +28,12 @@ THE SOFTWARE. */ using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; -using Serilog.Sinks.TestCorrelator; using Serilog.Events; +using Serilog.Sinks.TestCorrelator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class MessagePumpCommandProcessingExceptionTests { @@ -56,7 +56,7 @@ public MessagePumpCommandProcessingExceptionTests() null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) + _messagePump = new Reactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) { Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = _requeueCount }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_has_a_new_connection_added_while_running.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_has_a_new_connection_added_while_running.cs similarity index 95% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_has_a_new_connection_added_while_running.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_has_a_new_connection_added_while_running.cs index f536d4052e..b6fcd6bcd7 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_has_a_new_connection_added_while_running.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_has_a_new_connection_added_while_running.cs @@ -29,10 +29,10 @@ THE SOFTWARE. */ using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { [Collection("CommandProcessor")] public class DispatcherAddNewConnectionTests : IDisposable @@ -57,13 +57,13 @@ public DispatcherAddNewConnectionTests() Subscription subscription = new Subscription( new SubscriptionName("test"), noOfPerformers: 1, timeOut: TimeSpan.FromMilliseconds(1000), channelFactory: new InMemoryChannelFactory(_bus, TimeProvider.System), channelName: new ChannelName("fakeChannel"), - routingKey: _routingKey + messagePumpType: MessagePumpType.Reactor, routingKey: _routingKey ); _newSubscription = new Subscription( new SubscriptionName("newTest"), noOfPerformers: 1, timeOut: TimeSpan.FromMilliseconds(1000), channelFactory: new InMemoryChannelFactory(_bus, TimeProvider.System), - channelName: new ChannelName("fakeChannelTwo"), routingKey: _routingKeyTwo); + channelName: new ChannelName("fakeChannelTwo"), messagePumpType: MessagePumpType.Reactor, routingKey: _routingKeyTwo); _dispatcher = new Dispatcher(commandProcessor, new List { subscription }, messageMapperRegistry); var @event = new MyEvent(); diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_is_asked_to_connect_a_channel_and_handler.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_is_asked_to_connect_a_channel_and_handler.cs similarity index 97% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_is_asked_to_connect_a_channel_and_handler.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_is_asked_to_connect_a_channel_and_handler.cs index 58f0152ac2..f0eaa5ca34 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_is_asked_to_connect_a_channel_and_handler.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_is_asked_to_connect_a_channel_and_handler.cs @@ -29,10 +29,10 @@ THE SOFTWARE. */ using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { [Collection("CommandProcessor")] public class MessageDispatcherRoutingTests : IDisposable @@ -58,6 +58,7 @@ public MessageDispatcherRoutingTests() timeOut: TimeSpan.FromMilliseconds(1000), channelFactory: new InMemoryChannelFactory(_bus, _timeProvider), channelName: new ChannelName("myChannel"), + messagePumpType: MessagePumpType.Reactor, routingKey: _routingKey ); diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_restarts_a_connection.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_restarts_a_connection.cs similarity index 97% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_restarts_a_connection.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_restarts_a_connection.cs index 29cae0da7e..91b75eb17f 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_restarts_a_connection.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_restarts_a_connection.cs @@ -29,10 +29,10 @@ THE SOFTWARE. */ using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { [Collection("CommandProcessor")] public class MessageDispatcherResetConnection : IDisposable @@ -59,6 +59,7 @@ public MessageDispatcherResetConnection() timeOut: TimeSpan.FromMilliseconds(1000), channelFactory: new InMemoryChannelFactory(_bus, _timeProvider), channelName: new ChannelName("myChannel"), + messagePumpType: MessagePumpType.Reactor, routingKey: _routingKey ); diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_restarts_a_connection_after_all_connections_have_stopped.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_restarts_a_connection_after_all_connections_have_stopped.cs similarity index 92% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_restarts_a_connection_after_all_connections_have_stopped.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_restarts_a_connection_after_all_connections_have_stopped.cs index 5890c8720c..2850176faf 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_restarts_a_connection_after_all_connections_have_stopped.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_restarts_a_connection_after_all_connections_have_stopped.cs @@ -29,10 +29,10 @@ THE SOFTWARE. */ using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { [Collection("CommandProcessor")] public class DispatcherRestartConnectionTests : IDisposable @@ -55,11 +55,12 @@ public DispatcherRestartConnectionTests() messageMapperRegistry.Register(); Subscription subscription = new Subscription( - new SubscriptionName("test"), - noOfPerformers: 1, - timeOut: TimeSpan.FromMilliseconds(100), - channelFactory: new InMemoryChannelFactory(_bus, _timeProvider), - channelName: _channelName, + new SubscriptionName("test"), + noOfPerformers: 1, + timeOut: TimeSpan.FromMilliseconds(100), + channelFactory: new InMemoryChannelFactory(_bus, _timeProvider), + channelName: _channelName, + messagePumpType: MessagePumpType.Reactor, routingKey: _routingKey ); @@ -68,6 +69,7 @@ public DispatcherRestartConnectionTests() noOfPerformers: 1, timeOut: TimeSpan.FromMilliseconds(100), channelFactory: new InMemoryChannelFactory(_bus, _timeProvider), channelName: _channelName, + messagePumpType: MessagePumpType.Reactor, routingKey: _routingKey ); diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_shuts_a_connection.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_shuts_a_connection.cs similarity index 97% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_shuts_a_connection.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_shuts_a_connection.cs index 2658128675..5b8479742f 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_shuts_a_connection.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_shuts_a_connection.cs @@ -29,10 +29,10 @@ THE SOFTWARE. */ using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { [Collection("CommandProcessor")] public class MessageDispatcherShutConnectionTests : IDisposable @@ -61,6 +61,7 @@ public MessageDispatcherShutConnectionTests() timeOut: TimeSpan.FromMilliseconds(1000), channelFactory: new InMemoryChannelFactory(bus, _timeProvider), channelName: new ChannelName(ChannelName), + messagePumpType: MessagePumpType.Reactor, routingKey: _routingKey ); _dispatcher = new Dispatcher(commandProcessor, new List { _subscription }, messageMapperRegistry); diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_starts_different_types_of_performers.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_starts_different_types_of_performers.cs similarity index 95% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_starts_different_types_of_performers.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_starts_different_types_of_performers.cs index 3f8e9b1e27..828c258bd6 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_starts_different_types_of_performers.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_starts_different_types_of_performers.cs @@ -27,15 +27,15 @@ THE SOFTWARE. */ using System.Linq; using System.Threading.Tasks; using FluentAssertions; -using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; -using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; -using Paramore.Brighter.ServiceActivator; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; +using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { [Collection("CommandProcessor")] public class MessageDispatcherMultipleConnectionTests : IDisposable @@ -61,16 +61,15 @@ public MessageDispatcherMultipleConnectionTests() messageMapperRegistry.Register(); messageMapperRegistry.Register(); - var myEventConnection = new Subscription( new SubscriptionName("test"), noOfPerformers: 1, timeOut: TimeSpan.FromMilliseconds(1000), channelFactory: - new InMemoryChannelFactory(_bus, _timeProvider), channelName: new ChannelName("fakeEventChannel"), + new InMemoryChannelFactory(_bus, _timeProvider), messagePumpType: MessagePumpType.Reactor, channelName: new ChannelName("fakeEventChannel"), routingKey: _eventRoutingKey ); var myCommandConnection = new Subscription( new SubscriptionName("anothertest"), noOfPerformers: 1, timeOut: TimeSpan.FromMilliseconds(1000), channelFactory: new InMemoryChannelFactory(_bus, _timeProvider), - channelName: new ChannelName("fakeCommandChannel"), routingKey: _commandRoutingKey + channelName: new ChannelName("fakeCommandChannel"), messagePumpType: MessagePumpType.Reactor, routingKey: _commandRoutingKey ); _dispatcher = new Dispatcher(commandProcessor, new List { myEventConnection, myCommandConnection }, messageMapperRegistry); diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_starts_multiple_performers.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_starts_multiple_performers.cs similarity index 94% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_starts_multiple_performers.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_starts_multiple_performers.cs index cc7939c884..b55f05bbef 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_dispatcher_starts_multiple_performers.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_dispatcher_starts_multiple_performers.cs @@ -28,10 +28,10 @@ THE SOFTWARE. */ using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class MessageDispatcherMultiplePerformerTests @@ -47,7 +47,7 @@ public MessageDispatcherMultiplePerformerTests() _bus = new InternalBus(); var consumer = new InMemoryMessageConsumer(routingKey, _bus, TimeProvider.System, TimeSpan.FromMilliseconds(1000)); - IAmAChannel channel = new Channel(new (ChannelName), new(Topic), consumer, 6); + IAmAChannelSync channel = new Channel(new (ChannelName), new(Topic), consumer, 6); IAmACommandProcessor commandProcessor = new SpyCommandProcessor(); var messageMapperRegistry = new MessageMapperRegistry( @@ -61,6 +61,7 @@ public MessageDispatcherMultiplePerformerTests() timeOut: TimeSpan.FromMilliseconds(100), channelFactory: new InMemoryChannelFactory(_bus, TimeProvider.System), channelName: new ChannelName("fakeChannel"), + messagePumpType: MessagePumpType.Reactor, routingKey: routingKey ); _dispatcher = new Dispatcher(commandProcessor, new List { connection }, messageMapperRegistry); diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_fails_to_be_mapped_to_a_request.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_fails_to_be_mapped_to_a_request.cs similarity index 86% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_fails_to_be_mapped_to_a_request.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_fails_to_be_mapped_to_a_request.cs index 69ae65f083..54830f05c2 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_fails_to_be_mapped_to_a_request.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_fails_to_be_mapped_to_a_request.cs @@ -2,10 +2,10 @@ using System.Threading.Tasks; using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class MessagePumpFailingMessageTranslationTests @@ -26,8 +26,9 @@ public MessagePumpFailingMessageTranslationTests() new SimpleMessageMapperFactory(_ => new FailingEventMessageMapper()), null); messageMapperRegistry.Register(); + var messageTransformerFactory = new SimpleMessageTransformerFactory(_ => throw new NotImplementedException()); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) + _messagePump = new Reactor(provider, messageMapperRegistry, messageTransformerFactory, new InMemoryRequestContextFactory(), _channel) { Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3, UnacceptableMessageLimit = 3 }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_fails_to_be_mapped_to_a_request_and_the_unacceptable_message_limit_is_reached.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_fails_to_be_mapped_to_a_request_and_the_unacceptable_message_limit_is_reached.cs similarity index 94% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_fails_to_be_mapped_to_a_request_and_the_unacceptable_message_limit_is_reached.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_fails_to_be_mapped_to_a_request_and_the_unacceptable_message_limit_is_reached.cs index 8770212b1e..9bfe7442c4 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_fails_to_be_mapped_to_a_request_and_the_unacceptable_message_limit_is_reached.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_fails_to_be_mapped_to_a_request_and_the_unacceptable_message_limit_is_reached.cs @@ -26,10 +26,10 @@ THE SOFTWARE. */ using System.Threading.Tasks; using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class MessagePumpUnacceptableMessageLimitTests { @@ -50,7 +50,7 @@ public MessagePumpUnacceptableMessageLimitTests() null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), channel) + _messagePump = new Reactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), channel) { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3, UnacceptableMessageLimit = 3 }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_is_dispatched_it_should_reach_a_handler.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_is_dispatched_it_should_reach_a_handler.cs similarity index 95% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_is_dispatched_it_should_reach_a_handler.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_is_dispatched_it_should_reach_a_handler.cs index 9592482246..87d7298c4b 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_message_is_dispatched_it_should_reach_a_handler.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_message_is_dispatched_it_should_reach_a_handler.cs @@ -24,15 +24,15 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Text.Json; using FluentAssertions; +using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; using Polly.Registry; -using System.Text.Json; -using Microsoft.Extensions.Time.Testing; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class MessagePumpDispatchTests { @@ -70,7 +70,7 @@ public MessagePumpDispatchTests() null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), channel) + _messagePump = new Reactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), channel) { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000) }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_requeue_count_threshold_for_commands_has_been_reached.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_requeue_count_threshold_for_commands_has_been_reached.cs similarity index 94% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_requeue_count_threshold_for_commands_has_been_reached.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_requeue_count_threshold_for_commands_has_been_reached.cs index b2748daf96..3e12ed4e1e 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_requeue_count_threshold_for_commands_has_been_reached.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_requeue_count_threshold_for_commands_has_been_reached.cs @@ -23,16 +23,16 @@ THE SOFTWARE. */ #endregion using System; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; +using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; -using System.Text.Json; -using Microsoft.Extensions.Time.Testing; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class MessagePumpCommandRequeueCountThresholdTests { @@ -53,7 +53,7 @@ public MessagePumpCommandRequeueCountThresholdTests() new SimpleMessageMapperFactory(_ => new MyCommandMessageMapper()), null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), _channel) + _messagePump = new Reactor(provider, messageMapperRegistry, new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), _channel) { Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 }; var message1 = new Message(new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_requeue_count_threshold_for_events_has_been_reached.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_requeue_count_threshold_for_events_has_been_reached.cs similarity index 94% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_requeue_count_threshold_for_events_has_been_reached.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_requeue_count_threshold_for_events_has_been_reached.cs index 95c5b2611f..900205faed 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_requeue_count_threshold_for_events_has_been_reached.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_requeue_count_threshold_for_events_has_been_reached.cs @@ -23,16 +23,16 @@ THE SOFTWARE. */ #endregion using System; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; +using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; -using System.Text.Json; -using Microsoft.Extensions.Time.Testing; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class MessagePumpEventRequeueCountThresholdTests { @@ -54,7 +54,7 @@ public MessagePumpEventRequeueCountThresholdTests() null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), _channel) + _messagePump = new Reactor(provider, messageMapperRegistry, new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), _channel) { Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 }; var message1 = new Message( diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_requeue_of_command_exception_is_thrown.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_requeue_of_command_exception_is_thrown.cs similarity index 94% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_requeue_of_command_exception_is_thrown.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_requeue_of_command_exception_is_thrown.cs index 908c7ee148..cac2921f7b 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_requeue_of_command_exception_is_thrown.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_requeue_of_command_exception_is_thrown.cs @@ -24,15 +24,15 @@ THE SOFTWARE. */ using System; using System.Linq; +using System.Text.Json; using FluentAssertions; +using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; -using System.Text.Json; -using Microsoft.Extensions.Time.Testing; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class MessagePumpCommandRequeueTests { @@ -53,7 +53,7 @@ public MessagePumpCommandRequeueTests() new SimpleMessageMapperFactory(_ => new MyCommandMessageMapper()), null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), channel) + _messagePump = new Reactor(provider, messageMapperRegistry, new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), channel) { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = -1 }; var message1 = new Message( diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_requeue_of_event_exception_is_thrown.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_requeue_of_event_exception_is_thrown.cs similarity index 94% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_requeue_of_event_exception_is_thrown.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_requeue_of_event_exception_is_thrown.cs index 6c283d89d1..d656dec013 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_a_requeue_of_event_exception_is_thrown.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_requeue_of_event_exception_is_thrown.cs @@ -24,15 +24,15 @@ THE SOFTWARE. */ using System; using System.Linq; +using System.Text.Json; using FluentAssertions; +using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; -using System.Text.Json; -using Microsoft.Extensions.Time.Testing; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class MessagePumpEventRequeueTests { @@ -57,7 +57,7 @@ public MessagePumpEventRequeueTests() null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), channel) + _messagePump = new Reactor(provider, messageMapperRegistry, new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), channel) { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = -1 }; var message1 = new Message( diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_event_handler_throws_a_defer_message_Then_message_is_requeued_until_rejected.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_an_event_handler_throws_a_defer_message_Then_message_is_requeued_until_rejected.cs similarity index 95% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_event_handler_throws_a_defer_message_Then_message_is_requeued_until_rejected.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_an_event_handler_throws_a_defer_message_Then_message_is_requeued_until_rejected.cs index d5f971a5cd..866f8c204f 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_event_handler_throws_a_defer_message_Then_message_is_requeued_until_rejected.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_an_event_handler_throws_a_defer_message_Then_message_is_requeued_until_rejected.cs @@ -27,10 +27,10 @@ THE SOFTWARE. */ using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class MessagePumpEventProcessingDeferMessageActionTests { @@ -62,7 +62,7 @@ public MessagePumpEventProcessingDeferMessageActionTests() null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) + _messagePump = new Reactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) { Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = _requeueCount }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_event_handler_throws_unhandled_exception_Then_message_is_acked.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_an_event_handler_throws_unhandled_exception_Then_message_is_acked.cs similarity index 95% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_event_handler_throws_unhandled_exception_Then_message_is_acked.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_an_event_handler_throws_unhandled_exception_Then_message_is_acked.cs index 5f9ab45723..44c64fce44 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_event_handler_throws_unhandled_exception_Then_message_is_acked.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_an_event_handler_throws_unhandled_exception_Then_message_is_acked.cs @@ -28,12 +28,12 @@ THE SOFTWARE. */ using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; using Serilog.Events; using Serilog.Sinks.TestCorrelator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class MessagePumpEventProcessingExceptionTests { @@ -63,7 +63,7 @@ public MessagePumpEventProcessingExceptionTests() messageMapperRegistry.Register(); var requestContextFactory = new InMemoryRequestContextFactory(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, null, requestContextFactory, _channel) + _messagePump = new Reactor(provider, messageMapperRegistry, null, requestContextFactory, _channel) { Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = _requeueCount }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_unacceptable_message_is_recieved.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_an_unacceptable_message_is_recieved.cs similarity index 94% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_unacceptable_message_is_recieved.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_an_unacceptable_message_is_recieved.cs index 8f524a157d..340a4b15da 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_unacceptable_message_is_recieved.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_an_unacceptable_message_is_recieved.cs @@ -28,10 +28,10 @@ THE SOFTWARE. */ using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class MessagePumpUnacceptableMessageTests { @@ -56,7 +56,7 @@ public MessagePumpUnacceptableMessageTests() null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) + _messagePump = new Reactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel) { Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_unacceptable_message_limit_is_reached.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_an_unacceptable_message_limit_is_reached.cs similarity index 95% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_unacceptable_message_limit_is_reached.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_an_unacceptable_message_limit_is_reached.cs index 8f1e55fb73..413f22b4b2 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_an_unacceptable_message_limit_is_reached.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_an_unacceptable_message_limit_is_reached.cs @@ -28,10 +28,10 @@ THE SOFTWARE. */ using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class MessagePumpUnacceptableMessageLimitBreachedTests { @@ -54,7 +54,7 @@ public MessagePumpUnacceptableMessageLimitBreachedTests() null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), channel) + _messagePump = new Reactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), channel) { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3, UnacceptableMessageLimit = 3 }; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_reading_a_message_from_a_channel_pump_out_to_command_processor.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_reading_a_message_from_a_channel_pump_out_to_command_processor.cs similarity index 93% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_reading_a_message_from_a_channel_pump_out_to_command_processor.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_reading_a_message_from_a_channel_pump_out_to_command_processor.cs index 634d1c9ce8..ead38bff40 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_reading_a_message_from_a_channel_pump_out_to_command_processor.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_reading_a_message_from_a_channel_pump_out_to_command_processor.cs @@ -23,15 +23,15 @@ THE SOFTWARE. */ #endregion using System; +using System.Text.Json; using FluentAssertions; +using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; -using System.Text.Json; -using Microsoft.Extensions.Time.Testing; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class MessagePumpToCommandProcessorTests { @@ -57,7 +57,7 @@ public MessagePumpToCommandProcessorTests() null); messagerMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messagerMapperRegistry, new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), channel) + _messagePump = new Reactor(provider, messagerMapperRegistry, new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), channel) { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000) }; _event = new MyEvent(); diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_running_a_message_pump_on_a_thread_should_be_able_to_stop.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_running_a_message_pump_on_a_thread_should_be_able_to_stop.cs similarity index 93% rename from tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_running_a_message_pump_on_a_thread_should_be_able_to_stop.cs rename to tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_running_a_message_pump_on_a_thread_should_be_able_to_stop.cs index ff3aab9442..10b6d501a7 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/When_running_a_message_pump_on_a_thread_should_be_able_to_stop.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_running_a_message_pump_on_a_thread_should_be_able_to_stop.cs @@ -23,16 +23,16 @@ THE SOFTWARE. */ #endregion using System; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; +using Microsoft.Extensions.Time.Testing; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -using Xunit; using Paramore.Brighter.ServiceActivator; -using System.Text.Json; -using Microsoft.Extensions.Time.Testing; +using Xunit; -namespace Paramore.Brighter.Core.Tests.MessageDispatch +namespace Paramore.Brighter.Core.Tests.MessageDispatch.Reactor { public class PerformerCanStopTests { @@ -57,7 +57,7 @@ public PerformerCanStopTests() null); messageMapperRegistry.Register(); - var messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), channel); + var messagePump = new Reactor(provider, messageMapperRegistry, new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), channel); messagePump.Channel = channel; messagePump.TimeOut = TimeSpan.FromMilliseconds(5000); diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/FailingChannel.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/FailingChannel.cs index 7b6f5d2d13..5c326a8937 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/FailingChannel.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/FailingChannel.cs @@ -27,7 +27,7 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles { - internal class FailingChannel(ChannelName channelName, RoutingKey topic, IAmAMessageConsumer messageConsumer, int maxQueueLength= 1, bool brokenCircuit = false) + internal class FailingChannel(ChannelName channelName, RoutingKey topic, IAmAMessageConsumerSync messageConsumer, int maxQueueLength= 1, bool brokenCircuit = false) : Channel(channelName, topic, messageConsumer, maxQueueLength) { public int NumberOfRetries { get; set; } = 0; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/FailingChannelAsync.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/FailingChannelAsync.cs new file mode 100644 index 0000000000..355d3a56d8 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/FailingChannelAsync.cs @@ -0,0 +1,57 @@ +#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.Threading; +using System.Threading.Tasks; +using Polly.CircuitBreaker; + +namespace Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles +{ + internal class FailingChannelAsync(ChannelName channelName, RoutingKey topic, IAmAMessageConsumerAsync messageConsumer, int maxQueueLength= 1, bool brokenCircuit = false) + : ChannelAsync(channelName, topic, messageConsumer, maxQueueLength) + { + public int NumberOfRetries { get; set; } = 0; + private int _attempts = 0; + + + public override async Task ReceiveAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + if (_attempts <= NumberOfRetries) + { + _attempts++; + var channelFailureException = new ChannelFailureException("Test general failure", new Exception("inner test exception")); + if (brokenCircuit) + { + var brokenCircuitException = new BrokenCircuitException("An inner broken circuit exception"); + channelFailureException = new ChannelFailureException("Test broken circuit failure", brokenCircuitException); + } + + throw channelFailureException; + } + + return await base.ReceiveAsync(timeout, cancellationToken); + } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_A_Stop_Message_Is_Added_To_A_Channel.cs b/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_A_Stop_Message_Is_Added_To_A_Channel.cs index 68af9c7baa..86cf3ce433 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_A_Stop_Message_Is_Added_To_A_Channel.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_A_Stop_Message_Is_Added_To_A_Channel.cs @@ -31,13 +31,13 @@ public class ChannelStopTests { private readonly RoutingKey _routingKey = new("myTopic"); private const string ChannelName = "myChannel"; - private readonly IAmAChannel _channel; + private readonly IAmAChannelSync _channel; private readonly InternalBus _bus; public ChannelStopTests() { _bus = new InternalBus(); - IAmAMessageConsumer gateway = new InMemoryMessageConsumer(_routingKey, _bus, TimeProvider.System, TimeSpan.FromMilliseconds(1000)); + IAmAMessageConsumerSync gateway = new InMemoryMessageConsumer(_routingKey, _bus, TimeProvider.System, TimeSpan.FromMilliseconds(1000)); _channel = new Channel(new(ChannelName),_routingKey, gateway); diff --git a/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_Acknowledge_Is_Called_On_A_Channel.cs b/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_Acknowledge_Is_Called_On_A_Channel.cs index 7ef5a04c2e..75a6a0647a 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_Acknowledge_Is_Called_On_A_Channel.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_Acknowledge_Is_Called_On_A_Channel.cs @@ -30,7 +30,7 @@ namespace Paramore.Brighter.Core.Tests.MessagingGateway { public class ChannelAcknowledgeTests { - private readonly IAmAChannel _channel; + private readonly IAmAChannelSync _channel; private readonly InternalBus _bus = new(); private readonly FakeTimeProvider _fakeTimeProvider = new(); private readonly RoutingKey Topic = new("myTopic"); @@ -38,7 +38,7 @@ public class ChannelAcknowledgeTests public ChannelAcknowledgeTests() { - IAmAMessageConsumer gateway = new InMemoryMessageConsumer(new RoutingKey(Topic), _bus, _fakeTimeProvider, TimeSpan.FromMilliseconds(1000)); + IAmAMessageConsumerSync gateway = new InMemoryMessageConsumer(new RoutingKey(Topic), _bus, _fakeTimeProvider, TimeSpan.FromMilliseconds(1000)); _channel = new Channel(new (ChannelName), new(Topic), gateway); diff --git a/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_Listening_To_Messages_On_A_Channel.cs b/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_Listening_To_Messages_On_A_Channel.cs index 05f625dba7..4d5e6951c8 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_Listening_To_Messages_On_A_Channel.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_Listening_To_Messages_On_A_Channel.cs @@ -31,7 +31,7 @@ namespace Paramore.Brighter.Core.Tests.MessagingGateway { public class ChannelMessageReceiveTests { - private readonly IAmAChannel _channel; + private readonly IAmAChannelSync _channel; private readonly RoutingKey _routingKey = new("MyTopic"); private const string ChannelName = "myChannel"; private readonly InternalBus _bus = new(); @@ -40,7 +40,7 @@ public class ChannelMessageReceiveTests public ChannelMessageReceiveTests() { - IAmAMessageConsumer gateway = new InMemoryMessageConsumer(new RoutingKey(_routingKey), _bus, _fakeTimeProvider, TimeSpan.FromMilliseconds(1000)); + IAmAMessageConsumerSync gateway = new InMemoryMessageConsumer(new RoutingKey(_routingKey), _bus, _fakeTimeProvider, TimeSpan.FromMilliseconds(1000)); _channel = new Channel(new(ChannelName),new(_routingKey), gateway); diff --git a/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_No_Acknowledge_Is_Called_On_A_Channel.cs b/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_No_Acknowledge_Is_Called_On_A_Channel.cs index b4f027fc83..0ca98af541 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_No_Acknowledge_Is_Called_On_A_Channel.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_No_Acknowledge_Is_Called_On_A_Channel.cs @@ -30,7 +30,7 @@ namespace Paramore.Brighter.Core.Tests.MessagingGateway { public class ChannelNackTests { - private readonly IAmAChannel _channel; + private readonly IAmAChannelSync _channel; private readonly InternalBus _bus = new(); private readonly FakeTimeProvider _timeProvider = new(); private readonly RoutingKey _routingKey = new("myTopic"); @@ -38,7 +38,7 @@ public class ChannelNackTests public ChannelNackTests() { - IAmAMessageConsumer gateway = new InMemoryMessageConsumer(new RoutingKey(_routingKey), _bus, _timeProvider, TimeSpan.FromMilliseconds(1000)); + IAmAMessageConsumerSync gateway = new InMemoryMessageConsumer(new RoutingKey(_routingKey), _bus, _timeProvider, TimeSpan.FromMilliseconds(1000)); _channel = new Channel(new(ChannelName), _routingKey, gateway); diff --git a/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_Requeuing_A_Message_With_No_Delay.cs b/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_Requeuing_A_Message_With_No_Delay.cs index 652506cb91..00981ad4b5 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_Requeuing_A_Message_With_No_Delay.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_Requeuing_A_Message_With_No_Delay.cs @@ -31,7 +31,7 @@ namespace Paramore.Brighter.Core.Tests.MessagingGateway { public class ChannelRequeueWithoutDelayTest { - private readonly IAmAChannel _channel; + private readonly IAmAChannelSync _channel; private readonly RoutingKey _routingKey = new("myTopic"); private const string ChannelName = "myChannel"; private readonly InternalBus _bus = new(); diff --git a/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_The_Buffer_Is_Not_Empty_Read_From_That_Before_Receiving.cs b/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_The_Buffer_Is_Not_Empty_Read_From_That_Before_Receiving.cs index b99be2fcce..d0d95e4a70 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_The_Buffer_Is_Not_Empty_Read_From_That_Before_Receiving.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessagingGateway/When_The_Buffer_Is_Not_Empty_Read_From_That_Before_Receiving.cs @@ -7,8 +7,8 @@ namespace Paramore.Brighter.Core.Tests.MessagingGateway { public class BufferedChannelTests { - private readonly IAmAChannel _channel; - private readonly IAmAMessageConsumer _gateway; + private readonly IAmAChannelSync _channel; + private readonly IAmAMessageConsumerSync _gateway; private const int BufferLimit = 2; private readonly RoutingKey _routingKey = new("MyTopic"); private const string Channel = "MyChannel"; diff --git a/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_A_Message_Is_Dispatched_It_Should_Begin_A_Span.cs b/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_A_Message_Is_Dispatched_It_Should_Begin_A_Span.cs index d5ba95b31e..b49f7c0e9c 100644 --- a/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_A_Message_Is_Dispatched_It_Should_Begin_A_Span.cs +++ b/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_A_Message_Is_Dispatched_It_Should_Begin_A_Span.cs @@ -98,7 +98,7 @@ public MessagePumpDispatchObservabilityTests() null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, null, + _messagePump = new Reactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), channel, tracer, instrumentationOptions) { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000) diff --git a/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Are_No_Messages_Close_The_Span.cs b/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Are_No_Messages_Close_The_Span.cs index 9d5bb32657..1ee8c4017d 100644 --- a/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Are_No_Messages_Close_The_Span.cs +++ b/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Are_No_Messages_Close_The_Span.cs @@ -69,10 +69,10 @@ public MessagePumpEmptyQueueOberservabilityTests() null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, null, + _messagePump = new Reactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), channel, tracer, instrumentationOptions) { - Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000), EmptyChannelDelay = 1000 + Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000), EmptyChannelDelay = TimeSpan.FromMilliseconds(1000) }; //in theory the message pump should see this from the consumer when the queue is empty diff --git a/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Is_A_BrokenCircuit_Channel_Failure_Close_The_Span.cs b/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Is_A_BrokenCircuit_Channel_Failure_Close_The_Span.cs index d7c38f6872..f2b876b87b 100644 --- a/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Is_A_BrokenCircuit_Channel_Failure_Close_The_Span.cs +++ b/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Is_A_BrokenCircuit_Channel_Failure_Close_The_Span.cs @@ -76,10 +76,10 @@ public MessagePumpBrokenCircuitChannelFailureOberservabilityTests() null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, null, + _messagePump = new Reactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), channel, tracer, instrumentationOptions) { - Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000), EmptyChannelDelay = 1000 + Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000), EmptyChannelDelay = TimeSpan.FromMilliseconds(1000) }; var externalActivity = new ActivitySource("Paramore.Brighter.Tests").StartActivity("MessagePumpSpanTests"); diff --git a/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Is_A_Channel_Failure_Close_The_Span.cs b/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Is_A_Channel_Failure_Close_The_Span.cs index 77a620d8d9..88255ad2c1 100644 --- a/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Is_A_Channel_Failure_Close_The_Span.cs +++ b/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Is_A_Channel_Failure_Close_The_Span.cs @@ -75,10 +75,10 @@ public MessagePumpChannelFailureOberservabilityTests() null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, null, + _messagePump = new Reactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), channel, tracer, instrumentationOptions) { - Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000), EmptyChannelDelay = 1000 + Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000), EmptyChannelDelay = TimeSpan.FromMilliseconds(1000) }; var externalActivity = new ActivitySource("Paramore.Brighter.Tests").StartActivity("MessagePumpSpanTests"); diff --git a/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Is_A_Quit_Message_Close_The_Span.cs b/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Is_A_Quit_Message_Close_The_Span.cs index eb39bc60de..c2988303ac 100644 --- a/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Is_A_Quit_Message_Close_The_Span.cs +++ b/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Is_A_Quit_Message_Close_The_Span.cs @@ -71,10 +71,10 @@ public MessagePumpQuitOberservabilityTests() null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, null, + _messagePump = new Reactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), channel, tracer, instrumentationOptions) { - Channel = channel, TimeOut= TimeSpan.FromMilliseconds(5000), EmptyChannelDelay = 1000 + Channel = channel, TimeOut= TimeSpan.FromMilliseconds(5000), EmptyChannelDelay = TimeSpan.FromMilliseconds(1000) }; var quitMessage = MessageFactory.CreateQuitMessage(_routingKey); diff --git a/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Is_An_Unacceptable_Messages_Close_The_Span.cs b/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Is_An_Unacceptable_Messages_Close_The_Span.cs index 1cadd0167e..4ab4f13795 100644 --- a/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Is_An_Unacceptable_Messages_Close_The_Span.cs +++ b/tests/Paramore.Brighter.Core.Tests/Observability/MessageDispatch/When_There_Is_An_Unacceptable_Messages_Close_The_Span.cs @@ -70,10 +70,10 @@ public MessagePumpUnacceptableMessageOberservabilityTests() null); messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, null, + _messagePump = new Reactor(provider, messageMapperRegistry, null, new InMemoryRequestContextFactory(), _channel, tracer, instrumentationOptions) { - Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), EmptyChannelDelay = 1000 + Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), EmptyChannelDelay = TimeSpan.FromMilliseconds(1000) }; //in theory the message pump should see this from the consumer when the queue is empty diff --git a/tests/Paramore.Brighter.Core.Tests/Tasks/BrighterSynchronizationContextsTests.cs b/tests/Paramore.Brighter.Core.Tests/Tasks/BrighterSynchronizationContextsTests.cs new file mode 100644 index 0000000000..c102564b61 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Tasks/BrighterSynchronizationContextsTests.cs @@ -0,0 +1,382 @@ +#region Sources + +// This class is based on Stephen Cleary's AyncContext, see { value = 1; }, + context.Factory.CancellationToken, + context.Factory.CreationOptions | TaskCreationOptions.DenyChildAttach, + context.TaskScheduler); + + context.Execute(task); + + var taskTwo = context.Factory.StartNew( + () => { value = 2; }, + context.Factory.CancellationToken, + context.Factory.CreationOptions | TaskCreationOptions.DenyChildAttach, + TaskScheduler.Default); + + bool threadPoolExceptionRan = false; + try + { + await taskTwo.ContinueWith(_ => throw new Exception("Should run on thread pool"), TaskScheduler.Default); + } + catch (Exception e) + { + e.Message.Should().Be("Should run on thread pool"); + threadPoolExceptionRan = true; + } + + threadPoolExceptionRan.Should().BeTrue(); + } + + [Fact] + public void SynchronizationContext_IsEqualToCopyOfItself() + { + var synchronizationContext1 = + BrighterSynchronizationHelper.Run(() => System.Threading.SynchronizationContext.Current); + var synchronizationContext2 = synchronizationContext1.CreateCopy(); + + synchronizationContext1.GetHashCode().Should().Be(synchronizationContext2.GetHashCode()); + synchronizationContext1.Equals(synchronizationContext2).Should().BeTrue(); + synchronizationContext1.Equals(new System.Threading.SynchronizationContext()).Should().BeFalse(); + } + + [Fact] + public void Id_IsEqualToTaskSchedulerId() + { + var context = new BrighterSynchronizationHelper(); + context.Id.Should().Be(context.TaskScheduler.Id); + } +} diff --git a/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_a_dequeued_item_is_acknowledged_async.cs b/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_a_dequeued_item_is_acknowledged_async.cs new file mode 100644 index 0000000000..7123b349f0 --- /dev/null +++ b/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_a_dequeued_item_is_acknowledged_async.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Paramore.Brighter.InMemory.Tests.Consumer; + +public class AsyncInMemoryConsumerAcknowledgeTests +{ + [Fact] + public async Task When_a_dequeud_item_lock_expires() + { + //arrange + const string myTopic = "my topic"; + var routingKey = new RoutingKey(myTopic); + + var expectedMessage = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_EVENT), + new MessageBody("a test body")); + + var bus = new InternalBus(); + bus.Enqueue(expectedMessage); + + var timeProvider = new FakeTimeProvider(); + var consumer = new InMemoryMessageConsumer(routingKey, bus, timeProvider, TimeSpan.FromMilliseconds(1000)); + + //act + var receivedMessage = await consumer.ReceiveAsync(); + + timeProvider.Advance(TimeSpan.FromSeconds(2)); + + //assert + Assert.Single(bus.Stream(routingKey)); //-- the message should be returned to the bus if there is no Acknowledge or Reject + + } + + [Fact] + public async Task When_a_dequeued_item_is_acknowledged() + { + //arrange + const string myTopic = "my topic"; + var routingKey = new RoutingKey(myTopic); + + var expectedMessage = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_EVENT), + new MessageBody("a test body")); + + var bus = new InternalBus(); + bus.Enqueue(expectedMessage); + + var timeProvider = new FakeTimeProvider(); + var consumer = new InMemoryMessageConsumer(routingKey, bus, timeProvider, TimeSpan.FromMilliseconds(1000)); + + //act + var receivedMessage = await consumer.ReceiveAsync(); + await consumer.AcknowledgeAsync(receivedMessage.Single()); + + timeProvider.Advance(TimeSpan.FromSeconds(2)); //-- the message should be returned to the bus if there is no Acknowledge or Reject + + //assert + Assert.Empty(bus.Stream(routingKey)); + } +} diff --git a/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_a_dequeued_item_is_rejected_async.cs b/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_a_dequeued_item_is_rejected_async.cs new file mode 100644 index 0000000000..0c372c6be6 --- /dev/null +++ b/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_a_dequeued_item_is_rejected_async.cs @@ -0,0 +1,37 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Paramore.Brighter.InMemory.Tests.Consumer; + +public class AsyncInMemoryConsumerRejectTests +{ + [Fact] + public async Task When_a_dequeued_item_is_rejected() + { + //arrange + const string myTopic = "my topic"; + var routingKey = new RoutingKey(myTopic); + + var expectedMessage = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_EVENT), + new MessageBody("a test body")); + + var bus = new InternalBus(); + bus.Enqueue(expectedMessage); + + var timeProvider = new FakeTimeProvider(); + var consumer = new InMemoryMessageConsumer(routingKey, bus, timeProvider, TimeSpan.FromMilliseconds(1000)); + + //act + var receivedMessage = await consumer.ReceiveAsync(); + await consumer.RejectAsync(receivedMessage.Single()); + + timeProvider.Advance(TimeSpan.FromSeconds(2)); //-- the message should be returned to the bus if there is no Acknowledge or Reject + + //assert + Assert.Empty(bus.Stream(routingKey)); + } +} diff --git a/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_an_inmemory_channelfactory_is_called.cs b/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_an_inmemory_channelfactory_is_called.cs index 78793cd8e5..da2e306c9f 100644 --- a/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_an_inmemory_channelfactory_is_called.cs +++ b/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_an_inmemory_channelfactory_is_called.cs @@ -14,7 +14,7 @@ public void When_an_inmemory_channelfactory_is_called() var inMemoryChannelFactory = new InMemoryChannelFactory(internalBus, TimeProvider.System); //act - var channel = inMemoryChannelFactory.CreateChannel(new Subscription(typeof(MyEvent))); + var channel = inMemoryChannelFactory.CreateSyncChannel(new Subscription(typeof(MyEvent))); //assert Assert.NotNull(channel); diff --git a/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_reading_messages_via_a_consumer.cs b/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_reading_messages_via_a_consumer.cs index acdd624348..3767de6448 100644 --- a/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_reading_messages_via_a_consumer.cs +++ b/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_reading_messages_via_a_consumer.cs @@ -5,7 +5,7 @@ namespace Paramore.Brighter.InMemory.Tests.Consumer; -public class InMemoryConsumerRecieveTests +public class InMemoryConsumerReceiveTests { private readonly InMemoryConsumerRequeueTests _inMemoryConsumerRequeueTests = new InMemoryConsumerRequeueTests(); private readonly InMemoryConsumerAcknowledgeTests _inMemoryConsumerAcknowledgeTests = new InMemoryConsumerAcknowledgeTests(); diff --git a/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_reading_messages_via_a_consumer_async.cs b/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_reading_messages_via_a_consumer_async.cs new file mode 100644 index 0000000000..8ad3138af8 --- /dev/null +++ b/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_reading_messages_via_a_consumer_async.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Paramore.Brighter.InMemory.Tests.Consumer; + +public class AsyncInMemoryConsumerReceiveTests +{ + private readonly InMemoryConsumerRequeueTests _inMemoryConsumerRequeueTests = new InMemoryConsumerRequeueTests(); + private readonly InMemoryConsumerAcknowledgeTests _inMemoryConsumerAcknowledgeTests = new InMemoryConsumerAcknowledgeTests(); + private readonly InMemoryConsumerRejectTests _inMemoryConsumerRejectTests = new InMemoryConsumerRejectTests(); + + [Fact] + public async Task When_reading_messages_via_a_consumer() + { + //arrange + const string myTopic = "my topic"; + var routingKey = new RoutingKey(myTopic); + + var expectedMessage = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_EVENT), + new MessageBody("a test body")); + + + var bus = new InternalBus(); + bus.Enqueue(expectedMessage); + + var consumer = new InMemoryMessageConsumer(routingKey, bus, new FakeTimeProvider(), TimeSpan.FromMilliseconds(1000)); + + //act + var receivedMessage = await consumer.ReceiveAsync(); + var message = receivedMessage.Single(); + await consumer.AcknowledgeAsync(message); + + //assert + Assert.Equal(expectedMessage, message); + Assert.Empty(bus.Stream(routingKey)); + } +} diff --git a/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_requeueing_a_message_it_should_be_available_again_async.cs b/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_requeueing_a_message_it_should_be_available_again_async.cs new file mode 100644 index 0000000000..c2751130f9 --- /dev/null +++ b/tests/Paramore.Brighter.InMemory.Tests/Consumer/When_requeueing_a_message_it_should_be_available_again_async.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Paramore.Brighter.InMemory.Tests.Consumer; + +public class AsyncInMemoryConsumerRequeueTests +{ + [Fact] + public async Task When_requeueing_a_message_it_should_be_available_again() + { + //arrange + const string myTopic = "my topic"; + var routingKey = new RoutingKey(myTopic); + + var expectedMessage = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_EVENT), + new MessageBody("a test body")); + + var bus = new InternalBus(); + bus.Enqueue(expectedMessage); + + var consumer = new InMemoryMessageConsumer(routingKey, bus, new FakeTimeProvider(), TimeSpan.FromMilliseconds(1000)); + + //act + var receivedMessage = await consumer.ReceiveAsync(); + await consumer.RequeueAsync(receivedMessage.Single(), TimeSpan.Zero); + + //assert + Assert.Single(bus.Stream(routingKey)); + + } + + [Fact] + public async Task When_requeueing_a_message_with_a_delay_it_should_not_be_available_immediately() + { + //arrange + const string myTopic = "my topic"; + var routingKey = new RoutingKey(myTopic); + + var expectedMessage = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_EVENT), + new MessageBody("a test body")); + + var bus = new InternalBus(); + bus.Enqueue(expectedMessage); + + var timeProvider = new FakeTimeProvider(); + var consumer = new InMemoryMessageConsumer(routingKey, bus, timeProvider, TimeSpan.FromMilliseconds(1000)); + + //act + var receivedMessage = await consumer.ReceiveAsync(); + await consumer.RequeueAsync(receivedMessage.Single(), TimeSpan.FromMilliseconds(1000)); + + //assert + Assert.Empty(bus.Stream(routingKey)); + + timeProvider.Advance(TimeSpan.FromSeconds(2)); + + Assert.Single(bus.Stream(routingKey)); + } +} diff --git a/tests/Paramore.Brighter.InMemory.Tests/Sweeper/When_sweeping_the_outbox.cs b/tests/Paramore.Brighter.InMemory.Tests/Sweeper/When_sweeping_the_outbox.cs index 9d684d1680..5a31377efa 100644 --- a/tests/Paramore.Brighter.InMemory.Tests/Sweeper/When_sweeping_the_outbox.cs +++ b/tests/Paramore.Brighter.InMemory.Tests/Sweeper/When_sweeping_the_outbox.cs @@ -287,7 +287,7 @@ public async Task When_too_new_to_sweep_leaves_them_async() mapperRegistry.Register(); - var bus = new OutboxProducerMediator( + var mediator = new OutboxProducerMediator( producerRegistry, new DefaultPolicy(), mapperRegistry, @@ -302,7 +302,7 @@ public async Task When_too_new_to_sweep_leaves_them_async() var commandProcessor = new CommandProcessor( new InMemoryRequestContextFactory(), new PolicyRegistry(), - bus); + mediator); var sweeper = new OutboxSweeper(timeSinceSent, commandProcessor, new InMemoryRequestContextFactory()); diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_message_is_acknowledged_update_offset.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_message_is_acknowledged_update_offset.cs index b756854a39..f4dd0ec8ed 100644 --- a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_message_is_acknowledged_update_offset.cs +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_message_is_acknowledged_update_offset.cs @@ -8,158 +8,158 @@ using Xunit; using Xunit.Abstractions; -namespace Paramore.Brighter.Kafka.Tests.MessagingGateway +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Trait("Fragile", "CI")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaMessageConsumerUpdateOffset : IDisposable { - [Trait("Category", "Kafka")] - [Trait("Fragile", "CI")] - [Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition - public class KafkaMessageConsumerUpdateOffset : IDisposable + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly string _partitionKey = Guid.NewGuid().ToString(); + + public KafkaMessageConsumerUpdateOffset(ITestOutputHelper output) { - private readonly ITestOutputHelper _output; - private readonly string _queueName = Guid.NewGuid().ToString(); - private readonly string _topic = Guid.NewGuid().ToString(); - private readonly IAmAProducerRegistry _producerRegistry; - private readonly string _partitionKey = Guid.NewGuid().ToString(); + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {"localhost:9092"} + }, + new[] {new KafkaPublication + { + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 1, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 2000, + RequestTimeoutMs = 2000, + MakeChannels = OnMissingChannel.Create + }}).Create(); + } - public KafkaMessageConsumerUpdateOffset(ITestOutputHelper output) + [Fact] + public async Task When_a_message_is_acknowldgede_update_offset() + { + var groupId = Guid.NewGuid().ToString(); + + //send x messages to Kafka + var sentMessages = new string[10]; + for (int i = 0; i < 10; i++) { - _output = output; - _producerRegistry = new KafkaProducerRegistryFactory( - new KafkaMessagingGatewayConfiguration - { - Name = "Kafka Producer Send Test", - BootStrapServers = new[] {"localhost:9092"} - }, - new[] {new KafkaPublication - { - Topic = new RoutingKey(_topic), - NumPartitions = 1, - ReplicationFactor = 1, - //These timeouts support running on a container using the same host as the tests, - //your production values ought to be lower - MessageTimeoutMs = 2000, - RequestTimeoutMs = 2000, - MakeChannels = OnMissingChannel.Create - }}).Create(); + var msgId = Guid.NewGuid().ToString(); + SendMessage(msgId); + sentMessages[i] = msgId; } - [Fact] - public async Task When_a_message_is_acknowldgede_update_offset() - { - var groupId = Guid.NewGuid().ToString(); - - //send x messages to Kafka - var sentMessages = new string[10]; - for (int i = 0; i < 10; i++) - { - var msgId = Guid.NewGuid().ToString(); - SendMessage(msgId); - sentMessages[i] = msgId; - } - - //This will create, then delete the consumer - Message[] messages = ConsumeMessages(groupId: groupId, batchLimit: 5); + //This will create, then delete the consumer + Message[] messages = ConsumeMessages(groupId: groupId, batchLimit: 5); - //check we read the first 5 messages - messages.Length.Should().Be(5); - for (int i = 0; i < 5; i++) - { - messages[i].Id.Should().Be(sentMessages[i]); - } + //check we read the first 5 messages + messages.Length.Should().Be(5); + for (int i = 0; i < 5; i++) + { + messages[i].Id.Should().Be(sentMessages[i]); + } - //yield to broker to catch up - await Task.Delay(TimeSpan.FromSeconds(5)); + //yield to broker to catch up + await Task.Delay(TimeSpan.FromSeconds(5)); - //This will create a new consumer - Message[] newMessages = ConsumeMessages(groupId, batchLimit: 5); - //check we read the first 5 messages - messages.Length.Should().Be(5); - for (int i = 0; i < 5; i++) - { - newMessages[i].Id.Should().Be(sentMessages[i+5]); - } + //This will create a new consumer + Message[] newMessages = ConsumeMessages(groupId, batchLimit: 5); + //check we read the first 5 messages + messages.Length.Should().Be(5); + for (int i = 0; i < 5; i++) + { + newMessages[i].Id.Should().Be(sentMessages[i+5]); } + } - private void SendMessage(string messageId) - { - var routingKey = new RoutingKey(_topic); + private void SendMessage(string messageId) + { + var routingKey = new RoutingKey(_topic); - ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send( - new Message( - new MessageHeader(messageId, routingKey, MessageType.MT_COMMAND) {PartitionKey = _partitionKey}, - new MessageBody($"test content [{_queueName}]") - ) - ); - } + ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send( + new Message( + new MessageHeader(messageId, routingKey, MessageType.MT_COMMAND) {PartitionKey = _partitionKey}, + new MessageBody($"test content [{_queueName}]") + ) + ); + } - private Message[] ConsumeMessages(string groupId, int batchLimit) + private Message[] ConsumeMessages(string groupId, int batchLimit) + { + var consumedMessages = new List(); + using (IAmAMessageConsumerSync consumer = CreateConsumer(groupId)) { - var consumedMessages = new List(); - using (IAmAMessageConsumer consumer = CreateConsumer(groupId)) + for (int i = 0; i < batchLimit; i++) { - for (int i = 0; i < batchLimit; i++) - { - consumedMessages.Add(ConsumeMessage(consumer)); - } + consumedMessages.Add(ConsumeMessage(consumer)); } + } - return consumedMessages.ToArray(); + return consumedMessages.ToArray(); - Message ConsumeMessage(IAmAMessageConsumer consumer) + Message ConsumeMessage(IAmAMessageConsumerSync consumer) + { + Message[] messages = new []{new Message()}; + int maxTries = 0; + do { - Message[] messages = new []{new Message()}; - int maxTries = 0; - do + try { - try - { - maxTries++; - Task.Delay(500).Wait(); //Let topic propagate in the broker - messages = consumer.Receive(TimeSpan.FromMilliseconds(1000)); + maxTries++; + Task.Delay(500).Wait(); //Let topic propagate in the broker + messages = consumer.Receive(TimeSpan.FromMilliseconds(1000)); - if (messages[0].Header.MessageType != MessageType.MT_NONE) - { - consumer.Acknowledge(messages[0]); - return messages[0]; - } - - } - catch (ChannelFailureException cfx) + if (messages[0].Header.MessageType != MessageType.MT_NONE) { - //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing - _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + consumer.Acknowledge(messages[0]); + return messages[0]; } - } while (maxTries <= 3); - return messages[0]; - } + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } + } while (maxTries <= 3); + + return messages[0]; } + } - private IAmAMessageConsumer CreateConsumer(string groupId) - { - return new KafkaMessageConsumerFactory( + private IAmAMessageConsumerSync CreateConsumer(string groupId) + { + return new KafkaMessageConsumerFactory( new KafkaMessagingGatewayConfiguration { Name = "Kafka Consumer Test", BootStrapServers = new[] {"localhost:9092"} }) - .Create(new KafkaSubscription - ( - name: new SubscriptionName("Paramore.Brighter.Tests"), - channelName: new ChannelName(_queueName), - routingKey: new RoutingKey(_topic), - groupId: groupId, - offsetDefault: AutoOffsetReset.Earliest, - commitBatchSize:5, - numOfPartitions: 1, - replicationFactor: 1, - makeChannels: OnMissingChannel.Create - )); - } + .Create(new KafkaSubscription + ( + name: new SubscriptionName("Paramore.Brighter.Tests"), + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: groupId, + offsetDefault: AutoOffsetReset.Earliest, + commitBatchSize:5, + numOfPartitions: 1, + replicationFactor: 1, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create + )); + } - public void Dispose() - { - _producerRegistry?.Dispose(); - } + public void Dispose() + { + _producerRegistry?.Dispose(); } } diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_message_is_acknowledged_update_offset_async.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_message_is_acknowledged_update_offset_async.cs new file mode 100644 index 0000000000..32e16a021a --- /dev/null +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_message_is_acknowledged_update_offset_async.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Confluent.Kafka; +using FluentAssertions; +using Paramore.Brighter.Kafka.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.Kafka; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Trait("Fragile", "CI")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaMessageConsumerUpdateOffsetAsync : IDisposable +{ + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly string _partitionKey = Guid.NewGuid().ToString(); + + public KafkaMessageConsumerUpdateOffsetAsync(ITestOutputHelper output) + { + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {"localhost:9092"} + }, + new[] {new KafkaPublication + { + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 1, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 2000, + RequestTimeoutMs = 2000, + MakeChannels = OnMissingChannel.Create + }}).Create(); + } + + [Fact] + public async Task When_a_message_is_acknowldgede_update_offset() + { + var groupId = Guid.NewGuid().ToString(); + + //send x messages to Kafka + var sentMessages = new string[10]; + for (int i = 0; i < 10; i++) + { + var msgId = Guid.NewGuid().ToString(); + await SendMessageAsync(msgId); + sentMessages[i] = msgId; + } + + //This will create, then delete the consumer + Message[] messages = await ConsumeMessagesAsync(groupId: groupId, batchLimit: 5); + + //check we read the first 5 messages + messages.Length.Should().Be(5); + for (int i = 0; i < 5; i++) + { + messages[i].Id.Should().Be(sentMessages[i]); + } + + //yield to broker to catch up + await Task.Delay(TimeSpan.FromSeconds(5)); + + //This will create a new consumer + Message[] newMessages = await ConsumeMessagesAsync(groupId, batchLimit: 5); + //check we read the first 5 messages + newMessages.Length.Should().Be(5); + for (int i = 0; i < 5; i++) + { + newMessages[i].Id.Should().Be(sentMessages[i+5]); + } + } + + private async Task SendMessageAsync(string messageId) + { + var routingKey = new RoutingKey(_topic); + + await ((IAmAMessageProducerAsync)_producerRegistry.LookupBy(routingKey)).SendAsync( + new Message( + new MessageHeader(messageId, routingKey, MessageType.MT_COMMAND) {PartitionKey = _partitionKey}, + new MessageBody($"test content [{_queueName}]") + ) + ); + } + + private async Task ConsumeMessagesAsync(string groupId, int batchLimit) + { + var consumedMessages = new List(); + await using (IAmAMessageConsumerAsync consumer = CreateConsumer(groupId)) + { + for (int i = 0; i < batchLimit; i++) + { + consumedMessages.Add(await ConsumeMessageAsync(consumer)); + } + } + + return consumedMessages.ToArray(); + + async Task ConsumeMessageAsync(IAmAMessageConsumerAsync consumer) + { + Message[] messages = new []{new Message()}; + int maxTries = 0; + do + { + try + { + maxTries++; + await Task.Delay(500); //Let topic propagate in the broker + messages = await consumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000)); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + { + await consumer.AcknowledgeAsync(messages[0]); + return messages[0]; + } + + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } + } while (maxTries <= 3); + + return messages[0]; + } + } + + private IAmAMessageConsumerAsync CreateConsumer(string groupId) + { + return new KafkaMessageConsumerFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Consumer Test", + BootStrapServers = new[] {"localhost:9092"} + }) + .CreateAsync(new KafkaSubscription + ( + name: new SubscriptionName("Paramore.Brighter.Tests"), + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: groupId, + offsetDefault: AutoOffsetReset.Earliest, + commitBatchSize:5, + numOfPartitions: 1, + replicationFactor: 1, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + )); + } + + public void Dispose() + { + _producerRegistry.Dispose(); + } +} diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_set_of_messages_is_sent_preserve_order.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_set_of_messages_is_sent_preserve_order.cs index 4fe9481c84..5f6a841783 100644 --- a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_set_of_messages_is_sent_preserve_order.cs +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_set_of_messages_is_sent_preserve_order.cs @@ -9,153 +9,153 @@ using Xunit; using Xunit.Abstractions; -namespace Paramore.Brighter.Kafka.Tests.MessagingGateway +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaMessageConsumerPreservesOrder : IDisposable { - [Trait("Category", "Kafka")] - [Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition - public class KafkaMessageConsumerPreservesOrder : IDisposable - { - private readonly ITestOutputHelper _output; - private readonly string _queueName = Guid.NewGuid().ToString(); - private readonly string _topic = Guid.NewGuid().ToString(); - private readonly IAmAProducerRegistry _producerRegistry; - private readonly string _partitionKey = Guid.NewGuid().ToString(); - private readonly string _kafkaGroupId = Guid.NewGuid().ToString(); + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly string _partitionKey = Guid.NewGuid().ToString(); + private readonly string _kafkaGroupId = Guid.NewGuid().ToString(); - public KafkaMessageConsumerPreservesOrder (ITestOutputHelper output) - { - _output = output; - _producerRegistry = new KafkaProducerRegistryFactory( - new KafkaMessagingGatewayConfiguration - { - Name = "Kafka Producer Send Test", - BootStrapServers = new[] {"localhost:9092"} - }, - new[] {new KafkaPublication - { - Topic = new RoutingKey(_topic), - NumPartitions = 1, - ReplicationFactor = 1, - //These timeouts support running on a container using the same host as the tests, - //your production values ought to be lower - MessageTimeoutMs = 2000, - RequestTimeoutMs = 2000, - MakeChannels = OnMissingChannel.Create - }}).Create(); - } + public KafkaMessageConsumerPreservesOrder (ITestOutputHelper output) + { + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {"localhost:9092"} + }, + new[] {new KafkaPublication + { + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 1, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 2000, + RequestTimeoutMs = 2000, + MakeChannels = OnMissingChannel.Create + }}).Create(); + } - [Fact] - public void When_a_message_is_sent_keep_order() + [Fact] + public void When_a_message_is_sent_keep_order() + { + IAmAMessageConsumerSync consumer = null; + try { - IAmAMessageConsumer consumer = null; - try - { - //Send a sequence of messages to Kafka - var msgId = SendMessage(); - var msgId2 = SendMessage(); - var msgId3 = SendMessage(); - var msgId4 = SendMessage(); + //Send a sequence of messages to Kafka + var msgId = SendMessage(); + var msgId2 = SendMessage(); + var msgId3 = SendMessage(); + var msgId4 = SendMessage(); - consumer = CreateConsumer(); + consumer = CreateConsumer(); - //Now read those messages in order + //Now read those messages in order - var firstMessage = ConsumeMessages(consumer); - var message = firstMessage.First(); - message.Id.Should().Be(msgId); - consumer.Acknowledge(message); + var firstMessage = ConsumeMessages(consumer); + var message = firstMessage.First(); + message.Id.Should().Be(msgId); + consumer.Acknowledge(message); - var secondMessage = ConsumeMessages(consumer); - message = secondMessage.First(); - message.Id.Should().Be(msgId2); - consumer.Acknowledge(message); + var secondMessage = ConsumeMessages(consumer); + message = secondMessage.First(); + message.Id.Should().Be(msgId2); + consumer.Acknowledge(message); - var thirdMessages = ConsumeMessages(consumer); - message = thirdMessages .First(); - message.Id.Should().Be(msgId3); - consumer.Acknowledge(message); + var thirdMessages = ConsumeMessages(consumer); + message = thirdMessages .First(); + message.Id.Should().Be(msgId3); + consumer.Acknowledge(message); - var fourthMessage = ConsumeMessages(consumer); - message = fourthMessage .First(); - message.Id.Should().Be(msgId4); - consumer.Acknowledge(message); + var fourthMessage = ConsumeMessages(consumer); + message = fourthMessage .First(); + message.Id.Should().Be(msgId4); + consumer.Acknowledge(message); - } - finally - { - consumer?.Dispose(); - } } - - private string SendMessage() + finally { - var messageId = Guid.NewGuid().ToString(); + consumer?.Dispose(); + } + } - var routingKey = new RoutingKey(_topic); + private string SendMessage() + { + var messageId = Guid.NewGuid().ToString(); + + var routingKey = new RoutingKey(_topic); - ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send( - new Message( - new MessageHeader(messageId, routingKey, MessageType.MT_COMMAND) - { - PartitionKey = _partitionKey - }, - new MessageBody($"test content [{_queueName}]") - ) - ); + ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send( + new Message( + new MessageHeader(messageId, routingKey, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey + }, + new MessageBody($"test content [{_queueName}]") + ) + ); - return messageId; - } + return messageId; + } - private IEnumerable ConsumeMessages(IAmAMessageConsumer consumer) + private IEnumerable ConsumeMessages(IAmAMessageConsumerSync consumer) + { + var messages = new Message[0]; + int maxTries = 0; + do { - var messages = new Message[0]; - int maxTries = 0; - do + try { - try - { - maxTries++; - Task.Delay(500).Wait(); //Let topic propagate in the broker - messages = consumer.Receive(TimeSpan.FromMilliseconds(1000)); + maxTries++; + Task.Delay(500).Wait(); //Let topic propagate in the broker + messages = consumer.Receive(TimeSpan.FromMilliseconds(1000)); - if (messages[0].Header.MessageType != MessageType.MT_NONE) - break; - } - catch (ChannelFailureException cfx) - { - //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing - _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); - } - } while (maxTries <= 3); + if (messages[0].Header.MessageType != MessageType.MT_NONE) + break; + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } + } while (maxTries <= 3); - return messages; - } + return messages; + } - private IAmAMessageConsumer CreateConsumer() - { - return new KafkaMessageConsumerFactory( + private IAmAMessageConsumerSync CreateConsumer() + { + return new KafkaMessageConsumerFactory( new KafkaMessagingGatewayConfiguration { Name = "Kafka Consumer Test", BootStrapServers = new[] { "localhost:9092" } }) - .Create(new KafkaSubscription( - name: new SubscriptionName("Paramore.Brighter.Tests"), - channelName: new ChannelName(_queueName), - routingKey: new RoutingKey(_topic), - groupId: _kafkaGroupId, - offsetDefault: AutoOffsetReset.Earliest, - commitBatchSize:1, - numOfPartitions: 1, - replicationFactor: 1, - makeChannels: OnMissingChannel.Create - )); - } + .Create(new KafkaSubscription( + name: new SubscriptionName("Paramore.Brighter.Tests"), + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: _kafkaGroupId, + offsetDefault: AutoOffsetReset.Earliest, + commitBatchSize:1, + numOfPartitions: 1, + replicationFactor: 1, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create + )); + } - public void Dispose() - { - _producerRegistry?.Dispose(); - } + public void Dispose() + { + _producerRegistry?.Dispose(); } } diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_set_of_messages_is_sent_preserve_order_async.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_set_of_messages_is_sent_preserve_order_async.cs new file mode 100644 index 0000000000..dca1c61ded --- /dev/null +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_set_of_messages_is_sent_preserve_order_async.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Confluent.Kafka; +using FluentAssertions; +using Paramore.Brighter.Kafka.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.Kafka; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaMessageConsumerPreservesOrderAsync : IDisposable +{ + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly string _partitionKey = Guid.NewGuid().ToString(); + private readonly string _kafkaGroupId = Guid.NewGuid().ToString(); + + public KafkaMessageConsumerPreservesOrderAsync(ITestOutputHelper output) + { + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {"localhost:9092"} + }, + new[] {new KafkaPublication + { + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 1, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 2000, + RequestTimeoutMs = 2000, + MakeChannels = OnMissingChannel.Create + }}).Create(); + } + + [Fact] + public async Task When_a_message_is_sent_keep_order() + { + IAmAMessageConsumerAsync consumer = null; + try + { + //Send a sequence of messages to Kafka + var msgId = await SendMessageAsync(); + var msgId2 = await SendMessageAsync(); + var msgId3 = await SendMessageAsync(); + var msgId4 = await SendMessageAsync(); + + consumer = CreateConsumer(); + + //Now read those messages in order + + var firstMessage = await ConsumeMessagesAsync(consumer); + var message = firstMessage.First(); + message.Id.Should().Be(msgId); + await consumer.AcknowledgeAsync(message); + + var secondMessage = await ConsumeMessagesAsync(consumer); + message = secondMessage.First(); + message.Id.Should().Be(msgId2); + await consumer.AcknowledgeAsync(message); + + var thirdMessages = await ConsumeMessagesAsync(consumer); + message = thirdMessages.First(); + message.Id.Should().Be(msgId3); + await consumer.AcknowledgeAsync(message); + + var fourthMessage = await ConsumeMessagesAsync(consumer); + message = fourthMessage.First(); + message.Id.Should().Be(msgId4); + await consumer.AcknowledgeAsync(message); + + } + finally + { + if (consumer != null) + { + await consumer.DisposeAsync(); + } + } + } + + private async Task SendMessageAsync() + { + var messageId = Guid.NewGuid().ToString(); + + var routingKey = new RoutingKey(_topic); + + await ((IAmAMessageProducerAsync)_producerRegistry.LookupBy(routingKey)).SendAsync( + new Message( + new MessageHeader(messageId, routingKey, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey + }, + new MessageBody($"test content [{_queueName}]") + ) + ); + + return messageId; + } + + private async Task> ConsumeMessagesAsync(IAmAMessageConsumerAsync consumer) + { + var messages = new Message[0]; + int maxTries = 0; + do + { + try + { + maxTries++; + await Task.Delay(500); //Let topic propagate in the broker + messages = await consumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000)); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + break; + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } + } while (maxTries <= 3); + + return messages; + } + + private IAmAMessageConsumerAsync CreateConsumer() + { + return new KafkaMessageConsumerFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Consumer Test", + BootStrapServers = new[] { "localhost:9092" } + }) + .CreateAsync(new KafkaSubscription( + name: new SubscriptionName("Paramore.Brighter.Tests"), + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: _kafkaGroupId, + offsetDefault: AutoOffsetReset.Earliest, + commitBatchSize:1, + numOfPartitions: 1, + replicationFactor: 1, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + )); + } + + public void Dispose() + { + _producerRegistry.Dispose(); + } +} diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_set_of_messages_is_sent_preserve_order_on_a_confluent_cluster.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_set_of_messages_is_sent_preserve_order_on_a_confluent_cluster.cs index 94afe05e06..6364a853a1 100644 --- a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_set_of_messages_is_sent_preserve_order_on_a_confluent_cluster.cs +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_set_of_messages_is_sent_preserve_order_on_a_confluent_cluster.cs @@ -10,49 +10,49 @@ using Xunit; using Xunit.Abstractions; -namespace Paramore.Brighter.Kafka.Tests.MessagingGateway +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Trait("Category", "Confluent")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaMessageConsumerConfluentPreservesOrder : IDisposable { - [Trait("Category", "Kafka")] - [Trait("Category", "Confluent")] - [Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition - public class KafkaMessageConsumerConfluentPreservesOrder : IDisposable - { - private const string _groupId = "Kafka Message Producer Assume Topic Test"; - private readonly ITestOutputHelper _output; - private readonly string _queueName = Guid.NewGuid().ToString(); - private readonly string _topic = Guid.NewGuid().ToString(); - private readonly IAmAProducerRegistry _producerRegistry; - private readonly string _partitionKey = Guid.NewGuid().ToString(); - private readonly string _bootStrapServer; - private readonly string _userName; - private readonly string _password; + private const string _groupId = "Kafka Message Producer Assume Topic Test"; + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly string _partitionKey = Guid.NewGuid().ToString(); + private readonly string _bootStrapServer; + private readonly string _userName; + private readonly string _password; - public KafkaMessageConsumerConfluentPreservesOrder(ITestOutputHelper output) - { - // -- Confluent supply these values, see their .NET examples for your account - // You need to set those values as environment variables, which we then read, in order - // to run these tests + public KafkaMessageConsumerConfluentPreservesOrder(ITestOutputHelper output) + { + // -- Confluent supply these values, see their .NET examples for your account + // You need to set those values as environment variables, which we then read, in order + // to run these tests - _bootStrapServer = Environment.GetEnvironmentVariable("CONFLUENT_BOOSTRAP_SERVER"); - _userName = Environment.GetEnvironmentVariable("CONFLUENT_SASL_USERNAME"); - _password = Environment.GetEnvironmentVariable("CONFLUENT_SASL_PASSWORD"); + _bootStrapServer = Environment.GetEnvironmentVariable("CONFLUENT_BOOSTRAP_SERVER"); + _userName = Environment.GetEnvironmentVariable("CONFLUENT_SASL_USERNAME"); + _password = Environment.GetEnvironmentVariable("CONFLUENT_SASL_PASSWORD"); - _output = output; - _producerRegistry = new KafkaProducerRegistryFactory( - new KafkaMessagingGatewayConfiguration - { - Name = "Kafka Producer Send Test", - BootStrapServers = new[] {_bootStrapServer}, - SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol.SaslSsl, - SaslMechanisms = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism.Plain, - SaslUsername = _userName, - SaslPassword = _password, - SslCaLocation = SupplyCertificateLocation() + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {_bootStrapServer}, + SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol.SaslSsl, + SaslMechanisms = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism.Plain, + SaslUsername = _userName, + SaslPassword = _password, + SslCaLocation = SupplyCertificateLocation() - }, - new[] {new KafkaPublication - { + }, + new[] {new KafkaPublication + { Topic = new RoutingKey(_topic), NumPartitions = 1, ReplicationFactor = 3, @@ -62,133 +62,133 @@ public KafkaMessageConsumerConfluentPreservesOrder(ITestOutputHelper output) RequestTimeoutMs = 10000, MakeChannels = OnMissingChannel.Create //This will not make the topic } - }).Create(); - } + }).Create(); + } - [Fact] - public void When_a_message_is_sent_keep_order() + [Fact] + public void When_a_message_is_sent_keep_order() + { + IAmAMessageConsumerSync consumer = null; + try { - IAmAMessageConsumer consumer = null; - try - { - //Send a sequence of messages to Kafka - var msgId = SendMessage(); - var msgId2 = SendMessage(); - var msgId3 = SendMessage(); - var msgId4 = SendMessage(); + //Send a sequence of messages to Kafka + var msgId = SendMessage(); + var msgId2 = SendMessage(); + var msgId3 = SendMessage(); + var msgId4 = SendMessage(); - consumer = CreateConsumer(); + consumer = CreateConsumer(); - //Now read those messages in order + //Now read those messages in order - var firstMessage = ConsumeMessages(consumer); - var message = firstMessage.First(); - message.Id.Should().Be(msgId); - consumer.Acknowledge(message); + var firstMessage = ConsumeMessages(consumer); + var message = firstMessage.First(); + message.Id.Should().Be(msgId); + consumer.Acknowledge(message); - var secondMessage = ConsumeMessages(consumer); - message = secondMessage.First(); - message.Id.Should().Be(msgId2); - consumer.Acknowledge(message); + var secondMessage = ConsumeMessages(consumer); + message = secondMessage.First(); + message.Id.Should().Be(msgId2); + consumer.Acknowledge(message); - var thirdMessages = ConsumeMessages(consumer); - message = thirdMessages .First(); - message.Id.Should().Be(msgId3); - consumer.Acknowledge(message); + var thirdMessages = ConsumeMessages(consumer); + message = thirdMessages .First(); + message.Id.Should().Be(msgId3); + consumer.Acknowledge(message); - var fourthMessage = ConsumeMessages(consumer); - message = fourthMessage .First(); - message.Id.Should().Be(msgId4); - consumer.Acknowledge(message); + var fourthMessage = ConsumeMessages(consumer); + message = fourthMessage .First(); + message.Id.Should().Be(msgId4); + consumer.Acknowledge(message); - } - finally - { - consumer?.Dispose(); - } } - - private string SendMessage() + finally { - var messageId = Guid.NewGuid().ToString(); + consumer?.Dispose(); + } + } + + private string SendMessage() + { + var messageId = Guid.NewGuid().ToString(); - var routingKey = new RoutingKey(_topic); + var routingKey = new RoutingKey(_topic); - ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send( - new Message( - new MessageHeader(messageId, routingKey, MessageType.MT_COMMAND) - { - PartitionKey = _partitionKey - }, - new MessageBody($"test content [{_queueName}]") - ) - ); + ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send( + new Message( + new MessageHeader(messageId, routingKey, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey + }, + new MessageBody($"test content [{_queueName}]") + ) + ); - return messageId; - } + return messageId; + } - private IEnumerable ConsumeMessages(IAmAMessageConsumer consumer) + private IEnumerable ConsumeMessages(IAmAMessageConsumerSync consumer) + { + var messages = new Message[0]; + int maxTries = 0; + do { - var messages = new Message[0]; - int maxTries = 0; - do + try { - try - { - maxTries++; - Task.Delay(500).Wait(); //Let topic propagate in the broker - messages = consumer.Receive(TimeSpan.FromMilliseconds(1000)); + maxTries++; + Task.Delay(500).Wait(); //Let topic propagate in the broker + messages = consumer.Receive(TimeSpan.FromMilliseconds(1000)); - if (messages[0].Header.MessageType != MessageType.MT_NONE) - break; - } - catch (ChannelFailureException cfx) - { - //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing - _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); - } - } while (maxTries <= 3); + if (messages[0].Header.MessageType != MessageType.MT_NONE) + break; + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } + } while (maxTries <= 3); - return messages; - } + return messages; + } - private IAmAMessageConsumer CreateConsumer() - { - return new KafkaMessageConsumerFactory( - new KafkaMessagingGatewayConfiguration - { - Name = "Kafka Producer Send Test", - BootStrapServers = new[] {_bootStrapServer}, - SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol.SaslSsl, - SaslMechanisms = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism.Plain, - SaslUsername = _userName, - SaslPassword = _password, - SslCaLocation = SupplyCertificateLocation() + private IAmAMessageConsumerSync CreateConsumer() + { + return new KafkaMessageConsumerFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {_bootStrapServer}, + SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol.SaslSsl, + SaslMechanisms = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism.Plain, + SaslUsername = _userName, + SaslPassword = _password, + SslCaLocation = SupplyCertificateLocation() - }) - .Create(new KafkaSubscription( - channelName: new ChannelName(_queueName), - routingKey: new RoutingKey(_topic), - groupId: _groupId, - offsetDefault: AutoOffsetReset.Earliest, - commitBatchSize:1, - numOfPartitions: 1, - replicationFactor: 3, - makeChannels: OnMissingChannel.Create - )); - } + }) + .Create(new KafkaSubscription( + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: _groupId, + offsetDefault: AutoOffsetReset.Earliest, + commitBatchSize:1, + numOfPartitions: 1, + replicationFactor: 3, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create + )); + } - public void Dispose() - { - _producerRegistry?.Dispose(); - } + public void Dispose() + { + _producerRegistry?.Dispose(); + } - private string SupplyCertificateLocation() - { - //For different platforms, we have to figure out how to get the connection right - //see: https://docs.confluent.io/platform/current/tutorials/examples/clients/docs/csharp.html + private string SupplyCertificateLocation() + { + //For different platforms, we have to figure out how to get the connection right + //see: https://docs.confluent.io/platform/current/tutorials/examples/clients/docs/csharp.html - return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/usr/local/etc/openssl@1.1/cert.pem" : null; - } + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/usr/local/etc/openssl@1.1/cert.pem" : null; } } diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_set_of_messages_is_sent_preserve_order_on_a_confluent_cluster_async.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_set_of_messages_is_sent_preserve_order_on_a_confluent_cluster_async.cs new file mode 100644 index 0000000000..0f2beb3e66 --- /dev/null +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_a_set_of_messages_is_sent_preserve_order_on_a_confluent_cluster_async.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Confluent.Kafka; +using FluentAssertions; +using Paramore.Brighter.Kafka.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.Kafka; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Trait("Category", "Confluent")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaMessageConsumerConfluentPreservesOrderAsync : IDisposable +{ + private const string _groupId = "Kafka Message Producer Assume Topic Test"; + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly string _partitionKey = Guid.NewGuid().ToString(); + private readonly string _bootStrapServer; + private readonly string _userName; + private readonly string _password; + + public KafkaMessageConsumerConfluentPreservesOrderAsync(ITestOutputHelper output) + { + // -- Confluent supply these values, see their .NET examples for your account + // You need to set those values as environment variables, which we then read, in order + // to run these tests + + _bootStrapServer = Environment.GetEnvironmentVariable("CONFLUENT_BOOSTRAP_SERVER"); + _userName = Environment.GetEnvironmentVariable("CONFLUENT_SASL_USERNAME"); + _password = Environment.GetEnvironmentVariable("CONFLUENT_SASL_PASSWORD"); + + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {_bootStrapServer}, + SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol.SaslSsl, + SaslMechanisms = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism.Plain, + SaslUsername = _userName, + SaslPassword = _password, + SslCaLocation = SupplyCertificateLocation() + + }, + new[] {new KafkaPublication + { + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 3, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 10000, + RequestTimeoutMs = 10000, + MakeChannels = OnMissingChannel.Create //This will not make the topic + } + }).Create(); + } + + [Fact] + public async Task When_a_message_is_sent_keep_order() + { + IAmAMessageConsumerAsync consumer = null; + try + { + //Send a sequence of messages to Kafka + var msgId = await SendMessageAsync(); + var msgId2 = await SendMessageAsync(); + var msgId3 = await SendMessageAsync(); + var msgId4 = await SendMessageAsync(); + + consumer = CreateConsumer(); + + //Now read those messages in order + + var firstMessage = await ConsumeMessagesAsync(consumer); + var message = firstMessage.First(); + message.Id.Should().Be(msgId); + await consumer.AcknowledgeAsync(message); + + var secondMessage = await ConsumeMessagesAsync(consumer); + message = secondMessage.First(); + message.Id.Should().Be(msgId2); + await consumer.AcknowledgeAsync(message); + + var thirdMessages = await ConsumeMessagesAsync(consumer); + message = thirdMessages.First(); + message.Id.Should().Be(msgId3); + await consumer.AcknowledgeAsync(message); + + var fourthMessage = await ConsumeMessagesAsync(consumer); + message = fourthMessage.First(); + message.Id.Should().Be(msgId4); + await consumer.AcknowledgeAsync(message); + + } + finally + { + if (consumer != null) + { + await consumer.DisposeAsync(); + } + } + } + + private async Task SendMessageAsync() + { + var messageId = Guid.NewGuid().ToString(); + + var routingKey = new RoutingKey(_topic); + + await ((IAmAMessageProducerAsync)_producerRegistry.LookupBy(routingKey)).SendAsync( + new Message( + new MessageHeader(messageId, routingKey, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey + }, + new MessageBody($"test content [{_queueName}]") + ) + ); + + return messageId; + } + + private async Task> ConsumeMessagesAsync(IAmAMessageConsumerAsync consumer) + { + var messages = new Message[0]; + int maxTries = 0; + do + { + try + { + maxTries++; + await Task.Delay(500); //Let topic propagate in the broker + messages = await consumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000)); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + break; + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } + } while (maxTries <= 3); + + return messages; + } + + private IAmAMessageConsumerAsync CreateConsumer() + { + return new KafkaMessageConsumerFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {_bootStrapServer}, + SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol.SaslSsl, + SaslMechanisms = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism.Plain, + SaslUsername = _userName, + SaslPassword = _password, + SslCaLocation = SupplyCertificateLocation() + + }) + .CreateAsync(new KafkaSubscription( + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: _groupId, + offsetDefault: AutoOffsetReset.Earliest, + commitBatchSize:1, + numOfPartitions: 1, + replicationFactor: 3, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + )); + } + + public void Dispose() + { + _producerRegistry.Dispose(); + } + + private string SupplyCertificateLocation() + { + //For different platforms, we have to figure out how to get the connection right + //see: https://docs.confluent.io/platform/current/tutorials/examples/clients/docs/csharp.html + + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/usr/local/etc/openssl@1.1/cert.pem" : null; + } +} diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_assumes_topic_but_missing.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_assumes_topic_but_missing.cs index 57c5394052..f535d15598 100644 --- a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_assumes_topic_but_missing.cs +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_assumes_topic_but_missing.cs @@ -1,108 +1,77 @@ -#region Licence -/* The MIT License (MIT) -Copyright © 2014 Wayne Hunsley - -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.Linq; -using System.Runtime.InteropServices; +using System; using System.Threading.Tasks; -using Confluent.Kafka; using FluentAssertions; -using Paramore.Brighter.Kafka.Tests.TestDoubles; using Paramore.Brighter.MessagingGateway.Kafka; using Xunit; using Xunit.Abstractions; -using SaslMechanism = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism; -using SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol; -namespace Paramore.Brighter.Kafka.Tests.MessagingGateway +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaProducerAssumeTests : IDisposable { - [Trait("Category", "Kafka")] - [Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition - public class KafkaProducerAssumeTests : IDisposable - { - private readonly ITestOutputHelper _output; - private readonly string _queueName = Guid.NewGuid().ToString(); - private readonly string _topic = Guid.NewGuid().ToString(); - private readonly IAmAProducerRegistry _producerRegistry; - private readonly string _partitionKey = Guid.NewGuid().ToString(); + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly string _partitionKey = Guid.NewGuid().ToString(); - public KafkaProducerAssumeTests(ITestOutputHelper output) - { - _output = output; - _producerRegistry = new KafkaProducerRegistryFactory( - new KafkaMessagingGatewayConfiguration - { - Name = "Kafka Producer Send Test", - BootStrapServers = new[] {"localhost:9092"} - }, - new KafkaPublication[] {new KafkaPublication - { - Topic = new RoutingKey(_topic), - NumPartitions = 1, - ReplicationFactor = 1, - //These timeouts support running on a container using the same host as the tests, - //your production values ought to be lower - MessageTimeoutMs = 2000, - RequestTimeoutMs = 2000, - MakeChannels = OnMissingChannel.Create - }}).Create(); + public KafkaProducerAssumeTests(ITestOutputHelper output) + { + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {"localhost:9092"} + }, + new KafkaPublication[] {new KafkaPublication + { + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 1, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 2000, + RequestTimeoutMs = 2000, + MakeChannels = OnMissingChannel.Create + }}).Create(); - } + } - //[Fact(Skip = "Does not fail on docker container as has topic creation set to true")] - [Fact] - public void When_a_consumer_declares_topics() - { - var routingKey = new RoutingKey(_topic); - - var message = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) - { - PartitionKey = _partitionKey - }, - new MessageBody($"test content [{_queueName}]") - ); + //[Fact(Skip = "Does not fail on docker container as has topic creation set to true")] + [Fact] + public void When_a_consumer_declares_topics() + { + var routingKey = new RoutingKey(_topic); - bool messagePublished = false; - var producer = _producerRegistry.LookupBy(routingKey); - var producerConfirm = producer as ISupportPublishConfirmation; - producerConfirm.OnMessagePublished += delegate(bool success, string id) + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) { - if (success) messagePublished = true; - }; + PartitionKey = _partitionKey + }, + new MessageBody($"test content [{_queueName}]") + ); - ((IAmAMessageProducerSync)producer).Send(message); + bool messagePublished = false; + var producer = _producerRegistry.LookupBy(routingKey); + var producerConfirm = producer as ISupportPublishConfirmation; + producerConfirm.OnMessagePublished += delegate(bool success, string id) + { + if (success) messagePublished = true; + }; + + ((IAmAMessageProducerSync)producer).Send(message); - //Give this a chance to succeed - will fail - Task.Delay(5000); + //Give this a chance to succeed - will fail + Task.Delay(5000); - messagePublished.Should().BeFalse(); - } + messagePublished.Should().BeFalse(); + } - public void Dispose() - { - _producerRegistry?.Dispose(); - } + public void Dispose() + { + _producerRegistry?.Dispose(); } } diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_assumes_topic_but_missing_async.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_assumes_topic_but_missing_async.cs new file mode 100644 index 0000000000..b0574a85d0 --- /dev/null +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_assumes_topic_but_missing_async.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.Kafka; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaProducerAssumeTestsAsync : IDisposable +{ + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly string _partitionKey = Guid.NewGuid().ToString(); + + public KafkaProducerAssumeTestsAsync(ITestOutputHelper output) + { + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {"localhost:9092"} + }, + new KafkaPublication[] {new KafkaPublication + { + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 1, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 2000, + RequestTimeoutMs = 2000, + MakeChannels = OnMissingChannel.Create + }}).Create(); + } + + //[Fact(Skip = "Does not fail on docker container as has topic creation set to true")] + [Fact] + public async Task When_a_consumer_declares_topics() + { + var routingKey = new RoutingKey(_topic); + + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey + }, + new MessageBody($"test content [{_queueName}]") + ); + + bool messagePublished = false; + var producer = _producerRegistry.LookupBy(routingKey); + var producerConfirm = producer as ISupportPublishConfirmation; + producerConfirm.OnMessagePublished += delegate(bool success, string id) + { + if (success) messagePublished = true; + }; + + await ((IAmAMessageProducerAsync)producer).SendAsync(message); + + //Give this a chance to succeed - will fail + await Task.Delay(5000); + + messagePublished.Should().BeFalse(); + } + + public void Dispose() + { + _producerRegistry.Dispose(); + } +} diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_assumes_topic_but_missing_on_a_confluent_cluster.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_assumes_topic_but_missing_on_a_confluent_cluster.cs index cf18aa0262..4ddb1d5554 100644 --- a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_assumes_topic_but_missing_on_a_confluent_cluster.cs +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_assumes_topic_but_missing_on_a_confluent_cluster.cs @@ -35,50 +35,50 @@ THE SOFTWARE. */ using SaslMechanism = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism; using SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol; -namespace Paramore.Brighter.Kafka.Tests.MessagingGateway +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Trait("Category", "Confluent")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaConfluentProducerAssumeTests : IDisposable { - [Trait("Category", "Kafka")] - [Trait("Category", "Confluent")] - [Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition - public class KafkaConfluentProducerAssumeTests : IDisposable - { - private readonly string _queueName = Guid.NewGuid().ToString(); - private readonly string _topic = Guid.NewGuid().ToString(); - private readonly IAmAProducerRegistry _producerRegistry; - private readonly string _partitionKey = Guid.NewGuid().ToString(); + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly string _partitionKey = Guid.NewGuid().ToString(); - public KafkaConfluentProducerAssumeTests () + public KafkaConfluentProducerAssumeTests () + { + string SupplyCertificateLocation() { - string SupplyCertificateLocation() - { - //For different platforms, we have to figure out how to get the connection right - //see: https://docs.confluent.io/platform/current/tutorials/examples/clients/docs/csharp.html + //For different platforms, we have to figure out how to get the connection right + //see: https://docs.confluent.io/platform/current/tutorials/examples/clients/docs/csharp.html - return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/usr/local/etc/openssl@1.1/cert.pem" : null; - } + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/usr/local/etc/openssl@1.1/cert.pem" : null; + } - // -- Confluent supply these values, see their .NET examples for your account - // You need to set those values as environment variables, which we then read, in order - // to run these tests + // -- Confluent supply these values, see their .NET examples for your account + // You need to set those values as environment variables, which we then read, in order + // to run these tests - string bootStrapServer = Environment.GetEnvironmentVariable("CONFLUENT_BOOSTRAP_SERVER"); - string userName = Environment.GetEnvironmentVariable("CONFLUENT_SASL_USERNAME"); - string password = Environment.GetEnvironmentVariable("CONFLUENT_SASL_PASSWORD"); + string bootStrapServer = Environment.GetEnvironmentVariable("CONFLUENT_BOOSTRAP_SERVER"); + string userName = Environment.GetEnvironmentVariable("CONFLUENT_SASL_USERNAME"); + string password = Environment.GetEnvironmentVariable("CONFLUENT_SASL_PASSWORD"); - _producerRegistry = new KafkaProducerRegistryFactory( - new KafkaMessagingGatewayConfiguration - { - Name = "Kafka Producer Send Test", - BootStrapServers = new[] {bootStrapServer}, - SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol.SaslSsl, - SaslMechanisms = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism.Plain, - SaslUsername = userName, - SaslPassword = password, - SslCaLocation = SupplyCertificateLocation() + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {bootStrapServer}, + SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol.SaslSsl, + SaslMechanisms = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism.Plain, + SaslUsername = userName, + SaslPassword = password, + SslCaLocation = SupplyCertificateLocation() - }, - new KafkaPublication[] {new KafkaPublication - { + }, + new KafkaPublication[] {new KafkaPublication + { Topic = new RoutingKey(_topic), NumPartitions = 1, ReplicationFactor = 3, @@ -88,44 +88,43 @@ string SupplyCertificateLocation() RequestTimeoutMs = 10000, MakeChannels = OnMissingChannel.Create //This will not make the topic } - }).Create(); + }).Create(); - } + } - [Fact] - public void When_a_consumer_declares_topics_on_a_confluent_cluster() - { - var routingKey = new RoutingKey(_topic); + [Fact] + public void When_a_consumer_declares_topics_on_a_confluent_cluster() + { + var routingKey = new RoutingKey(_topic); - var message = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) - { - PartitionKey = _partitionKey - }, - new MessageBody($"test content [{_queueName}]") - ); + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey + }, + new MessageBody($"test content [{_queueName}]") + ); - bool messagePublished = false; + bool messagePublished = false; - var producer = _producerRegistry.LookupBy(routingKey); - var producerConfirm = producer as ISupportPublishConfirmation; - producerConfirm.OnMessagePublished += delegate(bool success, string id) - { - if (success) messagePublished = true; - }; + var producer = _producerRegistry.LookupBy(routingKey); + var producerConfirm = producer as ISupportPublishConfirmation; + producerConfirm.OnMessagePublished += delegate(bool success, string id) + { + if (success) messagePublished = true; + }; - ((IAmAMessageProducerSync)producer).Send(message); + ((IAmAMessageProducerSync)producer).Send(message); - //Give this a chance to succeed - will fail - Task.Delay(5000); + //Give this a chance to succeed - will fail + Task.Delay(5000); - messagePublished.Should().BeFalse(); - } + messagePublished.Should().BeFalse(); + } - public void Dispose() - { - _producerRegistry?.Dispose(); - } + public void Dispose() + { + _producerRegistry?.Dispose(); } } diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_assumes_topic_but_missing_on_a_confluent_cluster_async.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_assumes_topic_but_missing_on_a_confluent_cluster_async.cs new file mode 100644 index 0000000000..88bd23d830 --- /dev/null +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_assumes_topic_but_missing_on_a_confluent_cluster_async.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Confluent.Kafka; +using FluentAssertions; +using Paramore.Brighter.Kafka.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.Kafka; +using Xunit; +using Xunit.Abstractions; +using SaslMechanism = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism; +using SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol; + +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Trait("Category", "Confluent")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaConfluentProducerAssumeTestsAsync : IDisposable +{ + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly string _partitionKey = Guid.NewGuid().ToString(); + + public KafkaConfluentProducerAssumeTestsAsync() + { + string SupplyCertificateLocation() + { + //For different platforms, we have to figure out how to get the connection right + //see: https://docs.confluent.io/platform/current/tutorials/examples/clients/docs/csharp.html + + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/usr/local/etc/openssl@1.1/cert.pem" : null; + } + + // -- Confluent supply these values, see their .NET examples for your account + // You need to set those values as environment variables, which we then read, in order + // to run these tests + + string bootStrapServer = Environment.GetEnvironmentVariable("CONFLUENT_BOOSTRAP_SERVER"); + string userName = Environment.GetEnvironmentVariable("CONFLUENT_SASL_USERNAME"); + string password = Environment.GetEnvironmentVariable("CONFLUENT_SASL_PASSWORD"); + + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {bootStrapServer}, + SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol.SaslSsl, + SaslMechanisms = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism.Plain, + SaslUsername = userName, + SaslPassword = password, + SslCaLocation = SupplyCertificateLocation() + + }, + [ + new KafkaPublication + { + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 3, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 10000, + RequestTimeoutMs = 10000, + MakeChannels = OnMissingChannel.Create //This will not make the topic + } + ]).Create(); + + } + + [Fact] + public async Task When_a_consumer_declares_topics_on_a_confluent_cluster() + { + var routingKey = new RoutingKey(_topic); + + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey + }, + new MessageBody($"test content [{_queueName}]") + ); + + bool messagePublished = false; + + + var producer = _producerRegistry.LookupBy(routingKey); + var producerConfirm = producer as ISupportPublishConfirmation; + producerConfirm.OnMessagePublished += delegate(bool success, string id) + { + if (success) messagePublished = true; + }; + + await ((IAmAMessageProducerAsync)producer).SendAsync(message); + + //Give this a chance to succeed - will fail + await Task.Delay(5000); + + messagePublished.Should().BeFalse(); + } + + public void Dispose() + { + _producerRegistry?.Dispose(); + } +} diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_declares_topic.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_declares_topic.cs index f6963434d9..8368ba42aa 100644 --- a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_declares_topic.cs +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_declares_topic.cs @@ -1,28 +1,4 @@ -#region Licence -/* The MIT License (MIT) -Copyright © 2014 Wayne Hunsley - -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; using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.Kafka.Tests.TestDoubles; @@ -30,108 +6,108 @@ THE SOFTWARE. */ using Xunit; using Xunit.Abstractions; -namespace Paramore.Brighter.Kafka.Tests.MessagingGateway +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaConsumerDeclareTests : IDisposable { - [Trait("Category", "Kafka")] - [Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition - public class KafkaConsumerDeclareTests : IDisposable - { - private readonly ITestOutputHelper _output; - private readonly string _queueName = Guid.NewGuid().ToString(); - private readonly string _topic = Guid.NewGuid().ToString(); - private readonly IAmAProducerRegistry _producerRegistry; - private readonly IAmAMessageConsumer _consumer; - private readonly string _partitionKey = Guid.NewGuid().ToString(); + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly IAmAMessageConsumerSync _consumer; + private readonly string _partitionKey = Guid.NewGuid().ToString(); - public KafkaConsumerDeclareTests (ITestOutputHelper output) - { - const string groupId = "Kafka Message Producer Send Test"; - _output = output; - _producerRegistry = new KafkaProducerRegistryFactory( + public KafkaConsumerDeclareTests (ITestOutputHelper output) + { + const string groupId = "Kafka Message Producer Send Test"; + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {"localhost:9092"} + }, + new[] {new KafkaPublication + { + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 1, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 2000, + RequestTimeoutMs = 2000, + MakeChannels = OnMissingChannel.Create + }}).Create(); + + _consumer = new KafkaMessageConsumerFactory( new KafkaMessagingGatewayConfiguration { - Name = "Kafka Producer Send Test", - BootStrapServers = new[] {"localhost:9092"} - }, - new[] {new KafkaPublication - { - Topic = new RoutingKey(_topic), - NumPartitions = 1, - ReplicationFactor = 1, - //These timeouts support running on a container using the same host as the tests, - //your production values ought to be lower - MessageTimeoutMs = 2000, - RequestTimeoutMs = 2000, - MakeChannels = OnMissingChannel.Create - }}).Create(); - - _consumer = new KafkaMessageConsumerFactory( - new KafkaMessagingGatewayConfiguration - { - Name = "Kafka Consumer Test", - BootStrapServers = new[] { "localhost:9092" } - }) - .Create(new KafkaSubscription( - channelName: new ChannelName(_queueName), - routingKey: new RoutingKey(_topic), - groupId: groupId, - numOfPartitions: 1, - replicationFactor: 1, - makeChannels: OnMissingChannel.Create - ) - ); + Name = "Kafka Consumer Test", + BootStrapServers = new[] { "localhost:9092" } + }) + .Create(new KafkaSubscription( + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: groupId, + numOfPartitions: 1, + replicationFactor: 1, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create + ) + ); - } + } - [Fact] - public async Task When_a_consumer_declares_topics() - { - var routingKey = new RoutingKey(_topic); + [Fact] + public async Task When_a_consumer_declares_topics() + { + var routingKey = new RoutingKey(_topic); - var message = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) - { - PartitionKey = _partitionKey - }, - new MessageBody($"test content [{_queueName}]") - ); + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey + }, + new MessageBody($"test content [{_queueName}]") + ); - //This should fail, if consumer can't create the topic as set to Assume - ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send(message); + //This should fail, if consumer can't create the topic as set to Assume + ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send(message); - Message[] messages = new Message[0]; - int maxTries = 0; - do + Message[] messages = new Message[0]; + int maxTries = 0; + do + { + try { - try - { - maxTries++; - await Task.Delay(500); //Let topic propagate in the broker - messages = _consumer.Receive(TimeSpan.FromMilliseconds(10000)); - _consumer.Acknowledge(messages[0]); + maxTries++; + await Task.Delay(500); //Let topic propagate in the broker + messages = _consumer.Receive(TimeSpan.FromMilliseconds(10000)); + _consumer.Acknowledge(messages[0]); - if (messages[0].Header.MessageType != MessageType.MT_NONE) - break; + if (messages[0].Header.MessageType != MessageType.MT_NONE) + break; - } - catch (ChannelFailureException cfx) - { - //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing - _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); - } + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } - } while (maxTries <= 3); + } while (maxTries <= 3); - messages.Length.Should().Be(1); - messages[0].Header.MessageType.Should().Be(MessageType.MT_COMMAND); - messages[0].Header.PartitionKey.Should().Be(_partitionKey); - messages[0].Body.Value.Should().Be(message.Body.Value); - } + messages.Length.Should().Be(1); + messages[0].Header.MessageType.Should().Be(MessageType.MT_COMMAND); + messages[0].Header.PartitionKey.Should().Be(_partitionKey); + messages[0].Body.Value.Should().Be(message.Body.Value); + } - public void Dispose() - { - _producerRegistry?.Dispose(); - _consumer?.Dispose(); - } + public void Dispose() + { + _producerRegistry?.Dispose(); + _consumer?.Dispose(); } } diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_declares_topic_async.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_declares_topic_async.cs new file mode 100644 index 0000000000..41dad3e753 --- /dev/null +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_declares_topic_async.cs @@ -0,0 +1,119 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Kafka.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.Kafka; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaConsumerDeclareTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly IAmAMessageConsumerAsync _consumer; + private readonly string _partitionKey = Guid.NewGuid().ToString(); + + public KafkaConsumerDeclareTestsAsync(ITestOutputHelper output) + { + const string groupId = "Kafka Message Producer Send Test"; + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {"localhost:9092"} + }, + new[] {new KafkaPublication + { + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 1, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 2000, + RequestTimeoutMs = 2000, + MakeChannels = OnMissingChannel.Create + }}).CreateAsync().Result; + + _consumer = new KafkaMessageConsumerFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Consumer Test", + BootStrapServers = new[] { "localhost:9092" } + }) + .CreateAsync(new KafkaSubscription( + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: groupId, + numOfPartitions: 1, + replicationFactor: 1, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + )); + + } + + [Fact] + public async Task When_a_consumer_declares_topics() + { + var routingKey = new RoutingKey(_topic); + + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey + }, + new MessageBody($"test content [{_queueName}]") + ); + + //This should fail, if consumer can't create the topic as set to Assume + await ((IAmAMessageProducerAsync)_producerRegistry.LookupBy(routingKey)).SendAsync(message); + + Message[] messages = []; + int maxTries = 0; + do + { + try + { + maxTries++; + await Task.Delay(500); //Let topic propagate in the broker + messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(10000)); + await _consumer.AcknowledgeAsync(messages[0]); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + break; + + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } + + } while (maxTries <= 3); + + messages.Length.Should().Be(1); + messages[0].Header.MessageType.Should().Be(MessageType.MT_COMMAND); + messages[0].Header.PartitionKey.Should().Be(_partitionKey); + messages[0].Body.Value.Should().Be(message.Body.Value); + } + + public void Dispose() + { + _producerRegistry?.Dispose(); + ((IAmAMessageConsumerSync)_consumer).Dispose(); + } + + public async ValueTask DisposeAsync() + { + _producerRegistry.Dispose(); + await _consumer.DisposeAsync(); + + } +} diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_declares_topic_on_a_confluent_cluster.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_declares_topic_on_a_confluent_cluster.cs index aa03241620..7090b5ea9b 100644 --- a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_declares_topic_on_a_confluent_cluster.cs +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_declares_topic_on_a_confluent_cluster.cs @@ -1,28 +1,4 @@ -#region Licence -/* The MIT License (MIT) -Copyright © 2014 Wayne Hunsley - -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; using System.Runtime.InteropServices; using System.Threading.Tasks; using FluentAssertions; @@ -33,54 +9,54 @@ THE SOFTWARE. */ using SaslMechanism = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism; using SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol; -namespace Paramore.Brighter.Kafka.Tests.MessagingGateway +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Trait("Category", "Confluent")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaConfluentConsumerDeclareTests : IDisposable { - [Trait("Category", "Kafka")] - [Trait("Category", "Confluent")] - [Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition - public class KafkaConfluentConsumerDeclareTests : IDisposable - { - private readonly ITestOutputHelper _output; - private readonly string _queueName = Guid.NewGuid().ToString(); - private readonly string _topic = Guid.NewGuid().ToString(); - private readonly IAmAProducerRegistry _producerRegistry; - private readonly IAmAMessageConsumer _consumer; - private readonly string _partitionKey = Guid.NewGuid().ToString(); + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly IAmAMessageConsumerSync _consumer; + private readonly string _partitionKey = Guid.NewGuid().ToString(); - public KafkaConfluentConsumerDeclareTests(ITestOutputHelper output) + public KafkaConfluentConsumerDeclareTests(ITestOutputHelper output) + { + string SupplyCertificateLocation() { - string SupplyCertificateLocation() - { - //For different platforms, we have to figure out how to get the connection right - //see: https://docs.confluent.io/platform/current/tutorials/examples/clients/docs/csharp.html + //For different platforms, we have to figure out how to get the connection right + //see: https://docs.confluent.io/platform/current/tutorials/examples/clients/docs/csharp.html - return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/usr/local/etc/openssl@1.1/cert.pem" : null; - } + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/usr/local/etc/openssl@1.1/cert.pem" : null; + } - // -- Confluent supply these values, see their .NET examples for your account - // You need to set those values as environment variables, which we then read, in order - // to run these tests + // -- Confluent supply these values, see their .NET examples for your account + // You need to set those values as environment variables, which we then read, in order + // to run these tests - const string groupId = "Kafka Message Producer Send Test"; - string bootStrapServer = Environment.GetEnvironmentVariable("CONFLUENT_BOOSTRAP_SERVER"); - string userName = Environment.GetEnvironmentVariable("CONFLUENT_SASL_USERNAME"); - string password = Environment.GetEnvironmentVariable("CONFLUENT_SASL_PASSWORD"); + const string groupId = "Kafka Message Producer Send Test"; + string bootStrapServer = Environment.GetEnvironmentVariable("CONFLUENT_BOOSTRAP_SERVER"); + string userName = Environment.GetEnvironmentVariable("CONFLUENT_SASL_USERNAME"); + string password = Environment.GetEnvironmentVariable("CONFLUENT_SASL_PASSWORD"); - _output = output; - _producerRegistry = new KafkaProducerRegistryFactory( - new KafkaMessagingGatewayConfiguration - { - Name = "Kafka Producer Send Test", - BootStrapServers = new[] {bootStrapServer}, - SecurityProtocol = SecurityProtocol.SaslSsl, - SaslMechanisms = SaslMechanism.Plain, - SaslUsername = userName, - SaslPassword = password, - SslCaLocation = SupplyCertificateLocation() + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {bootStrapServer}, + SecurityProtocol = SecurityProtocol.SaslSsl, + SaslMechanisms = SaslMechanism.Plain, + SaslUsername = userName, + SaslPassword = password, + SslCaLocation = SupplyCertificateLocation() - }, - new[] {new KafkaPublication - { + }, + new[] {new KafkaPublication + { Topic = new RoutingKey(_topic), NumPartitions = 1, ReplicationFactor = 3, @@ -90,10 +66,10 @@ string SupplyCertificateLocation() RequestTimeoutMs = 10000, MakeChannels = OnMissingChannel.Create //This will not make the topic } - }).Create(); + }).Create(); - //This should force creation of the topic - will fail if no topic creation code - _consumer = new KafkaMessageConsumerFactory( + //This should force creation of the topic - will fail if no topic creation code + _consumer = new KafkaMessageConsumerFactory( new KafkaMessagingGatewayConfiguration { Name = "Kafka Producer Send Test", @@ -104,66 +80,65 @@ string SupplyCertificateLocation() SaslPassword = password, SslCaLocation = SupplyCertificateLocation() }) - .Create(new KafkaSubscription( - channelName: new ChannelName(_queueName), - routingKey: new RoutingKey(_topic), - groupId: groupId, - numOfPartitions: 1, - replicationFactor: 3, - makeChannels: OnMissingChannel.Create - ) - ); + .Create(new KafkaSubscription( + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: groupId, + numOfPartitions: 1, + replicationFactor: 3, + makeChannels: OnMissingChannel.Create + ) + ); - } + } - [Fact] - public async Task When_a_consumer_declares_topics_on_a_confluent_cluster() - { - var routingKey = new RoutingKey(_topic); + [Fact] + public async Task When_a_consumer_declares_topics_on_a_confluent_cluster() + { + var routingKey = new RoutingKey(_topic); - var message = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) - { - PartitionKey = _partitionKey - }, - new MessageBody($"test content [{_queueName}]")); + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey + }, + new MessageBody($"test content [{_queueName}]")); - //This should fail, if consumer can't create the topic as set to Assume - ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send(message); + //This should fail, if consumer can't create the topic as set to Assume + ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send(message); - Message[] messages = new Message[0]; - int maxTries = 0; - do + Message[] messages = new Message[0]; + int maxTries = 0; + do + { + try { - try - { - maxTries++; - await Task.Delay(500); //Let topic propagate in the broker - messages = _consumer.Receive(TimeSpan.FromMilliseconds(10000)); - _consumer.Acknowledge(messages[0]); + maxTries++; + await Task.Delay(500); //Let topic propagate in the broker + messages = _consumer.Receive(TimeSpan.FromMilliseconds(10000)); + _consumer.Acknowledge(messages[0]); - if (messages[0].Header.MessageType != MessageType.MT_NONE) - break; + if (messages[0].Header.MessageType != MessageType.MT_NONE) + break; - } - catch (ChannelFailureException cfx) - { - //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing - _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); - } + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } - } while (maxTries <= 3); + } while (maxTries <= 3); - messages.Length.Should().Be(1); - messages[0].Header.MessageType.Should().Be(MessageType.MT_COMMAND); - messages[0].Header.PartitionKey.Should().Be(_partitionKey); - messages[0].Body.Value.Should().Be(message.Body.Value); - } + messages.Length.Should().Be(1); + messages[0].Header.MessageType.Should().Be(MessageType.MT_COMMAND); + messages[0].Header.PartitionKey.Should().Be(_partitionKey); + messages[0].Body.Value.Should().Be(message.Body.Value); + } - public void Dispose() - { - _producerRegistry?.Dispose(); - _consumer?.Dispose(); - } + public void Dispose() + { + _producerRegistry?.Dispose(); + _consumer?.Dispose(); } } diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_declares_topic_on_a_confluent_cluster_async.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_declares_topic_on_a_confluent_cluster_async.cs new file mode 100644 index 0000000000..a457183b3b --- /dev/null +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_consumer_declares_topic_on_a_confluent_cluster_async.cs @@ -0,0 +1,150 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Kafka.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.Kafka; +using Xunit; +using Xunit.Abstractions; +using SaslMechanism = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism; +using SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol; + +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Trait("Category", "Confluent")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaConfluentConsumerDeclareTestsAsync : IAsyncDisposable +{ + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly IAmAMessageConsumerAsync _consumer; + private readonly string _partitionKey = Guid.NewGuid().ToString(); + + public KafkaConfluentConsumerDeclareTestsAsync(ITestOutputHelper output) + { + string SupplyCertificateLocation() + { + //For different platforms, we have to figure out how to get the connection right + //see: https://docs.confluent.io/platform/current/tutorials/examples/clients/docs/csharp.html + + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/usr/local/etc/openssl@1.1/cert.pem" : null; + } + + // -- Confluent supply these values, see their .NET examples for your account + // You need to set those values as environment variables, which we then read, in order + // to run these tests + + const string groupId = "Kafka Message Producer Send Test"; + string bootStrapServer = Environment.GetEnvironmentVariable("CONFLUENT_BOOSTRAP_SERVER"); + string userName = Environment.GetEnvironmentVariable("CONFLUENT_SASL_USERNAME"); + string password = Environment.GetEnvironmentVariable("CONFLUENT_SASL_PASSWORD"); + + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {bootStrapServer}, + SecurityProtocol = SecurityProtocol.SaslSsl, + SaslMechanisms = SaslMechanism.Plain, + SaslUsername = userName, + SaslPassword = password, + SslCaLocation = SupplyCertificateLocation() + + }, + new[] {new KafkaPublication + { + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 3, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 10000, + RequestTimeoutMs = 10000, + MakeChannels = OnMissingChannel.Create //This will not make the topic + } + }).CreateAsync().Result; + + //This should force creation of the topic - will fail if no topic creation code + _consumer = new KafkaMessageConsumerFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {bootStrapServer}, + SecurityProtocol = SecurityProtocol.SaslSsl, + SaslMechanisms = SaslMechanism.Plain, + SaslUsername = userName, + SaslPassword = password, + SslCaLocation = SupplyCertificateLocation() + }) + .CreateAsync(new KafkaSubscription( + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: groupId, + numOfPartitions: 1, + replicationFactor: 3, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + )); + + } + + [Fact] + public async Task When_a_consumer_declares_topics_on_a_confluent_cluster() + { + var routingKey = new RoutingKey(_topic); + + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey + }, + new MessageBody($"test content [{_queueName}]")); + + //This should fail, if consumer can't create the topic as set to Assume + await ((IAmAMessageProducerAsync)_producerRegistry.LookupAsyncBy(routingKey)).SendAsync(message); + + Message[] messages = []; + int maxTries = 0; + do + { + try + { + maxTries++; + await Task.Delay(500); //Let topic propagate in the broker + messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(10000)); + await _consumer.AcknowledgeAsync(messages[0]); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + break; + + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } + + } while (maxTries <= 3); + + messages.Length.Should().Be(1); + messages[0].Header.MessageType.Should().Be(MessageType.MT_COMMAND); + messages[0].Header.PartitionKey.Should().Be(_partitionKey); + messages[0].Body.Value.Should().Be(message.Body.Value); + } + + public void Dispose() + { + _producerRegistry?.Dispose(); + ((IAmAMessageConsumerSync)_consumer)?.Dispose(); + } + + public async ValueTask DisposeAsync() + { + _producerRegistry.Dispose(); + await _consumer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_offsets_awaiting_next_acknowledge_sweep_them.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_offsets_awaiting_next_acknowledge_sweep_them.cs index 990c12a9b8..4daf3e22e6 100644 --- a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_offsets_awaiting_next_acknowledge_sweep_them.cs +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_offsets_awaiting_next_acknowledge_sweep_them.cs @@ -7,137 +7,138 @@ using Xunit; using Xunit.Abstractions; -namespace Paramore.Brighter.Kafka.Tests.MessagingGateway +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaMessageConsumerSweepOffsets : IDisposable { - [Trait("Category", "Kafka")] - [Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition - public class KafkaMessageConsumerSweepOffsets : IDisposable - { - private readonly ITestOutputHelper _output; - private readonly string _queueName = Guid.NewGuid().ToString(); - private readonly string _topic = Guid.NewGuid().ToString(); - private readonly IAmAProducerRegistry _producerRegistry; - private readonly KafkaMessageConsumer _consumer; - private readonly string _partitionKey = Guid.NewGuid().ToString(); + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly KafkaMessageConsumer _consumer; + private readonly string _partitionKey = Guid.NewGuid().ToString(); - public KafkaMessageConsumerSweepOffsets(ITestOutputHelper output) - { - const string groupId = "Kafka Message Producer Sweep Test"; - _output = output; - _producerRegistry = new KafkaProducerRegistryFactory( + public KafkaMessageConsumerSweepOffsets(ITestOutputHelper output) + { + const string groupId = "Kafka Message Producer Sweep Test"; + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {"localhost:9092"} + }, + new[] {new KafkaPublication + { + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 1, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 2000, + RequestTimeoutMs = 2000, + MakeChannels = OnMissingChannel.Create + }}).Create(); + + _consumer = (KafkaMessageConsumer)new KafkaMessageConsumerFactory( new KafkaMessagingGatewayConfiguration { - Name = "Kafka Producer Send Test", - BootStrapServers = new[] {"localhost:9092"} - }, - new[] {new KafkaPublication - { - Topic = new RoutingKey(_topic), - NumPartitions = 1, - ReplicationFactor = 1, - //These timeouts support running on a container using the same host as the tests, - //your production values ought to be lower - MessageTimeoutMs = 2000, - RequestTimeoutMs = 2000, - MakeChannels = OnMissingChannel.Create - }}).Create(); - - _consumer = (KafkaMessageConsumer)new KafkaMessageConsumerFactory( - new KafkaMessagingGatewayConfiguration - { - Name = "Kafka Consumer Test", - BootStrapServers = new[] { "localhost:9092" } - }) - .Create(new KafkaSubscription( - channelName: new ChannelName(_queueName), - routingKey: new RoutingKey(_topic), - groupId: groupId, - commitBatchSize: 20, //This large commit batch size may never be sent - sweepUncommittedOffsetsInterval: TimeSpan.FromMilliseconds(10000), - numOfPartitions: 1, - replicationFactor: 1, - makeChannels: OnMissingChannel.Create - ) - ); - } + Name = "Kafka Consumer Test", + BootStrapServers = new[] { "localhost:9092" } + }) + .Create(new KafkaSubscription( + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: groupId, + commitBatchSize: 20, //This large commit batch size may never be sent + sweepUncommittedOffsetsInterval: TimeSpan.FromMilliseconds(10000), + numOfPartitions: 1, + replicationFactor: 1, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create + ) + ); + } - [Fact] - public async Task When_a_message_is_acknowldeged_but_no_batch_sent_sweep_offsets() + [Fact] + public async Task When_a_message_is_acknowldeged_but_no_batch_sent_sweep_offsets() + { + //send x messages to Kafka + var sentMessages = new string[10]; + for (int i = 0; i < 10; i++) { - //send x messages to Kafka - var sentMessages = new string[10]; - for (int i = 0; i < 10; i++) - { - var msgId = Guid.NewGuid().ToString(); - SendMessage(msgId); - sentMessages[i] = msgId; - } + var msgId = Guid.NewGuid().ToString(); + SendMessage(msgId); + sentMessages[i] = msgId; + } - var consumedMessages = new List(); - for (int j = 0; j < 9; j++) - { - consumedMessages.Add(await ReadMessageAsync()); - } + var consumedMessages = new List(); + for (int j = 0; j < 9; j++) + { + consumedMessages.Add(await ReadMessageAsync()); + } - consumedMessages.Count.Should().Be(9); - _consumer.StoredOffsets().Should().Be(9); + consumedMessages.Count.Should().Be(9); + _consumer.StoredOffsets().Should().Be(9); - //Let time elapse with no activity - await Task.Delay(10000); + //Let time elapse with no activity + await Task.Delay(10000); - //This should trigger a sweeper run (can be fragile when non scheduled in containers etc) - consumedMessages.Add(await ReadMessageAsync()); + //This should trigger a sweeper run (can be fragile when non scheduled in containers etc) + consumedMessages.Add(await ReadMessageAsync()); - //Let the sweeper run, can be slow in CI environments to run the thread - //Let the sweeper run, can be slow in CI environments to run the thread - await Task.Delay(10000); + //Let the sweeper run, can be slow in CI environments to run the thread + //Let the sweeper run, can be slow in CI environments to run the thread + await Task.Delay(10000); - //Sweeper will commit these - _consumer.StoredOffsets().Should().Be(0); + //Sweeper will commit these + _consumer.StoredOffsets().Should().Be(0); - async Task ReadMessageAsync() + async Task ReadMessageAsync() + { + Message[] messages = new []{new Message()}; + int maxTries = 0; + do { - Message[] messages = new []{new Message()}; - int maxTries = 0; - do + try { - try - { - maxTries++; - await Task.Delay(500); //Let topic propagate in the broker - messages = _consumer.Receive(TimeSpan.FromMilliseconds(1000)); + maxTries++; + await Task.Delay(500); //Let topic propagate in the broker + messages = _consumer.Receive(TimeSpan.FromMilliseconds(1000)); - if (messages[0].Header.MessageType != MessageType.MT_NONE) - { - _consumer.Acknowledge(messages[0]); - return messages[0]; - } - - } - catch (ChannelFailureException cfx) + if (messages[0].Header.MessageType != MessageType.MT_NONE) { - //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing - _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + _consumer.Acknowledge(messages[0]); + return messages[0]; } - } while (maxTries <= 3); - return messages[0]; - } + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } + } while (maxTries <= 3); + + return messages[0]; } + } - private void SendMessage(string messageId) - { - var routingKey = new RoutingKey(_topic); + private void SendMessage(string messageId) + { + var routingKey = new RoutingKey(_topic); - ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send(new Message( - new MessageHeader(messageId, routingKey, MessageType.MT_COMMAND) {PartitionKey = _partitionKey}, - new MessageBody($"test content [{_queueName}]"))); - } + ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send(new Message( + new MessageHeader(messageId, routingKey, MessageType.MT_COMMAND) {PartitionKey = _partitionKey}, + new MessageBody($"test content [{_queueName}]"))); + } - public void Dispose() - { - _producerRegistry?.Dispose(); - } + public void Dispose() + { + _producerRegistry?.Dispose(); + _consumer.Dispose(); } } diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_offsets_awaiting_next_acknowledge_sweep_them_async.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_offsets_awaiting_next_acknowledge_sweep_them_async.cs new file mode 100644 index 0000000000..9177079915 --- /dev/null +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_offsets_awaiting_next_acknowledge_sweep_them_async.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Kafka.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.Kafka; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaMessageConsumerSweepOffsetsAsync : IAsyncDisposable, IDisposable +{ + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly KafkaMessageConsumer _consumer; + private readonly string _partitionKey = Guid.NewGuid().ToString(); + + public KafkaMessageConsumerSweepOffsetsAsync(ITestOutputHelper output) + { + const string groupId = "Kafka Message Producer Sweep Test"; + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {"localhost:9092"} + }, + new[] {new KafkaPublication + { + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 1, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 2000, + RequestTimeoutMs = 2000, + MakeChannels = OnMissingChannel.Create + }}).CreateAsync().Result; + + _consumer = (KafkaMessageConsumer) new KafkaMessageConsumerFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Consumer Test", + BootStrapServers = new[] { "localhost:9092" } + }) + .CreateAsync(new KafkaSubscription( + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: groupId, + commitBatchSize: 20, //This large commit batch size may never be sent + sweepUncommittedOffsetsInterval: TimeSpan.FromMilliseconds(10000), + numOfPartitions: 1, + replicationFactor: 1, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + )); + } + + [Fact] + public async Task When_a_message_is_acknowledged_but_no_batch_sent_sweep_offsets() + { + //send x messages to Kafka + var sentMessages = new string[10]; + for (int i = 0; i < 10; i++) + { + var msgId = Guid.NewGuid().ToString(); + await SendMessageAsync(msgId); + sentMessages[i] = msgId; + } + + var consumedMessages = new List(); + for (int j = 0; j < 9; j++) + { + consumedMessages.Add(await ReadMessageAsync()); + } + + consumedMessages.Count.Should().Be(9); + _consumer.StoredOffsets().Should().Be(9); + + //Let time elapse with no activity + await Task.Delay(10000); + + //This should trigger a sweeper run (can be fragile when non scheduled in containers etc) + consumedMessages.Add(await ReadMessageAsync()); + + //Let the sweeper run, can be slow in CI environments to run the thread + await Task.Delay(10000); + + //Sweeper will commit these + _consumer.StoredOffsets().Should().Be(0); + + async Task ReadMessageAsync() + { + Message[] messages = new []{new Message()}; + int maxTries = 0; + do + { + try + { + maxTries++; + await Task.Delay(500); //Let topic propagate in the broker + messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000)); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + { + await _consumer.AcknowledgeAsync(messages[0]); + return messages[0]; + } + + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } + } while (maxTries <= 3); + + return messages[0]; + } + } + + private async Task SendMessageAsync(string messageId) + { + var routingKey = new RoutingKey(_topic); + + await ((IAmAMessageProducerAsync)_producerRegistry.LookupAsyncBy(routingKey)).SendAsync(new Message( + new MessageHeader(messageId, routingKey, MessageType.MT_COMMAND) {PartitionKey = _partitionKey}, + new MessageBody($"test content [{_queueName}]"))); + } + + public void Dispose() + { + _producerRegistry?.Dispose(); + _consumer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + _producerRegistry.Dispose(); + await _consumer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message.cs index 6023439196..726906e2e9 100644 --- a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message.cs +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message.cs @@ -1,29 +1,4 @@ -#region Licence - -/* The MIT License (MIT) -Copyright © 2014 Wayne Hunsley - -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; using System.Collections.Generic; using System.Text.Json; using System.Threading.Tasks; @@ -33,134 +8,134 @@ THE SOFTWARE. */ using Xunit; using Xunit.Abstractions; -namespace Paramore.Brighter.Kafka.Tests.MessagingGateway +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaMessageProducerSendTests : IDisposable { - [Trait("Category", "Kafka")] - [Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition - public class KafkaMessageProducerSendTests : IDisposable + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly IAmAMessageConsumerSync _consumer; + private readonly string _partitionKey = Guid.NewGuid().ToString(); + + public KafkaMessageProducerSendTests(ITestOutputHelper output) { - private readonly ITestOutputHelper _output; - private readonly string _queueName = Guid.NewGuid().ToString(); - private readonly string _topic = Guid.NewGuid().ToString(); - private readonly IAmAProducerRegistry _producerRegistry; - private readonly IAmAMessageConsumer _consumer; - private readonly string _partitionKey = Guid.NewGuid().ToString(); - - public KafkaMessageProducerSendTests(ITestOutputHelper output) - { - const string groupId = "Kafka Message Producer Send Test"; - _output = output; - _producerRegistry = new KafkaProducerRegistryFactory( - new KafkaMessagingGatewayConfiguration + const string groupId = "Kafka Message Producer Send Test"; + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", BootStrapServers = new[] { "localhost:9092" } + }, + new[] + { + new KafkaPublication { - Name = "Kafka Producer Send Test", BootStrapServers = new[] { "localhost:9092" } - }, - new[] + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 1, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 2000, + RequestTimeoutMs = 2000, + MakeChannels = OnMissingChannel.Create + } + }).Create(); + + _consumer = new KafkaMessageConsumerFactory( + new KafkaMessagingGatewayConfiguration { - new KafkaPublication - { - Topic = new RoutingKey(_topic), - NumPartitions = 1, - ReplicationFactor = 1, - //These timeouts support running on a container using the same host as the tests, - //your production values ought to be lower - MessageTimeoutMs = 2000, - RequestTimeoutMs = 2000, - MakeChannels = OnMissingChannel.Create - } - }).Create(); - - _consumer = new KafkaMessageConsumerFactory( - new KafkaMessagingGatewayConfiguration - { - Name = "Kafka Consumer Test", BootStrapServers = new[] { "localhost:9092" } - }) - .Create(new KafkaSubscription( - channelName: new ChannelName(_queueName), - routingKey: new RoutingKey(_topic), - groupId: groupId, - numOfPartitions: 1, - replicationFactor: 1, - makeChannels: OnMissingChannel.Create - ) - ); - } - - [Fact] - public void When_posting_a_message() - { - var command = new MyCommand { Value = "Test Content" }; + Name = "Kafka Consumer Test", BootStrapServers = new[] { "localhost:9092" } + }) + .Create(new KafkaSubscription( + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: groupId, + numOfPartitions: 1, + replicationFactor: 1, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create + ) + ); + } + + [Fact] + public void When_posting_a_message() + { + var command = new MyCommand { Value = "Test Content" }; - //vanilla i.e. no Kafka specific bytes at the beginning - var body = JsonSerializer.Serialize(command, JsonSerialisationOptions.Options); + //vanilla i.e. no Kafka specific bytes at the beginning + var body = JsonSerializer.Serialize(command, JsonSerialisationOptions.Options); - var routingKey = new RoutingKey(_topic); + var routingKey = new RoutingKey(_topic); - var message = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) - { - PartitionKey = _partitionKey, - ContentType = "application/json", - Bag = new Dictionary{{"Test Header", "Test Value"},}, - ReplyTo = "com.brightercommand.replyto", - CorrelationId = Guid.NewGuid().ToString(), - Delayed = TimeSpan.FromMilliseconds(10), - HandledCount = 2, - TimeStamp = DateTime.UtcNow - }, - new MessageBody(body)); - - ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send(message); - - var receivedMessage = GetMessage(); - - var receivedCommand = JsonSerializer.Deserialize(receivedMessage.Body.Value, JsonSerialisationOptions.Options); - - receivedMessage.Header.MessageType.Should().Be(MessageType.MT_COMMAND); - receivedMessage.Header.PartitionKey.Should().Be(_partitionKey); - receivedMessage.Body.Bytes.Should().Equal(message.Body.Bytes); - receivedMessage.Body.Value.Should().Be(message.Body.Value); - receivedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ssZ") - .Should().Be(message.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ssZ")); - receivedCommand.Id.Should().Be(command.Id); - receivedCommand.Value.Should().Be(command.Value); - } - - private Message GetMessage() + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey, + ContentType = "application/json", + Bag = new Dictionary{{"Test Header", "Test Value"},}, + ReplyTo = "com.brightercommand.replyto", + CorrelationId = Guid.NewGuid().ToString(), + Delayed = TimeSpan.FromMilliseconds(10), + HandledCount = 2, + TimeStamp = DateTime.UtcNow + }, + new MessageBody(body)); + + ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send(message); + + var receivedMessage = GetMessage(); + + var receivedCommand = JsonSerializer.Deserialize(receivedMessage.Body.Value, JsonSerialisationOptions.Options); + + receivedMessage.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + receivedMessage.Header.PartitionKey.Should().Be(_partitionKey); + receivedMessage.Body.Bytes.Should().Equal(message.Body.Bytes); + receivedMessage.Body.Value.Should().Be(message.Body.Value); + receivedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ssZ") + .Should().Be(message.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ssZ")); + receivedCommand.Id.Should().Be(command.Id); + receivedCommand.Value.Should().Be(command.Value); + } + + private Message GetMessage() + { + Message[] messages = []; + int maxTries = 0; + do { - Message[] messages = []; - int maxTries = 0; - do + try { - try - { - maxTries++; - Task.Delay(500).Wait(); //Let topic propagate in the broker - messages = _consumer.Receive(TimeSpan.FromMilliseconds(1000)); - - if (messages[0].Header.MessageType != MessageType.MT_NONE) - { - _consumer.Acknowledge(messages[0]); - break; - } - } - catch (ChannelFailureException cfx) + maxTries++; + Task.Delay(500).Wait(); //Let topic propagate in the broker + messages = _consumer.Receive(TimeSpan.FromMilliseconds(1000)); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) { - //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing - _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + _consumer.Acknowledge(messages[0]); + break; } - } while (maxTries <= 3); + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } + } while (maxTries <= 3); - if (messages[0].Header.MessageType == MessageType.MT_NONE) - throw new Exception($"Failed to read from topic:{_topic} after {maxTries} attempts"); + if (messages[0].Header.MessageType == MessageType.MT_NONE) + throw new Exception($"Failed to read from topic:{_topic} after {maxTries} attempts"); - return messages[0]; - } + return messages[0]; + } - public void Dispose() - { - _producerRegistry?.Dispose(); - _consumer?.Dispose(); - } + public void Dispose() + { + _producerRegistry?.Dispose(); + _consumer?.Dispose(); } } diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message_async.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message_async.cs new file mode 100644 index 0000000000..3489e75997 --- /dev/null +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message_async.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Kafka.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.Kafka; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaMessageProducerSendTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly IAmAMessageConsumerAsync _consumer; + private readonly string _partitionKey = Guid.NewGuid().ToString(); + + public KafkaMessageProducerSendTestsAsync(ITestOutputHelper output) + { + const string groupId = "Kafka Message Producer Send Test"; + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", BootStrapServers = new[] { "localhost:9092" } + }, + new[] + { + new KafkaPublication + { + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 1, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 2000, + RequestTimeoutMs = 2000, + MakeChannels = OnMissingChannel.Create + } + }).CreateAsync().Result; + + _consumer = new KafkaMessageConsumerFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Consumer Test", BootStrapServers = new[] { "localhost:9092" } + }) + .CreateAsync(new KafkaSubscription( + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: groupId, + numOfPartitions: 1, + replicationFactor: 1, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + ) + ); + } + + [Fact] + public async Task When_posting_a_message() + { + var command = new MyCommand { Value = "Test Content" }; + + //vanilla i.e. no Kafka specific bytes at the beginning + var body = JsonSerializer.Serialize(command, JsonSerialisationOptions.Options); + + var routingKey = new RoutingKey(_topic); + + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey, + ContentType = "application/json", + Bag = new Dictionary{{"Test Header", "Test Value"},}, + ReplyTo = "com.brightercommand.replyto", + CorrelationId = Guid.NewGuid().ToString(), + Delayed = TimeSpan.FromMilliseconds(10), + HandledCount = 2, + TimeStamp = DateTime.UtcNow + }, + new MessageBody(body)); + + await ((IAmAMessageProducerAsync)_producerRegistry.LookupAsyncBy(routingKey)).SendAsync(message); + + var receivedMessage = await GetMessageAsync(); + + var receivedCommand = JsonSerializer.Deserialize(receivedMessage.Body.Value, JsonSerialisationOptions.Options); + + receivedMessage.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + receivedMessage.Header.PartitionKey.Should().Be(_partitionKey); + receivedMessage.Body.Bytes.Should().Equal(message.Body.Bytes); + receivedMessage.Body.Value.Should().Be(message.Body.Value); + receivedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ssZ") + .Should().Be(message.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ssZ")); + receivedCommand.Id.Should().Be(command.Id); + receivedCommand.Value.Should().Be(command.Value); + } + + private async Task GetMessageAsync() + { + Message[] messages = []; + int maxTries = 0; + do + { + try + { + maxTries++; + await Task.Delay(500); //Let topic propagate in the broker + messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000)); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + { + await _consumer.AcknowledgeAsync(messages[0]); + break; + } + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } + } while (maxTries <= 3); + + if (messages[0].Header.MessageType == MessageType.MT_NONE) + throw new Exception($"Failed to read from topic:{_topic} after {maxTries} attempts"); + + return messages[0]; + } + + public void Dispose() + { + _producerRegistry?.Dispose(); + ((IAmAMessageConsumerSync)_consumer)?.Dispose(); + } + + public async ValueTask DisposeAsync() + { + _producerRegistry.Dispose(); + await _consumer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message_to_a_confluent_cluster.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message_to_a_confluent_cluster.cs index 327859fb4e..1856783fdf 100644 --- a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message_to_a_confluent_cluster.cs +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message_to_a_confluent_cluster.cs @@ -1,28 +1,4 @@ -#region Licence -/* The MIT License (MIT) -Copyright © 2014 Wayne Hunsley - -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; using System.Runtime.InteropServices; using System.Threading.Tasks; using FluentAssertions; @@ -33,54 +9,55 @@ THE SOFTWARE. */ using SaslMechanism = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism; using SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol; -namespace Paramore.Brighter.Kafka.Tests.MessagingGateway +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Trait("Category", "Confluent")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaConfluentProducerSendTests : IDisposable { - [Trait("Category", "Kafka")] - [Trait("Category", "Confluent")] - [Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition - public class KafkaConfluentProducerSendTests : IDisposable - { - private readonly ITestOutputHelper _output; - private readonly string _queueName = Guid.NewGuid().ToString(); - private readonly string _topic = Guid.NewGuid().ToString(); - private readonly IAmAProducerRegistry _producerRegistry; - private readonly IAmAMessageConsumer _consumer; - private readonly string _partitionKey = Guid.NewGuid().ToString(); + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly IAmAMessageConsumerSync _consumer; + private readonly string _partitionKey = Guid.NewGuid().ToString(); - public KafkaConfluentProducerSendTests(ITestOutputHelper output) + public KafkaConfluentProducerSendTests(ITestOutputHelper output) + { + string SupplyCertificateLocation() { - string SupplyCertificateLocation() - { - //For different platforms, we have to figure out how to get the connection right - //see: https://docs.confluent.io/platform/current/tutorials/examples/clients/docs/csharp.html + //For different platforms, we have to figure out how to get the connection right + //see: https://docs.confluent.io/platform/current/tutorials/examples/clients/docs/csharp.html - return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/usr/local/etc/openssl@1.1/cert.pem" : null; - } + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/usr/local/etc/openssl@1.1/cert.pem" : null; + } - // -- Confluent supply these values, see their .NET examples for your account - // You need to set those values as environment variables, which we then read, in order - // to run these tests + // -- Confluent supply these values, see their .NET examples for your account + // You need to set those values as environment variables, which we then read, in order + // to run these tests - const string groupId = "Kafka Message Producer Send Test"; - string bootStrapServer = Environment.GetEnvironmentVariable("CONFLUENT_BOOSTRAP_SERVER"); - string userName = Environment.GetEnvironmentVariable("CONFLUENT_SASL_USERNAME"); - string password = Environment.GetEnvironmentVariable("CONFLUENT_SASL_PASSWORD"); + const string groupId = "Kafka Message Producer Send Test"; + string bootStrapServer = Environment.GetEnvironmentVariable("CONFLUENT_BOOSTRAP_SERVER"); + string userName = Environment.GetEnvironmentVariable("CONFLUENT_SASL_USERNAME"); + string password = Environment.GetEnvironmentVariable("CONFLUENT_SASL_PASSWORD"); - _output = output; - _producerRegistry = new KafkaProducerRegistryFactory( - new KafkaMessagingGatewayConfiguration - { - Name = "Kafka Producer Send Test", - BootStrapServers = new[] {bootStrapServer}, - SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol.SaslSsl, - SaslMechanisms = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism.Plain, - SaslUsername = userName, - SaslPassword = password, - SslCaLocation = SupplyCertificateLocation() + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {bootStrapServer}, + SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol.SaslSsl, + SaslMechanisms = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism.Plain, + SaslUsername = userName, + SaslPassword = password, + SslCaLocation = SupplyCertificateLocation() - }, - new KafkaPublication[] {new KafkaPublication - { + }, + [ + new KafkaPublication + { Topic = new RoutingKey(_topic), NumPartitions = 1, ReplicationFactor = 3, @@ -90,9 +67,9 @@ string SupplyCertificateLocation() RequestTimeoutMs = 10000, MakeChannels = OnMissingChannel.Create //This will not make the topic } - }).Create(); + ]).Create(); - _consumer = new KafkaMessageConsumerFactory( + _consumer = new KafkaMessageConsumerFactory( new KafkaMessagingGatewayConfiguration { Name = "Kafka Producer Send Test", @@ -103,62 +80,62 @@ string SupplyCertificateLocation() SaslPassword = password, SslCaLocation = SupplyCertificateLocation() }) - .Create(new KafkaSubscription( - channelName: new ChannelName(_queueName), - routingKey: new RoutingKey(_topic), - groupId: groupId, - makeChannels: OnMissingChannel.Create - ) - ); + .Create(new KafkaSubscription( + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: groupId, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create + ) + ); - } + } - [Fact] - public async Task When_posting_a_message_to_a_confluent_cluster() - { - var routingKey = new RoutingKey(_topic); + [Fact] + public async Task When_posting_a_message_to_a_confluent_cluster() + { + var routingKey = new RoutingKey(_topic); - var message = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) - { - PartitionKey = _partitionKey - }, - new MessageBody($"test content [{_queueName}]")); - ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send(message); + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey + }, + new MessageBody($"test content [{_queueName}]")); + ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send(message); - Message[] messages = new Message[0]; - int maxTries = 0; - do + Message[] messages = new Message[0]; + int maxTries = 0; + do + { + try { - try - { - maxTries++; - await Task.Delay(500); //Let topic propagate in the broker - messages = _consumer.Receive(TimeSpan.FromMilliseconds(10000)); - _consumer.Acknowledge(messages[0]); + maxTries++; + await Task.Delay(500); //Let topic propagate in the broker + messages = _consumer.Receive(TimeSpan.FromMilliseconds(10000)); + _consumer.Acknowledge(messages[0]); - if (messages[0].Header.MessageType != MessageType.MT_NONE) - break; + if (messages[0].Header.MessageType != MessageType.MT_NONE) + break; - } - catch (ChannelFailureException cfx) - { - //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing - _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); - } + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } - } while (maxTries <= 3); + } while (maxTries <= 3); - messages.Length.Should().Be(1); - messages[0].Header.MessageType.Should().Be(MessageType.MT_COMMAND); - messages[0].Header.PartitionKey.Should().Be(_partitionKey); - messages[0].Body.Value.Should().Be(message.Body.Value); - } + messages.Length.Should().Be(1); + messages[0].Header.MessageType.Should().Be(MessageType.MT_COMMAND); + messages[0].Header.PartitionKey.Should().Be(_partitionKey); + messages[0].Body.Value.Should().Be(message.Body.Value); + } - public void Dispose() - { - _producerRegistry?.Dispose(); - _consumer?.Dispose(); - } + public void Dispose() + { + _producerRegistry?.Dispose(); + _consumer?.Dispose(); } } diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message_to_a_confluent_cluster_async.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message_to_a_confluent_cluster_async.cs new file mode 100644 index 0000000000..8d74905799 --- /dev/null +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message_to_a_confluent_cluster_async.cs @@ -0,0 +1,145 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Kafka.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.Kafka; +using Xunit; +using Xunit.Abstractions; +using SaslMechanism = Paramore.Brighter.MessagingGateway.Kafka.SaslMechanism; +using SecurityProtocol = Paramore.Brighter.MessagingGateway.Kafka.SecurityProtocol; + +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Trait("Category", "Confluent")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaConfluentProducerSendTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly IAmAMessageConsumerAsync _consumer; + private readonly string _partitionKey = Guid.NewGuid().ToString(); + + public KafkaConfluentProducerSendTestsAsync(ITestOutputHelper output) + { + string SupplyCertificateLocation() + { + //For different platforms, we have to figure out how to get the connection right + //see: https://docs.confluent.io/platform/current/tutorials/examples/clients/docs/csharp.html + + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/usr/local/etc/openssl@1.1/cert.pem" : null; + } + + // -- Confluent supply these values, see their .NET examples for your account + // You need to set those values as environment variables, which we then read, in order + // to run these tests + + const string groupId = "Kafka Message Producer Send Test"; + string bootStrapServer = Environment.GetEnvironmentVariable("CONFLUENT_BOOSTRAP_SERVER"); + string userName = Environment.GetEnvironmentVariable("CONFLUENT_SASL_USERNAME"); + string password = Environment.GetEnvironmentVariable("CONFLUENT_SASL_PASSWORD"); + + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {bootStrapServer}, + SecurityProtocol = SecurityProtocol.SaslSsl, + SaslMechanisms = SaslMechanism.Plain, + SaslUsername = userName, + SaslPassword = password, + SslCaLocation = SupplyCertificateLocation() + + }, + new[] {new KafkaPublication + { + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 3, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 10000, + RequestTimeoutMs = 10000, + MakeChannels = OnMissingChannel.Create //This will not make the topic + } + }).CreateAsync().Result; + + _consumer = new KafkaMessageConsumerFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {bootStrapServer}, + SecurityProtocol = SecurityProtocol.SaslSsl, + SaslMechanisms = SaslMechanism.Plain, + SaslUsername = userName, + SaslPassword = password, + SslCaLocation = SupplyCertificateLocation() + }) + .CreateAsync(new KafkaSubscription( + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: groupId, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + )); + + } + + [Fact] + public async Task When_posting_a_message_to_a_confluent_cluster() + { + var routingKey = new RoutingKey(_topic); + + var message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey + }, + new MessageBody($"test content [{_queueName}]")); + await ((IAmAMessageProducerAsync)_producerRegistry.LookupAsyncBy(routingKey)).SendAsync(message); + + Message[] messages = new Message[0]; + int maxTries = 0; + do + { + try + { + maxTries++; + await Task.Delay(500); //Let topic propagate in the broker + messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(10000)); + await _consumer.AcknowledgeAsync(messages[0]); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + break; + + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } + + } while (maxTries <= 3); + + messages.Length.Should().Be(1); + messages[0].Header.MessageType.Should().Be(MessageType.MT_COMMAND); + messages[0].Header.PartitionKey.Should().Be(_partitionKey); + messages[0].Body.Value.Should().Be(message.Body.Value); + } + + public void Dispose() + { + _producerRegistry?.Dispose(); + ((IAmAMessageConsumerSync)_consumer)?.Dispose(); + } + + public async ValueTask DisposeAsync() + { + _producerRegistry.Dispose(); + await _consumer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message_with_header_bytes.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message_with_header_bytes.cs index 46127ae381..edc4dd21cd 100644 --- a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message_with_header_bytes.cs +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message_with_header_bytes.cs @@ -1,28 +1,4 @@ -#region Licence -/* The MIT License (MIT) -Copyright © 2014 Wayne Hunsley - -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; using System.Linq; using System.Net; using System.Threading.Tasks; @@ -36,150 +12,150 @@ THE SOFTWARE. */ using Xunit; using Xunit.Abstractions; -namespace Paramore.Brighter.Kafka.Tests.MessagingGateway +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaMessageProducerHeaderBytesSendTests : IDisposable { - [Trait("Category", "Kafka")] - [Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition - public class KafkaMessageProducerHeaderBytesSendTests : IDisposable - { - private readonly ITestOutputHelper _output; - private readonly string _queueName = Guid.NewGuid().ToString(); - private readonly string _topic = Guid.NewGuid().ToString(); - private readonly IAmAProducerRegistry _producerRegistry; - private readonly IAmAMessageConsumer _consumer; - private readonly string _partitionKey = Guid.NewGuid().ToString(); - private readonly ISerializer _serializer; - private readonly IDeserializer _deserializer; - private readonly SerializationContext _serializationContext; + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly IAmAMessageConsumerSync _consumer; + private readonly string _partitionKey = Guid.NewGuid().ToString(); + private readonly ISerializer _serializer; + private readonly IDeserializer _deserializer; + private readonly SerializationContext _serializationContext; - public KafkaMessageProducerHeaderBytesSendTests (ITestOutputHelper output) - { - const string groupId = "Kafka Message Producer Header Bytes Send Test"; - _output = output; - _producerRegistry = new KafkaProducerRegistryFactory( + public KafkaMessageProducerHeaderBytesSendTests (ITestOutputHelper output) + { + const string groupId = "Kafka Message Producer Header Bytes Send Test"; + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {"localhost:9092"} + }, + new[] {new KafkaPublication + { + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 1, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 2000, + RequestTimeoutMs = 2000, + MakeChannels = OnMissingChannel.Create + }}).Create(); + + _consumer = new KafkaMessageConsumerFactory( new KafkaMessagingGatewayConfiguration { - Name = "Kafka Producer Send Test", - BootStrapServers = new[] {"localhost:9092"} - }, - new[] {new KafkaPublication - { - Topic = new RoutingKey(_topic), - NumPartitions = 1, - ReplicationFactor = 1, - //These timeouts support running on a container using the same host as the tests, - //your production values ought to be lower - MessageTimeoutMs = 2000, - RequestTimeoutMs = 2000, - MakeChannels = OnMissingChannel.Create - }}).Create(); - - _consumer = new KafkaMessageConsumerFactory( - new KafkaMessagingGatewayConfiguration - { - Name = "Kafka Consumer Test", - BootStrapServers = new[] { "localhost:9092" } - }) - .Create(new KafkaSubscription( - channelName: new ChannelName(_queueName), - routingKey: new RoutingKey(_topic), - groupId: groupId, - numOfPartitions: 1, - replicationFactor: 1, - makeChannels: OnMissingChannel.Create - ) - ); + Name = "Kafka Consumer Test", + BootStrapServers = new[] { "localhost:9092" } + }) + .Create(new KafkaSubscription( + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: groupId, + numOfPartitions: 1, + replicationFactor: 1, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create + ) + ); - var schemaRegistryConfig = new SchemaRegistryConfig { Url = "http://localhost:8081"}; - ISchemaRegistryClient schemaRegistryClient = new CachedSchemaRegistryClient(schemaRegistryConfig); + var schemaRegistryConfig = new SchemaRegistryConfig { Url = "http://localhost:8081"}; + ISchemaRegistryClient schemaRegistryClient = new CachedSchemaRegistryClient(schemaRegistryConfig); - _serializer = new JsonSerializer(schemaRegistryClient, ConfluentJsonSerializationConfig.SerdesJsonSerializerConfig(), ConfluentJsonSerializationConfig.NJsonSchemaGeneratorSettings()).AsSyncOverAsync(); - _deserializer = new JsonDeserializer().AsSyncOverAsync(); - _serializationContext = new SerializationContext(MessageComponentType.Value, _topic); - } + _serializer = new JsonSerializer(schemaRegistryClient, ConfluentJsonSerializationConfig.SerdesJsonSerializerConfig(), ConfluentJsonSerializationConfig.NJsonSchemaGeneratorSettings()).AsSyncOverAsync(); + _deserializer = new JsonDeserializer().AsSyncOverAsync(); + _serializationContext = new SerializationContext(MessageComponentType.Value, _topic); + } - [Fact] - public void When_posting_a_message_via_the_messaging_gateway() - { - //arrange + [Fact] + public void When_posting_a_message_via_the_messaging_gateway() + { + //arrange - var myCommand = new MyKafkaCommand{ Value = "Hello World"}; + var myCommand = new MyKafkaCommand{ Value = "Hello World"}; - //use the serdes json serializer to write the message to the topic - var body = _serializer.Serialize(myCommand, _serializationContext); + //use the serdes json serializer to write the message to the topic + var body = _serializer.Serialize(myCommand, _serializationContext); - //grab the schema id that was written to the message by the serializer - var schemaId = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(body.Skip(1).Take(4).ToArray())); + //grab the schema id that was written to the message by the serializer + var schemaId = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(body.Skip(1).Take(4).ToArray())); - var routingKey = new RoutingKey(_topic); + var routingKey = new RoutingKey(_topic); - var sent = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) - { - PartitionKey = _partitionKey - }, - new MessageBody(body)); + var sent = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey + }, + new MessageBody(body)); - //act + //act - ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send(sent); + ((IAmAMessageProducerSync)_producerRegistry.LookupBy(routingKey)).Send(sent); - var received = GetMessage(); + var received = GetMessage(); - received.Body.Bytes.Length.Should().BeGreaterThan(5); + received.Body.Bytes.Length.Should().BeGreaterThan(5); - var receivedSchemaId = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(received.Body.Bytes.Skip(1).Take(4).ToArray())); + var receivedSchemaId = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(received.Body.Bytes.Skip(1).Take(4).ToArray())); - var receivedCommand = _deserializer.Deserialize(received.Body.Bytes, received.Body.Bytes is null, _serializationContext); + var receivedCommand = _deserializer.Deserialize(received.Body.Bytes, received.Body.Bytes is null, _serializationContext); - //assert - received.Header.MessageType.Should().Be(MessageType.MT_COMMAND); - received.Header.PartitionKey.Should().Be(_partitionKey); - received.Body.Bytes.Should().Equal(received.Body.Bytes); - received.Body.Value.Should().Be(received.Body.Value); - receivedSchemaId.Should().Be(schemaId); - receivedCommand.Id.Should().Be(myCommand.Id); - receivedCommand.Value.Should().Be(myCommand.Value); - } + //assert + received.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + received.Header.PartitionKey.Should().Be(_partitionKey); + received.Body.Bytes.Should().Equal(received.Body.Bytes); + received.Body.Value.Should().Be(received.Body.Value); + receivedSchemaId.Should().Be(schemaId); + receivedCommand.Id.Should().Be(myCommand.Id); + receivedCommand.Value.Should().Be(myCommand.Value); + } - private Message GetMessage() + private Message GetMessage() + { + Message[] messages = Array.Empty(); + int maxTries = 0; + do { - Message[] messages = Array.Empty(); - int maxTries = 0; - do + try { - try - { - maxTries++; - Task.Delay(500).Wait(); //Let topic propagate in the broker - messages = _consumer.Receive(TimeSpan.FromMilliseconds(1000)); + maxTries++; + Task.Delay(500).Wait(); //Let topic propagate in the broker + messages = _consumer.Receive(TimeSpan.FromMilliseconds(1000)); - if (messages[0].Header.MessageType != MessageType.MT_NONE) - { - _consumer.Acknowledge(messages[0]); - break; - } - - } - catch (ChannelFailureException cfx) + if (messages[0].Header.MessageType != MessageType.MT_NONE) { - //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing - _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + _consumer.Acknowledge(messages[0]); + break; } + + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } - } while (maxTries <= 3); + } while (maxTries <= 3); - if (messages[0].Header.MessageType == MessageType.MT_NONE) - throw new Exception($"Failed to read from topic:{_topic} after {maxTries} attempts"); + if (messages[0].Header.MessageType == MessageType.MT_NONE) + throw new Exception($"Failed to read from topic:{_topic} after {maxTries} attempts"); - return messages[0]; - } + return messages[0]; + } - public void Dispose() - { - _producerRegistry?.Dispose(); - _consumer?.Dispose(); - } + public void Dispose() + { + _producerRegistry?.Dispose(); + _consumer?.Dispose(); } } diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message_with_header_bytes_async.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message_with_header_bytes_async.cs new file mode 100644 index 0000000000..9610f80b75 --- /dev/null +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message_with_header_bytes_async.cs @@ -0,0 +1,165 @@ +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Confluent.Kafka; +using Confluent.SchemaRegistry; +using Confluent.SchemaRegistry.Serdes; +using FluentAssertions; +using Paramore.Brighter.Kafka.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.Kafka; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +[Trait("Category", "Kafka")] +[Collection("Kafka")] //Kafka doesn't like multiple consumers of a partition +public class KafkaMessageProducerHeaderBytesSendTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly IAmAMessageConsumerAsync _consumer; + private readonly string _partitionKey = Guid.NewGuid().ToString(); + private readonly IAsyncSerializer _serializer; + private readonly IAsyncDeserializer _deserializer; + private readonly SerializationContext _serializationContext; + + public KafkaMessageProducerHeaderBytesSendTestsAsync(ITestOutputHelper output) + { + const string groupId = "Kafka Message Producer Header Bytes Send Test"; + _output = output; + _producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Producer Send Test", + BootStrapServers = new[] {"localhost:9092"} + }, + new[] {new KafkaPublication + { + Topic = new RoutingKey(_topic), + NumPartitions = 1, + ReplicationFactor = 1, + //These timeouts support running on a container using the same host as the tests, + //your production values ought to be lower + MessageTimeoutMs = 2000, + RequestTimeoutMs = 2000, + MakeChannels = OnMissingChannel.Create + }}).CreateAsync().Result; + + _consumer = new KafkaMessageConsumerFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Consumer Test", + BootStrapServers = new[] { "localhost:9092" } + }) + .CreateAsync(new KafkaSubscription( + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: groupId, + numOfPartitions: 1, + replicationFactor: 1, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create + )); + + var schemaRegistryConfig = new SchemaRegistryConfig { Url = "http://localhost:8081"}; + ISchemaRegistryClient schemaRegistryClient = new CachedSchemaRegistryClient(schemaRegistryConfig); + + _serializer = new JsonSerializer(schemaRegistryClient, ConfluentJsonSerializationConfig.SerdesJsonSerializerConfig(), + ConfluentJsonSerializationConfig.NJsonSchemaGeneratorSettings()); + _deserializer = new JsonDeserializer(); + _serializationContext = new SerializationContext(MessageComponentType.Value, _topic); + } + + [Fact] + public async Task When_posting_a_message_via_the_messaging_gateway() + { + //arrange + + var myCommand = new MyKafkaCommand{ Value = "Hello World"}; + + //use the serdes json serializer to write the message to the topic + var body = await _serializer.SerializeAsync(myCommand, _serializationContext); + + //grab the schema id that was written to the message by the serializer + var schemaId = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(body.Skip(1).Take(4).ToArray())); + + var routingKey = new RoutingKey(_topic); + + var sent = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey + }, + new MessageBody(body)); + + //act + + await ((IAmAMessageProducerAsync)_producerRegistry.LookupAsyncBy(routingKey)).SendAsync(sent); + + var received = await GetMessageAsync(); + + received.Body.Bytes.Length.Should().BeGreaterThan(5); + + var receivedSchemaId = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(received.Body.Bytes.Skip(1).Take(4).ToArray())); + + var receivedCommand =await _deserializer.DeserializeAsync(received.Body.Bytes, received.Body.Bytes is null, _serializationContext); + + //assert + received.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + received.Header.PartitionKey.Should().Be(_partitionKey); + received.Body.Bytes.Should().Equal(received.Body.Bytes); + received.Body.Value.Should().Be(received.Body.Value); + receivedSchemaId.Should().Be(schemaId); + receivedCommand.Id.Should().Be(myCommand.Id); + receivedCommand.Value.Should().Be(myCommand.Value); + } + + private async Task GetMessageAsync() + { + Message[] messages = Array.Empty(); + int maxTries = 0; + do + { + try + { + maxTries++; + await Task.Delay(500); //Let topic propagate in the broker + messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000)); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + { + await _consumer.AcknowledgeAsync(messages[0]); + break; + } + + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } + + } while (maxTries <= 3); + + if (messages[0].Header.MessageType == MessageType.MT_NONE) + throw new Exception($"Failed to read from topic:{_topic} after {maxTries} attempts"); + + return messages[0]; + } + + public void Dispose() + { + _producerRegistry?.Dispose(); + ((IAmAMessageConsumerSync)_consumer)?.Dispose(); + } + + public async ValueTask DisposeAsync() + { + _producerRegistry.Dispose(); + await _consumer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_recieving_a_message_without_partition_key_header.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_recieving_a_message_without_partition_key_header.cs index f7a58729c9..01295a555a 100644 --- a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_recieving_a_message_without_partition_key_header.cs +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_recieving_a_message_without_partition_key_header.cs @@ -17,7 +17,7 @@ public class KafkaMessageProducerMissingHeaderTests : IDisposable private readonly ITestOutputHelper _output; private readonly string _queueName = Guid.NewGuid().ToString(); private readonly string _topic = Guid.NewGuid().ToString(); - private readonly IAmAMessageConsumer _consumer; + private readonly IAmAMessageConsumerSync _consumer; private readonly IProducer _producer; public KafkaMessageProducerMissingHeaderTests(ITestOutputHelper output) @@ -66,6 +66,7 @@ public KafkaMessageProducerMissingHeaderTests(ITestOutputHelper output) groupId: groupId, numOfPartitions: 1, replicationFactor: 1, + messagePumpType: MessagePumpType.Reactor, makeChannels: OnMissingChannel.Create ) ); diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_recieving_a_message_without_partition_key_header_async.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_recieving_a_message_without_partition_key_header_async.cs new file mode 100644 index 0000000000..ba291542ed --- /dev/null +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_recieving_a_message_without_partition_key_header_async.cs @@ -0,0 +1,138 @@ +using System; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Confluent.Kafka; +using FluentAssertions; +using Paramore.Brighter.Kafka.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.Kafka; +using Xunit; +using Xunit.Abstractions; +using Acks = Confluent.Kafka.Acks; + +namespace Paramore.Brighter.Kafka.Tests.MessagingGateway; + +public class KafkaMessageProducerMissingHeaderTestsAsync : IAsyncDisposable +{ + private readonly ITestOutputHelper _output; + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topic = Guid.NewGuid().ToString(); + private readonly IAmAMessageConsumerAsync _consumer; + private readonly IProducer _producer; + + public KafkaMessageProducerMissingHeaderTestsAsync(ITestOutputHelper output) + { + const string groupId = "Kafka Message Producer Missing Header Test"; + _output = output; + + var clientConfig = new ClientConfig + { + Acks = (Acks)((int)Acks.All), + BootstrapServers = string.Join(",", new[] { "localhost:9092" }), + ClientId = "Kafka Producer Send with Missing Header Tests", + }; + + var producerConfig = new ProducerConfig(clientConfig) + { + BatchNumMessages = 10, + EnableIdempotence = true, + MaxInFlight = 1, + LingerMs = 5, + MessageTimeoutMs = 5000, + MessageSendMaxRetries = 3, + Partitioner = Confluent.Kafka.Partitioner.ConsistentRandom, + QueueBufferingMaxMessages = 10, + QueueBufferingMaxKbytes = 1048576, + RequestTimeoutMs = 500, + RetryBackoffMs = 100, + }; + + _producer = new ProducerBuilder(producerConfig) + .SetErrorHandler((_, error) => + { + output.WriteLine($"Kafka producer failed with Code: {error.Code}, Reason: { error.Reason}, Fatal: {error.IsFatal}", error.Code, error.Reason, error.IsFatal); + }) + .Build(); + + _consumer = new KafkaMessageConsumerFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "Kafka Consumer Test", BootStrapServers = new[] { "localhost:9092" } + }) + .CreateAsync(new KafkaSubscription( + channelName: new ChannelName(_queueName), + routingKey: new RoutingKey(_topic), + groupId: groupId, + numOfPartitions: 1, + replicationFactor: 1, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + )); + } + + [Fact] + public async Task When_recieving_a_message_without_partition_key_header() + { + var command = new MyCommand { Value = "Test Content" }; + + //vanilla i.e. no Kafka specific bytes at the beginning + var body = JsonSerializer.Serialize(command, JsonSerialisationOptions.Options); + var value = Encoding.UTF8.GetBytes(body); + var kafkaMessage = new Message + { + Key = command.Id, + Value = value + }; + + await _producer.ProduceAsync(_topic, kafkaMessage); + + var receivedMessage = await GetMessageAsync(); + + //Where we lack a partition key header, assume non-Brighter header and set to message key + receivedMessage.Header.PartitionKey.Should().Be(command.Id); + receivedMessage.Body.Bytes.Should().Equal(value); + } + + private async Task GetMessageAsync() + { + Message[] messages = Array.Empty(); + int maxTries = 0; + do + { + try + { + maxTries++; + await Task.Delay(500); //Let topic propagate in the broker + messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000)); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + { + await _consumer.AcknowledgeAsync(messages[0]); + break; + } + } + catch (ChannelFailureException cfx) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + _output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } + } while (maxTries <= 3); + + if (messages[0].Header.MessageType == MessageType.MT_NONE) + throw new Exception($"Failed to read from topic:{_topic} after {maxTries} attempts"); + + return messages[0]; + } + + public void Dispose() + { + _producer?.Dispose(); + ((IAmAMessageConsumerSync)_consumer)?.Dispose(); + } + + public async ValueTask DisposeAsync() + { + _producer?.Dispose(); + await _consumer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.MQTT.Tests/MessagingGateway/When_posting_multiples_message_via_the_messaging_gateway.cs b/tests/Paramore.Brighter.MQTT.Tests/MessagingGateway/When_posting_multiples_message_via_the_messaging_gateway.cs index 7cee580c49..c319c91b85 100644 --- a/tests/Paramore.Brighter.MQTT.Tests/MessagingGateway/When_posting_multiples_message_via_the_messaging_gateway.cs +++ b/tests/Paramore.Brighter.MQTT.Tests/MessagingGateway/When_posting_multiples_message_via_the_messaging_gateway.cs @@ -33,12 +33,12 @@ namespace Paramore.Brighter.MQTT.Tests.MessagingGateway { [Trait("Category", "MQTT")] [Collection("MQTT")] - public class MqttMessageProducerSendMessageTests : IDisposable + public class MqttMessageProducerSendMessageTests : IDisposable, IAsyncDisposable { private const string MqttHost = "localhost"; private const string ClientId = "BrighterIntegrationTests-Produce"; private readonly IAmAMessageProducerAsync _messageProducer; - private readonly IAmAMessageConsumer _messageConsumer; + private readonly IAmAMessageConsumerSync _messageConsumer; private readonly string _topicPrefix = "BrighterIntegrationTests/ProducerTests"; public MqttMessageProducerSendMessageTests() @@ -95,8 +95,14 @@ public void When_posting_multiples_message_via_the_messaging_gateway() public void Dispose() { - _messageProducer.Dispose(); + ((IAmAMessageProducerSync)_messageProducer).Dispose(); _messageConsumer.Dispose(); } + + public async ValueTask DisposeAsync() + { + await _messageProducer.DisposeAsync(); + await ((IAmAMessageConsumerAsync)_messageConsumer).DisposeAsync(); + } } } diff --git a/tests/Paramore.Brighter.MQTT.Tests/MessagingGateway/When_posting_multiples_message_via_the_messaging_gateway_async.cs b/tests/Paramore.Brighter.MQTT.Tests/MessagingGateway/When_posting_multiples_message_via_the_messaging_gateway_async.cs new file mode 100644 index 0000000000..685ce6261f --- /dev/null +++ b/tests/Paramore.Brighter.MQTT.Tests/MessagingGateway/When_posting_multiples_message_via_the_messaging_gateway_async.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.MQTT; +using Xunit; + +namespace Paramore.Brighter.MQTT.Tests.MessagingGateway +{ + [Trait("Category", "MQTT")] + [Collection("MQTT")] + public class MqttMessageProducerSendMessageTestsAsync : IAsyncDisposable, IDisposable + { + private const string MqttHost = "localhost"; + private const string ClientId = "BrighterIntegrationTests-Produce"; + private readonly IAmAMessageProducerAsync _messageProducer; + private readonly IAmAMessageConsumerAsync _messageConsumer; + private readonly string _topicPrefix = "BrighterIntegrationTests/ProducerTests"; + + public MqttMessageProducerSendMessageTestsAsync() + { + var mqttProducerConfig = new MQTTMessagingGatewayProducerConfiguration + { + Hostname = MqttHost, + TopicPrefix = _topicPrefix + }; + + MQTTMessagePublisher mqttMessagePublisher = new(mqttProducerConfig); + _messageProducer = new MQTTMessageProducer(mqttMessagePublisher); + + MQTTMessagingGatewayConsumerConfiguration mqttConsumerConfig = new() + { + Hostname = MqttHost, + TopicPrefix = _topicPrefix, + ClientID = ClientId + }; + + _messageConsumer = new MQTTMessageConsumer(mqttConsumerConfig); + } + + [Fact] + public async Task When_posting_multiples_message_via_the_messaging_gateway() + { + const int messageCount = 1000; + List sentMessages = new(); + + for (int i = 0; i < messageCount; i++) + { + Message message = new( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), MessageType.MT_COMMAND), + new MessageBody($"test message") + ); + + await _messageProducer.SendAsync(message); + sentMessages.Add(message); + } + + Message[] receivedMessages = await _messageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(100)); + + receivedMessages.Should().NotBeEmpty() + .And.HaveCount(messageCount) + .And.ContainInOrder(sentMessages) + .And.ContainItemsAssignableTo(); + } + + public void Dispose() + { + ((IAmAMessageProducerSync)_messageProducer).Dispose(); + ((IAmAMessageConsumerSync)_messageConsumer).Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _messageProducer.DisposeAsync(); + await _messageConsumer.DisposeAsync(); + } + } +} diff --git a/tests/Paramore.Brighter.MQTT.Tests/MessagingGateway/When_queue_is_Purged.cs b/tests/Paramore.Brighter.MQTT.Tests/MessagingGateway/When_queue_is_purged.cs similarity index 86% rename from tests/Paramore.Brighter.MQTT.Tests/MessagingGateway/When_queue_is_Purged.cs rename to tests/Paramore.Brighter.MQTT.Tests/MessagingGateway/When_queue_is_purged.cs index 4047c5f3b6..c38006dddf 100644 --- a/tests/Paramore.Brighter.MQTT.Tests/MessagingGateway/When_queue_is_Purged.cs +++ b/tests/Paramore.Brighter.MQTT.Tests/MessagingGateway/When_queue_is_purged.cs @@ -33,12 +33,12 @@ namespace Paramore.Brighter.MQTT.Tests.MessagingGateway { [Trait("Category", "MQTT")] [Collection("MQTT")] - public class When_queue_is_Purged : IDisposable + public class When_queue_is_Purged : IDisposable, IAsyncDisposable { private const string MqttHost = "localhost"; private const string ClientId = "BrighterIntegrationTests-Purge"; - private readonly IAmAMessageProducerAsync _messageProducer; - private readonly IAmAMessageConsumer _messageConsumer; + private readonly IAmAMessageProducerSync _messageProducer; + private readonly IAmAMessageConsumerSync _messageConsumer; private readonly string _topicPrefix = "BrighterIntegrationTests/PurgeTests"; private readonly Message _noopMessage = new(); @@ -72,13 +72,12 @@ public void When_purging_the_queue_on_the_messaging_gateway() { for (int i = 0; i < 5; i++) { - Message _message = new( + Message message = new( new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), MessageType.MT_COMMAND), new MessageBody($"test message") ); - Task task = _messageProducer.SendAsync(_message); - task.Wait(); + _messageProducer.Send(message); } Thread.Sleep(100); @@ -98,5 +97,11 @@ public void Dispose() _messageProducer.Dispose(); _messageConsumer.Dispose(); } + + public async ValueTask DisposeAsync() + { + await ((IAmAMessageProducerAsync)_messageProducer).DisposeAsync(); + await ((IAmAMessageConsumerAsync)_messageConsumer).DisposeAsync(); + } } } diff --git a/tests/Paramore.Brighter.MQTT.Tests/MessagingGateway/When_queue_is_purged_async.cs b/tests/Paramore.Brighter.MQTT.Tests/MessagingGateway/When_queue_is_purged_async.cs new file mode 100644 index 0000000000..f4268df7ba --- /dev/null +++ b/tests/Paramore.Brighter.MQTT.Tests/MessagingGateway/When_queue_is_purged_async.cs @@ -0,0 +1,78 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.MQTT; +using Xunit; + +namespace Paramore.Brighter.MQTT.Tests.MessagingGateway +{ + [Trait("Category", "MQTT")] + [Collection("MQTT")] + public class WhenQueueIsPurgedAsync : IAsyncDisposable, IDisposable + { + private const string MqttHost = "localhost"; + private const string ClientId = "BrighterIntegrationTests-Purge"; + private readonly IAmAMessageProducerAsync _messageProducer; + private readonly IAmAMessageConsumerAsync _messageConsumer; + private readonly string _topicPrefix = "BrighterIntegrationTests/PurgeTests"; + private readonly Message _noopMessage = new(); + + public WhenQueueIsPurgedAsync() + { + var mqttProducerConfig = new MQTTMessagingGatewayProducerConfiguration + { + Hostname = MqttHost, + TopicPrefix = _topicPrefix + }; + + MQTTMessagePublisher mqttMessagePublisher = new(mqttProducerConfig); + _messageProducer = new MQTTMessageProducer(mqttMessagePublisher); + + MQTTMessagingGatewayConsumerConfiguration mqttConsumerConfig = new() + { + Hostname = MqttHost, + TopicPrefix = _topicPrefix, + ClientID = ClientId + }; + + _messageConsumer = new MQTTMessageConsumer(mqttConsumerConfig); + } + + [Fact] + public async Task WhenPurgingTheQueueOnTheMessagingGatewayAsync() + { + for (int i = 0; i < 5; i++) + { + Message message = new( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), MessageType.MT_COMMAND), + new MessageBody($"test message") + ); + + await _messageProducer.SendAsync(message); + } + + await Task.Delay(100); + + await _messageConsumer.PurgeAsync(); + + Message[] receivedMessages = await _messageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(100)); + + receivedMessages.Should().NotBeEmpty() + .And.HaveCount(1) + .And.ContainInOrder(new[] { _noopMessage }) + .And.ContainItemsAssignableTo(); + } + + public void Dispose() + { + ((IAmAMessageProducerSync)_messageProducer).Dispose(); + ((IAmAMessageConsumerSync)_messageConsumer).Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _messageProducer.DisposeAsync(); + await _messageConsumer.DisposeAsync(); + } + } +} diff --git a/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_a_message_is_sent_keep_order.cs b/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_a_message_is_sent_keep_order.cs index 8752888eb0..b10049a84f 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_a_message_is_sent_keep_order.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_a_message_is_sent_keep_order.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.MessagingGateway.MsSql; using Paramore.Brighter.MSSQL.Tests.TestDoubles; @@ -9,12 +10,12 @@ namespace Paramore.Brighter.MSSQL.Tests.MessagingGateway { [Trait("Category", "MSSQL")] - public class OrderTest + public class OrderTest : IAsyncDisposable, IDisposable { private readonly string _queueName = Guid.NewGuid().ToString(); private readonly string _topicName = Guid.NewGuid().ToString(); - private readonly IAmAProducerRegistry _producerRegistry; - private readonly IAmAMessageConsumer _consumer; + private readonly IAmAProducerRegistry _producerRegistry; + private readonly IAmAMessageConsumerSync _consumer; public OrderTest() { @@ -22,12 +23,15 @@ public OrderTest() testHelper.SetupQueueDb(); var routingKey = new RoutingKey(_topicName); - - var sub = new Subscription(new SubscriptionName(_queueName), - new ChannelName(_topicName), routingKey); + + var sub = new Subscription( + new SubscriptionName(_queueName), + new ChannelName(_topicName), routingKey, + messagePumpType: MessagePumpType.Reactor); + _producerRegistry = new MsSqlProducerRegistryFactory( - testHelper.QueueConfiguration, - new Publication[] {new() {Topic = routingKey}} + testHelper.QueueConfiguration, + [new() { Topic = routingKey }] ).Create(); _consumer = new MsSqlMessageConsumerFactory(testHelper.QueueConfiguration).Create(sub); } @@ -35,42 +39,34 @@ public OrderTest() [Fact] public void When_a_message_is_sent_keep_order() { - IAmAMessageConsumer consumer = _consumer; - try - { - //Send a sequence of messages to Kafka - var msgId = SendMessage(); - var msgId2 = SendMessage(); - var msgId3 = SendMessage(); - var msgId4 = SendMessage(); - - //Now read those messages in order - - var firstMessage = ConsumeMessages(consumer); - var message = firstMessage.First(); - message.Empty.Should().BeFalse("A message should be returned"); - message.Id.Should().Be(msgId); - - var secondMessage = ConsumeMessages(consumer); - message = secondMessage.First(); - message.Empty.Should().BeFalse("A message should be returned"); - message.Id.Should().Be(msgId2); - - var thirdMessages = ConsumeMessages(consumer); - message = thirdMessages.First(); - message.Empty.Should().BeFalse("A message should be returned"); - message.Id.Should().Be(msgId3); - - var fourthMessage = ConsumeMessages(consumer); - message = fourthMessage.First(); - message.Empty.Should().BeFalse("A message should be returned"); - message.Id.Should().Be(msgId4); - - } - finally - { - consumer?.Dispose(); - } + IAmAMessageConsumerSync consumer = _consumer; + //Send a sequence of messages to Kafka + var msgId = SendMessage(); + var msgId2 = SendMessage(); + var msgId3 = SendMessage(); + var msgId4 = SendMessage(); + + //Now read those messages in order + + var firstMessage = ConsumeMessages(consumer); + var message = firstMessage.First(); + message.Empty.Should().BeFalse("A message should be returned"); + message.Id.Should().Be(msgId); + + var secondMessage = ConsumeMessages(consumer); + message = secondMessage.First(); + message.Empty.Should().BeFalse("A message should be returned"); + message.Id.Should().Be(msgId2); + + var thirdMessages = ConsumeMessages(consumer); + message = thirdMessages.First(); + message.Empty.Should().BeFalse("A message should be returned"); + message.Id.Should().Be(msgId3); + + var fourthMessage = ConsumeMessages(consumer); + message = fourthMessage.First(); + message.Empty.Should().BeFalse("A message should be returned"); + message.Id.Should().Be(msgId4); } private string SendMessage() @@ -84,9 +80,10 @@ private string SendMessage() return messageId; } - private IEnumerable ConsumeMessages(IAmAMessageConsumer consumer) + + private IEnumerable ConsumeMessages(IAmAMessageConsumerSync consumer) { - var messages = new Message[0]; + var messages = Array.Empty(); int maxTries = 0; do { @@ -108,5 +105,16 @@ private IEnumerable ConsumeMessages(IAmAMessageConsumer consumer) return messages; } + public async ValueTask DisposeAsync() + { + await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); + _producerRegistry.Dispose(); + } + + public void Dispose() + { + _consumer?.Dispose(); + _producerRegistry?.Dispose(); + } } } diff --git a/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_a_message_is_sent_keep_order_async.cs b/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_a_message_is_sent_keep_order_async.cs new file mode 100644 index 0000000000..5d2c87fccd --- /dev/null +++ b/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_a_message_is_sent_keep_order_async.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.MsSql; +using Paramore.Brighter.MSSQL.Tests.TestDoubles; +using Xunit; + +namespace Paramore.Brighter.MSSQL.Tests.MessagingGateway +{ + [Trait("Category", "MSSQL")] + public class OrderTestAsync : IAsyncDisposable, IDisposable + { + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly string _topicName = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly IAmAMessageConsumerAsync _consumer; + + public OrderTestAsync() + { + var testHelper = new MsSqlTestHelper(); + testHelper.SetupQueueDb(); + + var routingKey = new RoutingKey(_topicName); + + var sub = new Subscription( + new SubscriptionName(_queueName), + new ChannelName(_topicName), routingKey, + messagePumpType: MessagePumpType.Proactor); + + _producerRegistry = new MsSqlProducerRegistryFactory( + testHelper.QueueConfiguration, + [new() { Topic = routingKey }] + ).CreateAsync().Result; + _consumer = new MsSqlMessageConsumerFactory(testHelper.QueueConfiguration).CreateAsync(sub); + } + + [Fact] + public async Task When_a_message_is_sent_keep_order() + { + IAmAMessageConsumerAsync consumer = _consumer; + //Send a sequence of messages to Kafka + var msgId = await SendMessageAsync(); + var msgId2 = await SendMessageAsync(); + var msgId3 = await SendMessageAsync(); + var msgId4 = await SendMessageAsync(); + + //Now read those messages in order + + var firstMessage = await ConsumeMessagesAsync(consumer); + var message = firstMessage.First(); + message.Empty.Should().BeFalse("A message should be returned"); + message.Id.Should().Be(msgId); + + var secondMessage = await ConsumeMessagesAsync(consumer); + message = secondMessage.First(); + message.Empty.Should().BeFalse("A message should be returned"); + message.Id.Should().Be(msgId2); + + var thirdMessages = await ConsumeMessagesAsync(consumer); + message = thirdMessages.First(); + message.Empty.Should().BeFalse("A message should be returned"); + message.Id.Should().Be(msgId3); + + var fourthMessage = await ConsumeMessagesAsync(consumer); + message = fourthMessage.First(); + message.Empty.Should().BeFalse("A message should be returned"); + message.Id.Should().Be(msgId4); + } + + private async Task SendMessageAsync() + { + var messageId = Guid.NewGuid().ToString(); + + var routingKey = new RoutingKey(_topicName); + await ((IAmAMessageProducerAsync)_producerRegistry.LookupAsyncBy(routingKey)).SendAsync(new Message( + new MessageHeader(messageId, routingKey, MessageType.MT_COMMAND), + new MessageBody($"test content [{_queueName}]"))); + + return messageId; + } + + private async Task> ConsumeMessagesAsync(IAmAMessageConsumerAsync consumer) + { + var messages = Array.Empty(); + int maxTries = 0; + do + { + try + { + maxTries++; + messages = await consumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000)); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + break; + } + catch (ChannelFailureException) + { + //Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + //_output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } + } while (maxTries <= 3); + + return messages; + } + + public void Dispose() + { + _producerRegistry.Dispose(); + ((IAmAMessageConsumerSync)_consumer).Dispose(); + } + + public async ValueTask DisposeAsync() + { + _producerRegistry.Dispose(); + await _consumer.DisposeAsync(); + } + } +} diff --git a/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_queue_is_Purged.cs b/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_queue_is_purged.cs similarity index 75% rename from tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_queue_is_Purged.cs rename to tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_queue_is_purged.cs index 772b51ad2a..0b5b93beb6 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_queue_is_Purged.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_queue_is_purged.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.MessagingGateway.MsSql; using Paramore.Brighter.MSSQL.Tests.TestDoubles; @@ -10,11 +10,11 @@ namespace Paramore.Brighter.MSSQL.Tests.MessagingGateway { [Trait("Category", "MSSQL")] - public class PurgeTest + public class PurgeTest : IAsyncDisposable, IDisposable { private readonly string _queueName = Guid.NewGuid().ToString(); private readonly IAmAProducerRegistry _producerRegistry; - private readonly IAmAMessageConsumer _consumer; + private readonly IAmAMessageConsumerSync _consumer; private readonly RoutingKey _routingKey; public PurgeTest() @@ -24,11 +24,14 @@ public PurgeTest() _routingKey = new RoutingKey(Guid.NewGuid().ToString()); - var sub = new Subscription(new SubscriptionName(_queueName), - new ChannelName(_routingKey.Value), _routingKey); + var sub = new Subscription( + new SubscriptionName(_queueName), + new ChannelName(_routingKey.Value), _routingKey, + messagePumpType: MessagePumpType.Reactor); + _producerRegistry = new MsSqlProducerRegistryFactory( - testHelper.QueueConfiguration, - new Publication[] {new() {Topic = _routingKey}} + testHelper.QueueConfiguration, + [new() {Topic = _routingKey}] ).Create(); _consumer = new MsSqlMessageConsumerFactory(testHelper.QueueConfiguration).Create(sub); } @@ -36,9 +39,7 @@ public PurgeTest() [Fact] public void When_queue_is_Purged() { - IAmAMessageConsumer consumer = _consumer; - try - { + IAmAMessageConsumerSync consumer = _consumer; //Send a sequence of messages to Kafka var msgId = SendMessage(); @@ -56,11 +57,6 @@ public void When_queue_is_Purged() message = nextMessage.First(); Assert.Equal(new Message(), message); - } - finally - { - consumer?.Dispose(); - } } private string SendMessage() @@ -73,9 +69,9 @@ private string SendMessage() return messageId; } - private IEnumerable ConsumeMessages(IAmAMessageConsumer consumer) + private IEnumerable ConsumeMessages(IAmAMessageConsumerSync consumer) { - var messages = new Message[0]; + var messages = Array.Empty(); int maxTries = 0; do { @@ -97,21 +93,16 @@ private IEnumerable ConsumeMessages(IAmAMessageConsumer consumer) return messages; } - } - - public class ExampleCommand : ICommand - { - - public string Id { get; set; } + public async ValueTask DisposeAsync() + { + await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); + _producerRegistry.Dispose(); + } - public ExampleCommand() + public void Dispose() { - Id = Guid.NewGuid().ToString(); + _consumer.Dispose(); + _producerRegistry.Dispose(); } - - /// - /// Gets or sets the span that this operation live within - /// - public Activity Span { get; set; } } } diff --git a/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_queue_is_purged_async.cs b/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_queue_is_purged_async.cs new file mode 100644 index 0000000000..2170fecc98 --- /dev/null +++ b/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_queue_is_purged_async.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.MsSql; +using Paramore.Brighter.MSSQL.Tests.TestDoubles; +using Xunit; + +namespace Paramore.Brighter.MSSQL.Tests.MessagingGateway +{ + [Trait("Category", "MSSQL")] + public class PurgeTestAsync : IAsyncDisposable, IDisposable + { + private readonly string _queueName = Guid.NewGuid().ToString(); + private readonly IAmAProducerRegistry _producerRegistry; + private readonly IAmAMessageConsumerAsync _consumer; + private readonly RoutingKey _routingKey; + + public PurgeTestAsync() + { + var testHelper = new MsSqlTestHelper(); + testHelper.SetupQueueDb(); + + _routingKey = new RoutingKey(Guid.NewGuid().ToString()); + + var sub = new Subscription( + new SubscriptionName(_queueName), + new ChannelName(_routingKey.Value), _routingKey, + messagePumpType: MessagePumpType.Proactor); + + _producerRegistry = new MsSqlProducerRegistryFactory( + testHelper.QueueConfiguration, + new Publication[] { new() { Topic = _routingKey } } + ).CreateAsync().Result; + + _consumer = new MsSqlMessageConsumerFactory(testHelper.QueueConfiguration).CreateAsync(sub); + } + + [Fact] + public async Task When_queue_is_Purged() + { + IAmAMessageConsumerAsync consumer = _consumer; + // Send a sequence of messages to Kafka + var msgId = await SendMessageAsync(); + + // Now read those messages in order + var firstMessage = await ConsumeMessagesAsync(consumer); + var message = firstMessage.First(); + message.Id.Should().Be(msgId); + + await _consumer.PurgeAsync(); + + // Next Message should not exist (default will be returned) + var nextMessage = await ConsumeMessagesAsync(consumer); + message = nextMessage.First(); + + Assert.Equal(new Message(), message); + } + + private async Task SendMessageAsync() + { + var messageId = Guid.NewGuid().ToString(); + + await ((IAmAMessageProducerAsync)_producerRegistry.LookupAsyncBy(_routingKey)).SendAsync(new Message( + new MessageHeader(messageId, _routingKey, MessageType.MT_COMMAND), + new MessageBody($"test content [{_queueName}]"))); + + return messageId; + } + + private async Task> ConsumeMessagesAsync(IAmAMessageConsumerAsync consumer) + { + var messages = Array.Empty(); + int maxTries = 0; + do + { + try + { + maxTries++; + messages = await consumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000)); + + if (messages[0].Header.MessageType != MessageType.MT_NONE) + break; + } + catch (ChannelFailureException) + { + // Lots of reasons to be here as Kafka propagates a topic, or the test cluster is still initializing + //_output.WriteLine($" Failed to read from topic:{_topic} because {cfx.Message} attempt: {maxTries}"); + } + } while (maxTries <= 3); + + return messages; + } + + public void Dispose() + { + ((IAmAMessageConsumerSync)_consumer).Dispose(); + _producerRegistry.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _consumer.DisposeAsync(); + _producerRegistry.Dispose(); + } + } +} diff --git a/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_requeueing_a_message.cs b/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_requeueing_a_message.cs index b686471f63..3ee3e8c2a5 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_requeueing_a_message.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_requeueing_a_message.cs @@ -34,11 +34,14 @@ public MsSqlMessageConsumerRequeueTests() var testHelper = new MsSqlTestHelper(); testHelper.SetupQueueDb(); - _subscription = new MsSqlSubscription(new SubscriptionName(channelName), - new ChannelName(_topic), new RoutingKey(_topic)); + _subscription = new MsSqlSubscription( + new SubscriptionName(channelName), + new ChannelName(_topic), new RoutingKey(_topic), + messagePumpType: MessagePumpType.Reactor); + _producerRegistry = new MsSqlProducerRegistryFactory( - testHelper.QueueConfiguration, - new Publication[] {new Publication {Topic = new RoutingKey(_topic)}} + testHelper.QueueConfiguration, + [new Publication {Topic = new RoutingKey(_topic)}] ).Create(); _channelFactory = new ChannelFactory(new MsSqlMessageConsumerFactory(testHelper.QueueConfiguration)); } @@ -47,7 +50,7 @@ public MsSqlMessageConsumerRequeueTests() public void When_requeueing_a_message() { ((IAmAMessageProducerSync)_producerRegistry.LookupBy(_topic)).Send(_message); - var channel = _channelFactory.CreateChannel(_subscription); + var channel = _channelFactory.CreateSyncChannel(_subscription); var message = channel.Receive(TimeSpan.FromMilliseconds(2000)); channel.Requeue(message, TimeSpan.FromMilliseconds(100)); diff --git a/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_requeueing_a_message_aync.cs b/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_requeueing_a_message_aync.cs new file mode 100644 index 0000000000..2f24048495 --- /dev/null +++ b/tests/Paramore.Brighter.MSSQL.Tests/MessagingGateway/When_requeueing_a_message_aync.cs @@ -0,0 +1,69 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.MsSql; +using Paramore.Brighter.MSSQL.Tests.TestDoubles; +using Xunit; + +namespace Paramore.Brighter.MSSQL.Tests.MessagingGateway +{ + [Trait("Category", "MSSQL")] + public class MsSqlMessageConsumerRequeueTestsAsync : IDisposable + { + private readonly Message _message; + private readonly IAmAProducerRegistry _producerRegistry; + private readonly IAmAChannelFactory _channelFactory; + private readonly MsSqlSubscription _subscription; + private readonly RoutingKey _topic; + + public MsSqlMessageConsumerRequeueTestsAsync() + { + var myCommand = new MyCommand { Value = "Test" }; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Consumer-Requeue-Tests-{Guid.NewGuid()}"; + _topic = new RoutingKey($"Consumer-Requeue-Tests-{Guid.NewGuid()}"); + + _message = new Message( + new MessageHeader(myCommand.Id, _topic, MessageType.MT_COMMAND, correlationId:correlationId, + replyTo:new RoutingKey(replyTo), contentType:contentType), + new MessageBody(JsonSerializer.Serialize(myCommand, JsonSerialisationOptions.Options)) + ); + + var testHelper = new MsSqlTestHelper(); + testHelper.SetupQueueDb(); + + _subscription = new MsSqlSubscription(new SubscriptionName(channelName), + new ChannelName(_topic), new RoutingKey(_topic)); + _producerRegistry = new MsSqlProducerRegistryFactory( + testHelper.QueueConfiguration, + new Publication[] {new Publication {Topic = new RoutingKey(_topic)}} + ).CreateAsync().Result; + _channelFactory = new ChannelFactory(new MsSqlMessageConsumerFactory(testHelper.QueueConfiguration)); + } + + [Fact] + public async Task When_requeueing_a_message_async() + { + await ((IAmAMessageProducerAsync)_producerRegistry.LookupAsyncBy(_topic)).SendAsync(_message); + var channel = await _channelFactory.CreateAsyncChannelAsync(_subscription); + var message = await channel.ReceiveAsync(TimeSpan.FromMilliseconds(2000)); + await channel.RequeueAsync(message, TimeSpan.FromMilliseconds(100)); + + var requeuedMessage = await channel.ReceiveAsync(TimeSpan.FromMilliseconds(1000)); + + //clear the queue + await channel.AcknowledgeAsync(requeuedMessage); + + requeuedMessage.Body.Value.Should().Be(message.Body.Value); + } + + public void Dispose() + { + _producerRegistry.Dispose(); + } + + } +} diff --git a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_removing_messages_from_the_message_store.cs b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_removing_messages_from_the_message_store.cs index 95cb70ee6e..670cb3d5e1 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_removing_messages_from_the_message_store.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_removing_messages_from_the_message_store.cs @@ -124,7 +124,6 @@ private void Release() public void Dispose() { - GC.SuppressFinalize(this); Release(); } diff --git a/tests/Paramore.Brighter.RMQ.Tests/Catch.cs b/tests/Paramore.Brighter.RMQ.Tests/Catch.cs deleted file mode 100644 index 14c41680d3..0000000000 --- a/tests/Paramore.Brighter.RMQ.Tests/Catch.cs +++ /dev/null @@ -1,66 +0,0 @@ -#region License NUnit.Specifications -/* From https://raw.githubusercontent.com/derekgreer/NUnit.Specifications/master/license.txt -Copyright(c) 2015 Derek B.Greer - - -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.Diagnostics; -using System.Threading.Tasks; - -namespace Paramore.Brighter.RMQ.Tests -{ - [DebuggerStepThrough] - public static class Catch - { - public static Exception Exception(Action action) - { - Exception exception = null; - - try - { - action(); - } - catch (Exception e) - { - exception = e; - } - - return exception; - } - public static async Task ExceptionAsync(Func action) - { - Exception exception = null; - - try - { - await action(); - } - catch (Exception e) - { - exception = e; - } - - return exception; - } - } -} 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 006fb49f69..44d25c32e2 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 @@ -1,29 +1,6 @@ -#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; using System.Linq; +using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.DependencyInjection; @@ -35,104 +12,113 @@ THE SOFTWARE. */ using Polly.Registry; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessageDispatch +namespace Paramore.Brighter.RMQ.Tests.MessageDispatch; + +[Collection("CommandProcessor")] +public class DispatchBuilderTests : IDisposable { - [Collection("CommandProcessor")] - public class DispatchBuilderTests : IDisposable + private readonly IAmADispatchBuilder _builder; + private Dispatcher? _dispatcher; + + public DispatchBuilderTests() { - private readonly IAmADispatchBuilder _builder; - private Dispatcher _dispatcher; + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory((_) => new MyEventMessageMapper()), + null); + messageMapperRegistry.Register(); + + var retryPolicy = Policy + .Handle() + .WaitAndRetry(new[] + { + TimeSpan.FromMilliseconds(50), + TimeSpan.FromMilliseconds(100), + TimeSpan.FromMilliseconds(150) + }); + + var circuitBreakerPolicy = Policy + .Handle() + .CircuitBreaker(1, TimeSpan.FromMilliseconds(500)); - public DispatchBuilderTests() + var rmqConnection = new RmqMessagingGatewayConnection { - var messageMapperRegistry = new MessageMapperRegistry( - new SimpleMessageMapperFactory((_) => new MyEventMessageMapper()), - null); - messageMapperRegistry.Register(); - - var retryPolicy = Policy - .Handle() - .WaitAndRetry(new[] - { - TimeSpan.FromMilliseconds(50), - TimeSpan.FromMilliseconds(100), - TimeSpan.FromMilliseconds(150) - }); - - var circuitBreakerPolicy = Policy - .Handle() - .CircuitBreaker(1, TimeSpan.FromMilliseconds(500)); - - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.brighter.exchange") - }; + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; - var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(rmqConnection); - var container = new ServiceCollection(); + var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(rmqConnection); + var container = new ServiceCollection(); - var tracer = new BrighterTracer(TimeProvider.System); - var instrumentationOptions = InstrumentationOptions.All; + var tracer = new BrighterTracer(TimeProvider.System); + var instrumentationOptions = InstrumentationOptions.All; - var commandProcessor = CommandProcessorBuilder.StartNew() - .Handlers(new HandlerConfiguration(new SubscriberRegistry(), new ServiceProviderHandlerFactory(container.BuildServiceProvider()))) - .Policies(new PolicyRegistry - { - { CommandProcessor.RETRYPOLICY, retryPolicy }, - { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } - }) - .NoExternalBus() - .ConfigureInstrumentation(tracer, instrumentationOptions) - .RequestContextFactory(new InMemoryRequestContextFactory()) - .Build(); - - _builder = DispatchBuilder.StartNew() - .CommandProcessorFactory(() => + var commandProcessor = CommandProcessorBuilder.StartNew() + .Handlers(new HandlerConfiguration(new SubscriberRegistry(), new ServiceProviderHandlerFactory(container.BuildServiceProvider()))) + .Policies(new PolicyRegistry + { + { CommandProcessor.RETRYPOLICY, retryPolicy }, + { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } + }) + .NoExternalBus() + .ConfigureInstrumentation(tracer, instrumentationOptions) + .RequestContextFactory(new InMemoryRequestContextFactory()) + .Build(); + + _builder = DispatchBuilder.StartNew() + .CommandProcessorFactory(() => new CommandProcessorProvider(commandProcessor), - new InMemoryRequestContextFactory() - ) - .MessageMappers(messageMapperRegistry, null, null, null) - .ChannelFactory(new ChannelFactory(rmqMessageConsumerFactory)) - .Subscriptions(new [] - { - new RmqSubscription( - new SubscriptionName("foo"), - new ChannelName("mary"), - new RoutingKey("bob"), - timeOut: TimeSpan.FromMilliseconds(200)), - new RmqSubscription( - new SubscriptionName("bar"), - new ChannelName("alice"), - new RoutingKey("simon"), - timeOut: TimeSpan.FromMilliseconds(200)) - }) - .ConfigureInstrumentation(tracer, instrumentationOptions); - } - - [Fact] - public void When_Building_A_Dispatcher() - { - _dispatcher = _builder.Build(); - - //_should_build_a_dispatcher - AssertionExtensions.Should(_dispatcher).NotBeNull(); - //_should_have_a_foo_connection - GetConnection("foo").Should().NotBeNull(); - //_should_have_a_bar_connection - GetConnection("bar").Should().NotBeNull(); - //_should_be_in_the_awaiting_state - AssertionExtensions.Should(_dispatcher.State).Be(DispatcherState.DS_AWAITING); - } - - public void Dispose() - { - CommandProcessor.ClearServiceBus(); - } + new InMemoryRequestContextFactory() + ) + .MessageMappers(messageMapperRegistry, null, null, null) + .ChannelFactory(new ChannelFactory(rmqMessageConsumerFactory)) + .Subscriptions(new [] + { + new RmqSubscription( + new SubscriptionName("foo"), + new ChannelName("mary"), + new RoutingKey("bob"), + messagePumpType: MessagePumpType.Reactor, + timeOut: TimeSpan.FromMilliseconds(200)), + new RmqSubscription( + new SubscriptionName("bar"), + new ChannelName("alice"), + new RoutingKey("simon"), + messagePumpType: MessagePumpType.Reactor, + timeOut: TimeSpan.FromMilliseconds(200)) + }) + .ConfigureInstrumentation(tracer, instrumentationOptions); + } - private Subscription GetConnection(string name) - { - return Enumerable.SingleOrDefault(_dispatcher.Subscriptions, conn => conn.Name == name); - } + [Fact] + public async Task When_Building_A_Dispatcher() + { + _dispatcher = _builder.Build(); + + _dispatcher.Should().NotBeNull(); + GetConnection("foo").Should().NotBeNull(); + GetConnection("bar").Should().NotBeNull(); + _dispatcher.State.Should().Be(DispatcherState.DS_AWAITING); + + await Task.Delay(1000); + + _dispatcher.Receive(); + + await Task.Delay(1000); + + _dispatcher.State.Should().Be(DispatcherState.DS_RUNNING); + + await _dispatcher.End(); + + _dispatcher.State.Should().Be(DispatcherState.DS_STOPPED); + } + + public void Dispose() + { + CommandProcessor.ClearServiceBus(); + } + + private Subscription GetConnection(string name) + { + return Enumerable.SingleOrDefault(_dispatcher.Subscriptions, conn => conn.Name == name); } } 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 new file mode 100644 index 0000000000..c4d9be10db --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher_async.cs @@ -0,0 +1,122 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.MessagingGateway.RMQ; +using Paramore.Brighter.Observability; +using Paramore.Brighter.RMQ.Tests.TestDoubles; +using Paramore.Brighter.ServiceActivator; +using Polly; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.RMQ.Tests.MessageDispatch; + +[Collection("CommandProcessor")] +public class DispatchBuilderTestsAsync : IDisposable +{ + private readonly IAmADispatchBuilder _builder; + private Dispatcher? _dispatcher; + + public DispatchBuilderTestsAsync() + { + var messageMapperRegistry = new MessageMapperRegistry( + null, + new SimpleMessageMapperFactoryAsync((_) => new MyEventMessageMapperAsync())); + messageMapperRegistry.RegisterAsync(); + + var retryPolicy = Policy + .Handle() + .WaitAndRetry(new[] + { + TimeSpan.FromMilliseconds(50), + TimeSpan.FromMilliseconds(100), + TimeSpan.FromMilliseconds(150) + }); + + var circuitBreakerPolicy = Policy + .Handle() + .CircuitBreaker(1, TimeSpan.FromMilliseconds(500)); + + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; + + var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(rmqConnection); + var container = new ServiceCollection(); + + var tracer = new BrighterTracer(TimeProvider.System); + var instrumentationOptions = InstrumentationOptions.All; + + var commandProcessor = CommandProcessorBuilder.StartNew() + .Handlers(new HandlerConfiguration(new SubscriberRegistry(), new ServiceProviderHandlerFactory(container.BuildServiceProvider()))) + .Policies(new PolicyRegistry + { + { CommandProcessor.RETRYPOLICY, retryPolicy }, + { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } + }) + .NoExternalBus() + .ConfigureInstrumentation(tracer, instrumentationOptions) + .RequestContextFactory(new InMemoryRequestContextFactory()) + .Build(); + + _builder = DispatchBuilder.StartNew() + .CommandProcessorFactory(() => + new CommandProcessorProvider(commandProcessor), + new InMemoryRequestContextFactory() + ) + .MessageMappers(null, messageMapperRegistry, null, new EmptyMessageTransformerFactoryAsync()) + .ChannelFactory(new ChannelFactory(rmqMessageConsumerFactory)) + .Subscriptions(new [] + { + new RmqSubscription( + new SubscriptionName("foo"), + new ChannelName("mary"), + new RoutingKey("bob"), + messagePumpType: MessagePumpType.Proactor, + timeOut: TimeSpan.FromMilliseconds(200)), + new RmqSubscription( + new SubscriptionName("bar"), + new ChannelName("alice"), + new RoutingKey("simon"), + messagePumpType: MessagePumpType.Proactor, + timeOut: TimeSpan.FromMilliseconds(200)) + }) + .ConfigureInstrumentation(tracer, instrumentationOptions); + } + + [Fact] + public async Task When_Building_A_Dispatcher_With_Async() + { + _dispatcher = _builder.Build(); + + _dispatcher.Should().NotBeNull(); + GetConnection("foo").Should().NotBeNull(); + GetConnection("bar").Should().NotBeNull(); + _dispatcher.State.Should().Be(DispatcherState.DS_AWAITING); + + await Task.Delay(1000); + + _dispatcher.Receive(); + + await Task.Delay(1000); + + _dispatcher.State.Should().Be(DispatcherState.DS_RUNNING); + + await _dispatcher.End(); + } + + public void Dispose() + { + CommandProcessor.ClearServiceBus(); + } + + private Subscription GetConnection(string name) + { + return _dispatcher.Subscriptions.SingleOrDefault(conn => conn.Name == name); + } +} 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 4c51fc1691..53d38f26e9 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 @@ -1,28 +1,4 @@ -#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; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.DependencyInjection; @@ -34,90 +10,90 @@ THE SOFTWARE. */ using Polly.Registry; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessageDispatch +namespace Paramore.Brighter.RMQ.Tests.MessageDispatch; + +[Collection("CommandProcessor")] +public class DispatchBuilderWithNamedGateway : IDisposable { - [Collection("CommandProcessor")] - public class DispatchBuilderWithNamedGateway : IDisposable - { - private readonly IAmADispatchBuilder _builder; - private Dispatcher _dispatcher; + private readonly IAmADispatchBuilder _builder; + private Dispatcher _dispatcher; - public DispatchBuilderWithNamedGateway() + public DispatchBuilderWithNamedGateway() + { + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory((_) => new MyEventMessageMapper()), + null + ); + messageMapperRegistry.Register(); + var policyRegistry = new PolicyRegistry { - var messageMapperRegistry = new MessageMapperRegistry( - new SimpleMessageMapperFactory((_) => new MyEventMessageMapper()), - null - ); - messageMapperRegistry.Register(); - var policyRegistry = new PolicyRegistry { - { - CommandProcessor.RETRYPOLICY, Policy - .Handle() - .WaitAndRetry(new[] {TimeSpan.FromMilliseconds(50)}) - }, - { - CommandProcessor.CIRCUITBREAKER, Policy - .Handle() - .CircuitBreaker(1, TimeSpan.FromMilliseconds(500)) - } - }; - - var connection = new RmqMessagingGatewayConnection + CommandProcessor.RETRYPOLICY, Policy + .Handle() + .WaitAndRetry(new[] {TimeSpan.FromMilliseconds(50)}) + }, { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.brighter.exchange") - }; + CommandProcessor.CIRCUITBREAKER, Policy + .Handle() + .CircuitBreaker(1, TimeSpan.FromMilliseconds(500)) + } + }; - var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(connection); + var connection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; + + var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(connection); - var container = new ServiceCollection(); - var tracer = new BrighterTracer(TimeProvider.System); - var instrumentationOptions = InstrumentationOptions.All; + var container = new ServiceCollection(); + var tracer = new BrighterTracer(TimeProvider.System); + var instrumentationOptions = InstrumentationOptions.All; - var commandProcessor = CommandProcessorBuilder.StartNew() - .Handlers(new HandlerConfiguration(new SubscriberRegistry(), new ServiceProviderHandlerFactory(container.BuildServiceProvider()))) - .Policies(policyRegistry) - .NoExternalBus() - .ConfigureInstrumentation(tracer, instrumentationOptions) - .RequestContextFactory(new InMemoryRequestContextFactory()) - .Build(); + var commandProcessor = CommandProcessorBuilder.StartNew() + .Handlers(new HandlerConfiguration(new SubscriberRegistry(), new ServiceProviderHandlerFactory(container.BuildServiceProvider()))) + .Policies(policyRegistry) + .NoExternalBus() + .ConfigureInstrumentation(tracer, instrumentationOptions) + .RequestContextFactory(new InMemoryRequestContextFactory()) + .Build(); - _builder = DispatchBuilder.StartNew() - .CommandProcessorFactory(() => + _builder = DispatchBuilder.StartNew() + .CommandProcessorFactory(() => new CommandProcessorProvider(commandProcessor), - new InMemoryRequestContextFactory() - ) - .MessageMappers(messageMapperRegistry, null, null, null) - .ChannelFactory(new ChannelFactory(rmqMessageConsumerFactory)) - .Subscriptions(new [] - { - new RmqSubscription( - new SubscriptionName("foo"), - new ChannelName("mary"), - new RoutingKey("bob"), - timeOut: TimeSpan.FromMilliseconds(200)), - new RmqSubscription( - new SubscriptionName("bar"), - new ChannelName("alice"), - new RoutingKey("simon"), - timeOut: TimeSpan.FromMilliseconds(200)) - }) - .ConfigureInstrumentation(tracer, instrumentationOptions); - } + new InMemoryRequestContextFactory() + ) + .MessageMappers(messageMapperRegistry, null, new EmptyMessageTransformerFactory(), null) + .ChannelFactory(new ChannelFactory(rmqMessageConsumerFactory)) + .Subscriptions(new [] + { + new RmqSubscription( + new SubscriptionName("foo"), + new ChannelName("mary"), + new RoutingKey("bob"), + messagePumpType: MessagePumpType.Reactor, + timeOut: TimeSpan.FromMilliseconds(200)), + new RmqSubscription( + new SubscriptionName("bar"), + new ChannelName("alice"), + new RoutingKey("simon"), + messagePumpType: MessagePumpType.Reactor, + timeOut: TimeSpan.FromMilliseconds(200)) + }) + .ConfigureInstrumentation(tracer, instrumentationOptions); + } - [Fact] - public void When_building_a_dispatcher_with_named_gateway() - { - _dispatcher = _builder.Build(); + [Fact] + public void When_building_a_dispatcher_with_named_gateway() + { + _dispatcher = _builder.Build(); - //_should_build_a_dispatcher - AssertionExtensions.Should(_dispatcher).NotBeNull(); - } + _dispatcher.Should().NotBeNull(); + } - public void Dispose() - { - CommandProcessor.ClearServiceBus(); - } + public void Dispose() + { + CommandProcessor.ClearServiceBus(); } } 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 new file mode 100644 index 0000000000..6f981df2dc --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/MessageDispatch/When_building_a_dispatcher_with_named_gateway_async.cs @@ -0,0 +1,100 @@ +using System; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.MessagingGateway.RMQ; +using Paramore.Brighter.Observability; +using Paramore.Brighter.RMQ.Tests.TestDoubles; +using Paramore.Brighter.ServiceActivator; +using Polly; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.RMQ.Tests.MessageDispatch; + +[Collection("CommandProcessor")] +public class DispatchBuilderWithNamedGatewayAsync : IDisposable +{ + private readonly IAmADispatchBuilder _builder; + private Dispatcher _dispatcher; + + public DispatchBuilderWithNamedGatewayAsync() + { + var messageMapperRegistry = new MessageMapperRegistry( + null, + new SimpleMessageMapperFactoryAsync((_) => new MyEventMessageMapperAsync()) + ); + messageMapperRegistry.RegisterAsync(); + + var policyRegistry = new PolicyRegistry + { + { + CommandProcessor.RETRYPOLICY, Policy + .Handle() + .WaitAndRetry(new[] {TimeSpan.FromMilliseconds(50)}) + }, + { + CommandProcessor.CIRCUITBREAKER, Policy + .Handle() + .CircuitBreaker(1, TimeSpan.FromMilliseconds(500)) + } + }; + + var connection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; + + var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(connection); + + var container = new ServiceCollection(); + var tracer = new BrighterTracer(TimeProvider.System); + var instrumentationOptions = InstrumentationOptions.All; + + var commandProcessor = CommandProcessorBuilder.StartNew() + .Handlers(new HandlerConfiguration(new SubscriberRegistry(), new ServiceProviderHandlerFactory(container.BuildServiceProvider()))) + .Policies(policyRegistry) + .NoExternalBus() + .ConfigureInstrumentation(tracer, instrumentationOptions) + .RequestContextFactory(new InMemoryRequestContextFactory()) + .Build(); + + _builder = DispatchBuilder.StartNew() + .CommandProcessorFactory(() => + new CommandProcessorProvider(commandProcessor), + new InMemoryRequestContextFactory() + ) + .MessageMappers(messageMapperRegistry, null, null, null) + .ChannelFactory(new ChannelFactory(rmqMessageConsumerFactory)) + .Subscriptions(new [] + { + new RmqSubscription( + new SubscriptionName("foo"), + new ChannelName("mary"), + new RoutingKey("bob"), + messagePumpType: MessagePumpType.Proactor, + timeOut: TimeSpan.FromMilliseconds(200)), + new RmqSubscription( + new SubscriptionName("bar"), + new ChannelName("alice"), + new RoutingKey("simon"), + messagePumpType: MessagePumpType.Proactor, + timeOut: TimeSpan.FromMilliseconds(200)) + }) + .ConfigureInstrumentation(tracer, instrumentationOptions); + } + + [Fact] + public void When_building_a_dispatcher_with_named_gateway() + { + _dispatcher = _builder.Build(); + + _dispatcher.Should().NotBeNull(); + } + + public void Dispose() + { + CommandProcessor.ClearServiceBus(); + } +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/TestHelpers.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/TestHelpers.cs index a0670ed0d1..3e0aa0a593 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/TestHelpers.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/TestHelpers.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2014 Ian Cooper @@ -22,53 +23,36 @@ THE SOFTWARE. */ #endregion -using System; using System.Linq; using System.Threading.Tasks; using Paramore.Brighter.MessagingGateway.RMQ; using RabbitMQ.Client; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway -{ +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; - internal class QueueFactory +internal class QueueFactory(RmqMessagingGatewayConnection connection, ChannelName channelName, RoutingKeys routingKeys) +{ + public async Task CreateAsync() { - private readonly RmqMessagingGatewayConnection _connection; - private readonly ChannelName _channelName; - private readonly RoutingKeys _routingKeys; - - public QueueFactory(RmqMessagingGatewayConnection connection, ChannelName channelName, RoutingKeys routingKeys) - { - _connection = connection; - _channelName = channelName; - _routingKeys = routingKeys; - } - - public void Create(TimeSpan timeToDelayForCreation) + var connectionFactory = new ConnectionFactory { Uri = connection.AmpqUri.Uri }; + await using var connection1 = await connectionFactory.CreateConnectionAsync(); + await using var channel = + await connection1.CreateChannelAsync(new CreateChannelOptions( + publisherConfirmationsEnabled: true, + publisherConfirmationTrackingEnabled: true)); + + await channel.DeclareExchangeForConnection(connection, OnMissingChannel.Create); + await channel.QueueDeclareAsync(channelName.Value, false, false, false, null); + if (routingKeys.Any()) { - var connectionFactory = new ConnectionFactory {Uri = _connection.AmpqUri.Uri}; - using (var connection = connectionFactory.CreateConnection()) + foreach (RoutingKey routingKey in routingKeys) { - using (var channel = connection.CreateModel()) - { - channel.DeclareExchangeForConnection(_connection, OnMissingChannel.Create); - channel.QueueDeclare(_channelName.Value, false, false, false, null); - if (_routingKeys.Any()) - { - foreach (RoutingKey routingKey in _routingKeys) - channel.QueueBind(_channelName.Value, _connection.Exchange.Name, routingKey); - } - else - { - channel.QueueBind(_channelName.Value, _connection.Exchange.Name, _channelName); - } - - } + await channel.QueueBindAsync(channelName.Value, connection.Exchange.Name, routingKey); } - - //We need to delay to actually create these queues before we send to them - Task.Delay(timeToDelayForCreation).Wait(); + } + else + { + await channel.QueueBindAsync(channelName.Value, connection.Exchange.Name, channelName!); } } -} - +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages.cs index b93712842b..280acfc4e6 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages.cs @@ -4,76 +4,75 @@ using Paramore.Brighter.MessagingGateway.RMQ; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RMQBufferedConsumerTests : IDisposable { - [Trait("Category", "RMQ")] - public class RMQBufferedConsumerTests : IDisposable - { - private readonly IAmAMessageProducerSync _messageProducer; - private readonly IAmAMessageConsumer _messageConsumer; - private readonly ChannelName _channelName = new(Guid.NewGuid().ToString()); - private readonly RoutingKey _routingKey = new(Guid.NewGuid().ToString()); - private const int BatchSize = 3; + private readonly IAmAMessageProducerSync _messageProducer; + private readonly IAmAMessageConsumerSync _messageConsumer; + private readonly ChannelName _channelName = new(Guid.NewGuid().ToString()); + private readonly RoutingKey _routingKey = new(Guid.NewGuid().ToString()); + private const int BatchSize = 3; - public RMQBufferedConsumerTests() + public RMQBufferedConsumerTests() + { + var rmqConnection = new RmqMessagingGatewayConnection { - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.brighter.exchange") - }; + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; - _messageProducer = new RmqMessageProducer(rmqConnection); - _messageConsumer = new RmqMessageConsumer(connection:rmqConnection, queueName:_channelName, routingKey:_routingKey, isDurable:false, highAvailability:false, batchSize:BatchSize); + _messageProducer = new RmqMessageProducer(rmqConnection); + _messageConsumer = new RmqMessageConsumer(connection:rmqConnection, queueName:_channelName, routingKey:_routingKey, isDurable:false, highAvailability:false, batchSize:BatchSize); - //create the queue, so that we can receive messages posted to it - new QueueFactory(rmqConnection, _channelName, new RoutingKeys([_routingKey])).Create(TimeSpan.FromMilliseconds(3000)); - } + //create the queue, so that we can receive messages posted to it + new QueueFactory(rmqConnection, _channelName, new RoutingKeys(_routingKey)).CreateAsync().GetAwaiter().GetResult(); + } - [Fact] - public void When_a_message_consumer_reads_multiple_messages() - { - //Post one more than batch size messages - var messageOne = new Message(new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), new MessageBody("test content One")); - _messageProducer.Send(messageOne); - var messageTwo= new Message(new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), new MessageBody("test content Two")); - _messageProducer.Send(messageTwo); - var messageThree= new Message(new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), new MessageBody("test content Three")); - _messageProducer.Send(messageThree); - var messageFour= new Message(new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), new MessageBody("test content Four")); - _messageProducer.Send(messageFour); + [Fact] + public void When_a_message_consumer_reads_multiple_messages() + { + //Post one more than batch size messages + var messageOne = new Message(new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), new MessageBody("test content One")); + _messageProducer.Send(messageOne); + var messageTwo= new Message(new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), new MessageBody("test content Two")); + _messageProducer.Send(messageTwo); + var messageThree= new Message(new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), new MessageBody("test content Three")); + _messageProducer.Send(messageThree); + var messageFour= new Message(new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), new MessageBody("test content Four")); + _messageProducer.Send(messageFour); - //let them arrive - Task.Delay(5000); + //let them arrive + Task.Delay(5000); - //Now retrieve messages from the consumer - var messages = _messageConsumer.Receive(TimeSpan.FromMilliseconds(1000)); + //Now retrieve messages from the consumer + var messages = _messageConsumer.Receive(TimeSpan.FromMilliseconds(1000)); - //We should only have three messages - messages.Length.Should().Be(3); + //We should only have three messages + messages.Length.Should().Be(3); - //ack those to remove from the queue - foreach (var message in messages) - { - _messageConsumer.Acknowledge(message); - } + //ack those to remove from the queue + foreach (var message in messages) + { + _messageConsumer.Acknowledge(message); + } - //Allow ack to register - Task.Delay(1000); + //Allow ack to register + Task.Delay(1000); - //Now retrieve again - messages = _messageConsumer.Receive(TimeSpan.FromMilliseconds(500)); + //Now retrieve again + messages = _messageConsumer.Receive(TimeSpan.FromMilliseconds(500)); - //This time, just the one message - messages.Length.Should().Be(1); + //This time, just the one message + messages.Length.Should().Be(1); - } + } - public void Dispose() - { - _messageConsumer.Purge(); - _messageConsumer.Dispose(); - _messageProducer.Dispose(); - } + public void Dispose() + { + _messageConsumer.Purge(); + _messageConsumer.Dispose(); + _messageProducer.Dispose(); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages_async.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages_async.cs new file mode 100644 index 0000000000..595c698dc8 --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages_async.cs @@ -0,0 +1,84 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.RMQ; +using Xunit; + +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RMQBufferedConsumerTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessageProducerAsync _messageProducer; + private readonly IAmAMessageConsumerAsync _messageConsumer; + private readonly ChannelName _channelName = new(Guid.NewGuid().ToString()); + private readonly RoutingKey _routingKey = new(Guid.NewGuid().ToString()); + private const int BatchSize = 3; + + public RMQBufferedConsumerTestsAsync() + { + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; + + _messageProducer = new RmqMessageProducer(rmqConnection); + _messageConsumer = new RmqMessageConsumer(connection:rmqConnection, queueName:_channelName, routingKey:_routingKey, isDurable:false, highAvailability:false, batchSize:BatchSize); + + //create the queue, so that we can receive messages posted to it + new QueueFactory(rmqConnection, _channelName, new RoutingKeys(_routingKey)).CreateAsync().GetAwaiter().GetResult(); + } + + [Fact] + public async Task When_a_message_consumer_reads_multiple_messages() + { + //Post one more than batch size messages + var messageOne = new Message(new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), new MessageBody("test content One")); + await _messageProducer.SendAsync(messageOne); + var messageTwo = new Message(new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), new MessageBody("test content Two")); + await _messageProducer.SendAsync(messageTwo); + var messageThree = new Message(new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), new MessageBody("test content Three")); + await _messageProducer.SendAsync(messageThree); + var messageFour = new Message(new MessageHeader(Guid.NewGuid().ToString(), _routingKey, MessageType.MT_COMMAND), new MessageBody("test content Four")); + await _messageProducer.SendAsync(messageFour); + + //let them arrive + await Task.Delay(5000); + + //Now retrieve messages from the consumer + var messages = await _messageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000)); + + //We should only have three messages + messages.Length.Should().Be(3); + + //ack those to remove from the queue + foreach (var message in messages) + { + await _messageConsumer.AcknowledgeAsync(message); + } + + //Allow ack to register + await Task.Delay(1000); + + //Now retrieve again + messages = await _messageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(500)); + + //This time, just the one message + messages.Length.Should().Be(1); + } + + public void Dispose() + { + _messageConsumer.PurgeAsync().GetAwaiter().GetResult(); + ((IAmAMessageProducerSync)_messageProducer).Dispose(); + ((IAmAMessageProducerSync)_messageProducer).Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _messageConsumer.PurgeAsync(); + await _messageProducer.DisposeAsync(); + await _messageConsumer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_throws_an_already_closed_exception_when_connecting.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_throws_an_already_closed_exception_when_connecting.cs index 2c0b98ec1d..70e109db94 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_throws_an_already_closed_exception_when_connecting.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_throws_an_already_closed_exception_when_connecting.cs @@ -1,27 +1,3 @@ -#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 FluentAssertions; using Paramore.Brighter.MessagingGateway.RMQ; @@ -29,56 +5,61 @@ THE SOFTWARE. */ using RabbitMQ.Client.Exceptions; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageConsumerConnectionClosedTests : IDisposable { - [Trait("Category", "RMQ")] - public class RmqMessageConsumerConnectionClosedTests : IDisposable - { - private readonly IAmAMessageProducerSync _sender; - private readonly IAmAMessageConsumer _receiver; - private readonly IAmAMessageConsumer _badReceiver; - private readonly Message _sentMessage; - private Exception _firstException; + private readonly IAmAMessageProducerSync _sender; + private readonly IAmAMessageConsumerSync _receiver; + private readonly IAmAMessageConsumerSync _badReceiver; + private readonly Message _sentMessage; + private Exception _firstException; - public RmqMessageConsumerConnectionClosedTests() - { - var messageHeader = new MessageHeader(Guid.NewGuid().ToString(), - new RoutingKey(Guid.NewGuid().ToString()), MessageType.MT_COMMAND); + public RmqMessageConsumerConnectionClosedTests() + { + var messageHeader = new MessageHeader(Guid.NewGuid().ToString(), + new RoutingKey(Guid.NewGuid().ToString()), MessageType.MT_COMMAND); - messageHeader.UpdateHandledCount(); - _sentMessage = new Message(messageHeader, new MessageBody("test content")); + messageHeader.UpdateHandledCount(); + _sentMessage = new Message(messageHeader, new MessageBody("test content")); - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.brighter.exchange") - }; + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; - _sender = new RmqMessageProducer(rmqConnection); - var queueName = new ChannelName(Guid.NewGuid().ToString()); + _sender = new RmqMessageProducer(rmqConnection); + var queueName = new ChannelName(Guid.NewGuid().ToString()); - _receiver = new RmqMessageConsumer(rmqConnection, queueName, _sentMessage.Header.Topic, false, false); - _badReceiver = new AlreadyClosedRmqMessageConsumer(rmqConnection, queueName, _sentMessage.Header.Topic, false, 1, false); + _receiver = new RmqMessageConsumer(rmqConnection, queueName, _sentMessage.Header.Topic, false, false); + _badReceiver = new AlreadyClosedRmqMessageConsumer(rmqConnection, queueName, _sentMessage.Header.Topic, false, 1, false); - _sender.Send(_sentMessage); - } - - [Fact] - public void When_a_message_consumer_throws_an_already_closed_exception_when_connecting() - { - _firstException = Catch.Exception(() => _badReceiver.Receive(TimeSpan.FromMilliseconds(2000))); + } - //_should_return_a_channel_failure_exception - _firstException.Should().BeOfType(); + [Fact] + public void When_a_message_consumer_throws_an_already_closed_exception_when_connecting() + { + _sender.Send(_sentMessage); - //_should_return_an_explaining_inner_exception - _firstException.InnerException.Should().BeOfType(); + bool exceptionHappened = false; + try + { + _badReceiver.Receive(TimeSpan.FromMilliseconds(2000)); } - - public void Dispose() + catch (ChannelFailureException cfe) { - _sender.Dispose(); - _receiver.Dispose(); + exceptionHappened = true; + cfe.InnerException.Should().BeOfType(); } + + exceptionHappened.Should().BeTrue(); + } + + public void Dispose() + { + _sender.Dispose(); + _receiver.Dispose(); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_throws_an_already_closed_exception_when_connecting_async.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_throws_an_already_closed_exception_when_connecting_async.cs new file mode 100644 index 0000000000..22c14a4bd4 --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_throws_an_already_closed_exception_when_connecting_async.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.RMQ; +using Paramore.Brighter.RMQ.Tests.TestDoubles; +using RabbitMQ.Client.Exceptions; +using Xunit; + +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageConsumerConnectionClosedTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessageProducerAsync _sender; + private readonly IAmAMessageConsumerAsync _receiver; + private readonly IAmAMessageConsumerAsync _badReceiver; + private readonly Message _sentMessage; + private Exception _firstException; + + public RmqMessageConsumerConnectionClosedTestsAsync() + { + var messageHeader = new MessageHeader(Guid.NewGuid().ToString(), + new RoutingKey(Guid.NewGuid().ToString()), MessageType.MT_COMMAND); + + messageHeader.UpdateHandledCount(); + _sentMessage = new Message(messageHeader, new MessageBody("test content")); + + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; + + _sender = new RmqMessageProducer(rmqConnection); + var queueName = new ChannelName(Guid.NewGuid().ToString()); + + _receiver = new RmqMessageConsumer(rmqConnection, queueName, _sentMessage.Header.Topic, false, false); + _badReceiver = new AlreadyClosedRmqMessageConsumer(rmqConnection, queueName, _sentMessage.Header.Topic, false, 1, false); + + + } + + [Fact] + public async Task When_a_message_consumer_throws_an_already_closed_exception_when_connecting() + { + await _sender.SendAsync(_sentMessage); + + bool exceptionHappened = false; + try + { + await _badReceiver.ReceiveAsync(TimeSpan.FromMilliseconds(2000)); + } + catch (ChannelFailureException cfe) + { + exceptionHappened = true; + cfe.InnerException.Should().BeOfType(); + } + + exceptionHappened.Should().BeTrue(); + } + + public void Dispose() + { + ((IAmAMessageProducerSync)_sender).Dispose(); + ((IAmAMessageConsumerSync)_receiver).Dispose(); + ((IAmAMessageConsumerSync)_badReceiver).Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _receiver.DisposeAsync(); + await _badReceiver.DisposeAsync(); + await _sender.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_throws_an_not_supported_exception_when_connecting.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_throws_an_not_supported_exception_when_connecting.cs index 0268c0fed6..a94b260f03 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_throws_an_not_supported_exception_when_connecting.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_throws_an_not_supported_exception_when_connecting.cs @@ -28,55 +28,60 @@ THE SOFTWARE. */ using Paramore.Brighter.RMQ.Tests.TestDoubles; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageConsumerChannelFailureTests : IDisposable { - [Trait("Category", "RMQ")] - public class RmqMessageConsumerChannelFailureTests : IDisposable - { - private readonly IAmAMessageProducerSync _sender; - private readonly IAmAMessageConsumer _receiver; - private readonly IAmAMessageConsumer _badReceiver; - private Exception _firstException; + private readonly IAmAMessageProducerSync _sender; + private readonly IAmAMessageConsumerSync _receiver; + private readonly IAmAMessageConsumerSync _badReceiver; + private Exception _firstException; - public RmqMessageConsumerChannelFailureTests() - { - var messageHeader = new MessageHeader(Guid.NewGuid().ToString(), - new RoutingKey(Guid.NewGuid().ToString()), MessageType.MT_COMMAND); + public RmqMessageConsumerChannelFailureTests() + { + var messageHeader = new MessageHeader(Guid.NewGuid().ToString(), + new RoutingKey(Guid.NewGuid().ToString()), MessageType.MT_COMMAND); - messageHeader.UpdateHandledCount(); - Message sentMessage = new(messageHeader, new MessageBody("test content")); + messageHeader.UpdateHandledCount(); + Message sentMessage = new(messageHeader, new MessageBody("test content")); - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.brighter.exchange") - }; + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; - _sender = new RmqMessageProducer(rmqConnection); - var queueName = new ChannelName(Guid.NewGuid().ToString()); + _sender = new RmqMessageProducer(rmqConnection); + var queueName = new ChannelName(Guid.NewGuid().ToString()); - _receiver = new RmqMessageConsumer(rmqConnection, queueName, sentMessage.Header.Topic, false, false); - _badReceiver = new NotSupportedRmqMessageConsumer(rmqConnection,queueName, sentMessage.Header.Topic, false, 1, false); + _receiver = new RmqMessageConsumer(rmqConnection, queueName, sentMessage.Header.Topic, false, false); + _badReceiver = new NotSupportedRmqMessageConsumer(rmqConnection,queueName, sentMessage.Header.Topic, false, 1, false); - _sender.Send(sentMessage); - } + _sender.Send(sentMessage); + } - [Fact] - public void When_a_message_consumer_throws_an_not_supported_exception_when_connecting() + [Fact] + public void When_a_message_consumer_throws_an_not_supported_exception_when_connecting() + { + bool exceptionHappened = false; + try { - _firstException = Catch.Exception(() => _badReceiver.Receive(TimeSpan.FromMilliseconds(2000))); - - //_should_return_a_channel_failure_exception - _firstException.Should().BeOfType(); - //_should_return_an_explaining_inner_exception - _firstException.InnerException.Should().BeOfType(); + _receiver.Receive(TimeSpan.FromMilliseconds(2000)); } - - [Fact] - public void Dispose() + catch (ChannelFailureException cfe) { - _sender.Dispose(); - _receiver.Dispose(); + exceptionHappened = true; + cfe.InnerException.Should().BeOfType(); } + + exceptionHappened.Should().BeTrue(); + } + + [Fact] + public void Dispose() + { + _sender.Dispose(); + _receiver.Dispose(); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_throws_an_operation_interrupted_exception_when_connecting.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_throws_an_operation_interrupted_exception_when_connecting.cs index a6981b1faa..e162f29413 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_throws_an_operation_interrupted_exception_when_connecting.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_a_message_consumer_throws_an_operation_interrupted_exception_when_connecting.cs @@ -30,53 +30,63 @@ THE SOFTWARE. */ using Xunit; [assembly: CollectionBehavior(DisableTestParallelization = true)] -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageConsumerOperationInterruptedTests : IDisposable { - [Trait("Category", "RMQ")] - public class RmqMessageConsumerOperationInterruptedTests : IDisposable - { - private readonly IAmAMessageProducerSync _sender; - private readonly IAmAMessageConsumer _receiver; - private readonly IAmAMessageConsumer _badReceiver; - private readonly Message _sentMessage; - private Exception _firstException; + private readonly IAmAMessageProducerSync _sender; + private readonly IAmAMessageConsumerSync _receiver; + private readonly IAmAMessageConsumerSync _badReceiver; + private readonly Message _sentMessage; + private Exception _firstException; - public RmqMessageConsumerOperationInterruptedTests() - { - var messageHeader = new MessageHeader(Guid.NewGuid().ToString(), - new RoutingKey(Guid.NewGuid().ToString()), MessageType.MT_COMMAND); + public RmqMessageConsumerOperationInterruptedTests() + { + var messageHeader = new MessageHeader(Guid.NewGuid().ToString(), + new RoutingKey(Guid.NewGuid().ToString()), MessageType.MT_COMMAND); - messageHeader.UpdateHandledCount(); - _sentMessage = new Message(messageHeader, new MessageBody("test content")); + messageHeader.UpdateHandledCount(); + _sentMessage = new Message(messageHeader, new MessageBody("test content")); - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.brighter.exchange") - }; + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; - _sender = new RmqMessageProducer(rmqConnection); - _receiver = new RmqMessageConsumer(rmqConnection, new ChannelName(Guid.NewGuid().ToString()), _sentMessage.Header.Topic, false, false); - _badReceiver = new OperationInterruptedRmqMessageConsumer(rmqConnection, new ChannelName(Guid.NewGuid().ToString()), _sentMessage.Header.Topic, false, 1, false); + _sender = new RmqMessageProducer(rmqConnection); + _receiver = new RmqMessageConsumer(rmqConnection, new ChannelName(Guid.NewGuid().ToString()), _sentMessage.Header.Topic, false, false); + _badReceiver = new OperationInterruptedRmqMessageConsumer(rmqConnection, new ChannelName(Guid.NewGuid().ToString()), _sentMessage.Header.Topic, false, 1, false); - _sender.Send(_sentMessage); - } + _sender.Send(_sentMessage); + } - [Fact] - public void When_a_message_consumer_throws_an_operation_interrupted_exception_when_connecting() + [Fact] + public void When_a_message_consumer_throws_an_operation_interrupted_exception_when_connecting() + { + //_should_return_a_channel_failure_exception + _firstException.Should().BeOfType(); + //_should_return_an_explaining_inner_exception + _firstException.InnerException.Should().BeOfType(); + + bool exceptionHappened = false; + try { - _firstException = Catch.Exception(() => _badReceiver.Receive(TimeSpan.FromMilliseconds(2000))); - - //_should_return_a_channel_failure_exception - _firstException.Should().BeOfType(); - //_should_return_an_explaining_inner_exception - _firstException.InnerException.Should().BeOfType(); + _badReceiver.Receive(TimeSpan.FromMilliseconds(2000)); } - - public void Dispose() + catch (ChannelFailureException cfe) { - _sender.Dispose(); - _receiver.Dispose(); + exceptionHappened = true; + cfe.InnerException.Should().BeOfType(); } + + exceptionHappened.Should().BeTrue(); + } + + public void Dispose() + { + _sender.Dispose(); + _receiver.Dispose(); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_binding_a_channel_to_multiple_topics.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_binding_a_channel_to_multiple_topics.cs index f2413f9f65..5c85cda7f0 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_binding_a_channel_to_multiple_topics.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_binding_a_channel_to_multiple_topics.cs @@ -4,67 +4,66 @@ using Paramore.Brighter.MessagingGateway.RMQ; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway -{ - [Trait("Category", "RMQ")] - public class RmqMessageConsumerMultipleTopicTests : IDisposable - { - private readonly IAmAMessageProducerSync _messageProducer; - private readonly IAmAMessageConsumer _messageConsumer; - private readonly Message _messageTopic1, _messageTopic2; +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; - public RmqMessageConsumerMultipleTopicTests() - { - var routingKey = new RoutingKey(Guid.NewGuid().ToString()); +[Trait("Category", "RMQ")] +public class RmqMessageConsumerMultipleTopicTests : IDisposable +{ + private readonly IAmAMessageProducerSync _messageProducer; + private readonly IAmAMessageConsumerSync _messageConsumer; + private readonly Message _messageTopic1, _messageTopic2; + + public RmqMessageConsumerMultipleTopicTests() + { + var routingKey = new RoutingKey(Guid.NewGuid().ToString()); - _messageTopic1 = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), - new MessageBody("test content for topic test 1")); - _messageTopic2 = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), - new MessageBody("test content for topic test 2")); + _messageTopic1 = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), + new MessageBody("test content for topic test 1")); + _messageTopic2 = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), + new MessageBody("test content for topic test 2")); - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.brighter.exchange") - }; + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; - var topics = new RoutingKeys([ - new RoutingKey(_messageTopic1.Header.Topic), - new RoutingKey(_messageTopic2.Header.Topic) - ]); - var queueName = new ChannelName(Guid.NewGuid().ToString()); + var topics = new RoutingKeys([ + new RoutingKey(_messageTopic1.Header.Topic), + new RoutingKey(_messageTopic2.Header.Topic) + ]); + var queueName = new ChannelName(Guid.NewGuid().ToString()); - _messageProducer = new RmqMessageProducer(rmqConnection); - _messageConsumer = new RmqMessageConsumer(rmqConnection, queueName , topics, false, false); + _messageProducer = new RmqMessageProducer(rmqConnection); + _messageConsumer = new RmqMessageConsumer(rmqConnection, queueName , topics, false, false); - new QueueFactory(rmqConnection, queueName, topics).Create(TimeSpan.FromMilliseconds(3000)); - } + new QueueFactory(rmqConnection, queueName, topics).CreateAsync().GetAwaiter().GetResult(); + } - [Fact] - public void When_reading_a_message_from_a_channel_with_multiple_topics() - { - _messageProducer.Send(_messageTopic1); - _messageProducer.Send(_messageTopic2); + [Fact] + public void When_reading_a_message_from_a_channel_with_multiple_topics() + { + _messageProducer.Send(_messageTopic1); + _messageProducer.Send(_messageTopic2); - var topic1Result = _messageConsumer.Receive(TimeSpan.FromMilliseconds(10000)).First(); - _messageConsumer.Acknowledge(topic1Result); - var topic2Result = _messageConsumer.Receive(TimeSpan.FromMilliseconds(10000)).First(); - _messageConsumer.Acknowledge(topic2Result); + var topic1Result = _messageConsumer.Receive(TimeSpan.FromMilliseconds(10000)).First(); + _messageConsumer.Acknowledge(topic1Result); + var topic2Result = _messageConsumer.Receive(TimeSpan.FromMilliseconds(10000)).First(); + _messageConsumer.Acknowledge(topic2Result); - // should_received_a_message_from_test1_with_same_topic_and_body - topic1Result.Header.Topic.Should().Be(_messageTopic1.Header.Topic); - topic1Result.Body.Value.Should().BeEquivalentTo(_messageTopic1.Body.Value); + // should_received_a_message_from_test1_with_same_topic_and_body + topic1Result.Header.Topic.Should().Be(_messageTopic1.Header.Topic); + topic1Result.Body.Value.Should().BeEquivalentTo(_messageTopic1.Body.Value); - // should_received_a_message_from_test2_with_same_topic_and_body - topic2Result.Header.Topic.Should().Be(_messageTopic2.Header.Topic); - topic2Result.Body.Value.Should().BeEquivalentTo(_messageTopic2.Body.Value); - } + // should_received_a_message_from_test2_with_same_topic_and_body + topic2Result.Header.Topic.Should().Be(_messageTopic2.Header.Topic); + topic2Result.Body.Value.Should().BeEquivalentTo(_messageTopic2.Body.Value); + } - public void Dispose() - { - _messageProducer.Dispose(); - } + public void Dispose() + { + _messageProducer.Dispose(); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_confirming_posting_a_message_via_the_messaging_gateway.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_confirming_posting_a_message_via_the_messaging_gateway.cs index 4d94ff0cf7..ec3706778a 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_confirming_posting_a_message_via_the_messaging_gateway.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_confirming_posting_a_message_via_the_messaging_gateway.cs @@ -28,65 +28,66 @@ THE SOFTWARE. */ using Paramore.Brighter.MessagingGateway.RMQ; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageProducerConfirmationsSendMessageTests : IDisposable { - [Trait("Category", "RMQ")] - public class RmqMessageProducerConfirmationsSendMessageTests : IDisposable + private readonly RmqMessageProducer _messageProducer; + private readonly Message _message; + private bool _messageWasPublished = false; + private bool _messageWasNotPublished = true; + + public RmqMessageProducerConfirmationsSendMessageTests () { - private readonly RmqMessageProducer _messageProducer; - private readonly Message _message; - private bool _messageWasPublished = false; - private bool _messageWasNotPublished = true; + _message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), + MessageType.MT_COMMAND), + new MessageBody("test content")); - public RmqMessageProducerConfirmationsSendMessageTests () + var rmqConnection = new RmqMessagingGatewayConnection { - _message = new Message( - new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), - MessageType.MT_COMMAND), - new MessageBody("test content")); + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; - var rmqConnection = new RmqMessagingGatewayConnection + _messageProducer = new RmqMessageProducer(rmqConnection); + _messageProducer.OnMessagePublished += (success, guid) => + { + if (success) { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.brighter.exchange") - }; - - _messageProducer = new RmqMessageProducer(rmqConnection); - _messageProducer.OnMessagePublished += (success, guid) => + guid.Should().Be(_message.Id); + _messageWasPublished = true; + _messageWasNotPublished = false; + } + else { - if (success) - { - guid.Should().Be(_message.Id); - _messageWasPublished = true; - _messageWasNotPublished = false; - } - else - { - _messageWasNotPublished = true; - } - }; + _messageWasNotPublished = true; + } + }; - //we need a queue to avoid a discard - new QueueFactory(rmqConnection, new ChannelName(Guid.NewGuid().ToString()), new RoutingKeys(_message.Header.Topic)) - .Create(TimeSpan.FromMilliseconds(3000)); - } + //we need a queue to avoid a discard + new QueueFactory(rmqConnection, new ChannelName(Guid.NewGuid().ToString()), new RoutingKeys(_message.Header.Topic)) + .CreateAsync() + .GetAwaiter() + .GetResult(); + } - [Fact] - public async Task When_confirming_posting_a_message_via_the_messaging_gateway() - { - _messageProducer.Send(_message); + [Fact] + public async Task When_confirming_posting_a_message_via_the_messaging_gateway() + { + _messageProducer.Send(_message); - await Task.Delay(500); + await Task.Delay(500); - //if this is true, then possible test failed because of timeout or RMQ issues - _messageWasNotPublished.Should().BeFalse(); - //did we see the message - intent to test logic here - _messageWasPublished.Should().BeTrue(); - } + //if this is true, then possible test failed because of timeout or RMQ issues + _messageWasNotPublished.Should().BeFalse(); + //did we see the message - intent to test logic here + _messageWasPublished.Should().BeTrue(); + } - public void Dispose() - { - _messageProducer.Dispose(); - } + public void Dispose() + { + _messageProducer.Dispose(); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_confirming_posting_a_message_via_the_messaging_gateway_async.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_confirming_posting_a_message_via_the_messaging_gateway_async.cs index afcb8f864c..eb96fb61a7 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_confirming_posting_a_message_via_the_messaging_gateway_async.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_confirming_posting_a_message_via_the_messaging_gateway_async.cs @@ -28,66 +28,66 @@ THE SOFTWARE. */ using Paramore.Brighter.MessagingGateway.RMQ; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageProducerConfirmationsSendMessageAsyncTests : IDisposable { - [Trait("Category", "RMQ")] - public class RmqMessageProducerConfirmationsSendMessageAsyncTests : IDisposable + private readonly RmqMessageProducer _messageProducer; + private readonly Message _message; + private bool _messageWasPublished = false; + private bool _messageWasNotPublished = true; + + public RmqMessageProducerConfirmationsSendMessageAsyncTests() { - private readonly RmqMessageProducer _messageProducer; - private readonly Message _message; - private bool _messageWasPublished = false; - private bool _messageWasNotPublished = true; + _message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), + MessageType.MT_COMMAND), + new MessageBody("test content")); - public RmqMessageProducerConfirmationsSendMessageAsyncTests() + var rmqConnection = new RmqMessagingGatewayConnection { - _message = new Message( - new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), - MessageType.MT_COMMAND), - new MessageBody("test content")); + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; - var rmqConnection = new RmqMessagingGatewayConnection + _messageProducer = new RmqMessageProducer(rmqConnection); + _messageProducer.OnMessagePublished += (success, guid) => + { + if (success) { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.brighter.exchange") - }; - - _messageProducer = new RmqMessageProducer(rmqConnection); - _messageProducer.OnMessagePublished += (success, guid) => + guid.Should().Be(_message.Id); + _messageWasPublished = true; + _messageWasNotPublished = false; + } + else { - if (success) - { - guid.Should().Be(_message.Id); - _messageWasPublished = true; - _messageWasNotPublished = false; - } - else - { - _messageWasNotPublished = true; - } - }; + _messageWasNotPublished = true; + } + }; - //we need a queue to avoid a discard - new QueueFactory(rmqConnection, new ChannelName(Guid.NewGuid().ToString()), new RoutingKeys(_message.Header.Topic)) - .Create(TimeSpan.FromMilliseconds(3000)); - } + //we need a queue to avoid a discard + new QueueFactory(rmqConnection, new ChannelName(Guid.NewGuid().ToString()), new RoutingKeys(_message.Header.Topic)) + .CreateAsync() + .GetAwaiter() + .GetResult(); + } - [Fact] - public async Task When_confirming_posting_a_message_via_the_messaging_gateway_async() - { - //The RMQ client doesn't support async, so this is async over sync, but let's check it works all the same - await _messageProducer.SendAsync(_message); + [Fact] + public async Task When_confirming_posting_a_message_via_the_messaging_gateway_async() + { + await _messageProducer.SendAsync(_message); - await Task.Delay(500); + await Task.Delay(500); - //if this is true, then possible test failed because of timeout or RMQ issues - _messageWasNotPublished.Should().BeFalse(); - //did we see the message - intent to test logic here - _messageWasPublished.Should().BeTrue(); - } + //if this is true, then possible test failed because of timeout or RMQ issues + _messageWasNotPublished.Should().BeFalse(); + //did we see the message - intent to test logic here + _messageWasPublished.Should().BeTrue(); + } - public void Dispose() - { - _messageProducer.Dispose(); - } + public void Dispose() + { + _messageProducer.Dispose(); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_infrastructure_exists_can_assert.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_infrastructure_exists_can_assert.cs index ed006158fe..99bc658470 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_infrastructure_exists_can_assert.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_infrastructure_exists_can_assert.cs @@ -3,67 +3,66 @@ using Paramore.Brighter.MessagingGateway.RMQ; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +public class RmqAssumeExistingInfrastructureTests : IDisposable { - public class RmqAssumeExistingInfrastructureTests : IDisposable - { - private readonly IAmAMessageProducerSync _messageProducer; - private readonly IAmAMessageConsumer _messageConsumer; - private readonly Message _message; + private readonly IAmAMessageProducerSync _messageProducer; + private readonly IAmAMessageConsumerSync _messageConsumer; + private readonly Message _message; - public RmqAssumeExistingInfrastructureTests() - { - _message = new Message( - new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), - MessageType.MT_COMMAND), - new MessageBody("test content")); + public RmqAssumeExistingInfrastructureTests() + { + _message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), + MessageType.MT_COMMAND), + new MessageBody("test content")); - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange(Guid.NewGuid().ToString()) - }; + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange(Guid.NewGuid().ToString()) + }; - _messageProducer = new RmqMessageProducer(rmqConnection, new RmqPublication{MakeChannels = OnMissingChannel.Assume}); - var queueName = new ChannelName(Guid.NewGuid().ToString()); + _messageProducer = new RmqMessageProducer(rmqConnection, new RmqPublication{MakeChannels = OnMissingChannel.Assume}); + var queueName = new ChannelName(Guid.NewGuid().ToString()); - _messageConsumer = new RmqMessageConsumer( - connection:rmqConnection, - queueName: queueName, - routingKey:_message.Header.Topic, - isDurable: false, - highAvailability:false, - makeChannels: OnMissingChannel.Assume); + _messageConsumer = new RmqMessageConsumer( + connection:rmqConnection, + queueName: queueName, + routingKey:_message.Header.Topic, + isDurable: false, + highAvailability:false, + makeChannels: OnMissingChannel.Assume); - //This creates the infrastructure we want - new QueueFactory(rmqConnection, queueName, new RoutingKeys( _message.Header.Topic)) - .Create(TimeSpan.FromMilliseconds(3000)); - } + //This creates the infrastructure we want + new QueueFactory(rmqConnection, queueName, new RoutingKeys( _message.Header.Topic)) + .CreateAsync() + .GetAwaiter() + .GetResult() ; + } - [Fact] - public void When_infrastructure_exists_can_assume_producer() + [Fact] + public void When_infrastructure_exists_can_assume_producer() + { + var exceptionThrown = false; + try { - var exceptionThrown = false; - try - { - //As we validate and don't create, this would throw due to lack of infrastructure if not already created - _messageProducer.Send(_message); - _messageConsumer.Receive(new TimeSpan(10000)); - } - catch (ChannelFailureException) - { - exceptionThrown = true; - } - - exceptionThrown.Should().BeFalse(); + //As we validate and don't create, this would throw due to lack of infrastructure if not already created + _messageProducer.Send(_message); + _messageConsumer.Receive(new TimeSpan(10000)); } - - public void Dispose() + catch (ChannelFailureException) { - _messageProducer.Dispose(); - _messageConsumer.Dispose(); - } + exceptionThrown = true; + } + + exceptionThrown.Should().BeFalse(); } - - + + public void Dispose() + { + _messageProducer.Dispose(); + _messageConsumer.Dispose(); + } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_infrastructure_exists_can_assert_async.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_infrastructure_exists_can_assert_async.cs new file mode 100644 index 0000000000..d1cc5946de --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_infrastructure_exists_can_assert_async.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.RMQ; +using Xunit; + +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +public class RmqAssumeExistingInfrastructureTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessageProducerAsync _messageProducer; + private readonly IAmAMessageConsumerAsync _messageConsumer; + private readonly Message _message; + + public RmqAssumeExistingInfrastructureTestsAsync() + { + _message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), + MessageType.MT_COMMAND), + new MessageBody("test content")); + + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange(Guid.NewGuid().ToString()) + }; + + _messageProducer = new RmqMessageProducer(rmqConnection, new RmqPublication{MakeChannels = OnMissingChannel.Assume}); + var queueName = new ChannelName(Guid.NewGuid().ToString()); + + _messageConsumer = new RmqMessageConsumer( + connection:rmqConnection, + queueName: queueName, + routingKey:_message.Header.Topic, + isDurable: false, + highAvailability:false, + makeChannels: OnMissingChannel.Assume); + + //This creates the infrastructure we want + new QueueFactory(rmqConnection, queueName, new RoutingKeys( _message.Header.Topic)) + .CreateAsync() + .GetAwaiter() + .GetResult() ; + } + + [Fact] + public async Task When_infrastructure_exists_can_assume_producer() + { + var exceptionThrown = false; + try + { + //As we validate and don't create, this would throw due to lack of infrastructure if not already created + await _messageProducer.SendAsync(_message); + await _messageConsumer.ReceiveAsync(new TimeSpan(10000)); + } + catch (ChannelFailureException) + { + exceptionThrown = true; + } + + exceptionThrown.Should().BeFalse(); + } + + public void Dispose() + { + ((IAmAMessageProducerSync)_messageProducer).Dispose(); + ((IAmAMessageConsumerSync)_messageConsumer).Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _messageProducer.DisposeAsync(); + await _messageConsumer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_infrastructure_exists_can_validate.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_infrastructure_exists_can_validate.cs index a52300b157..48d7144450 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_infrastructure_exists_can_validate.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_infrastructure_exists_can_validate.cs @@ -3,65 +3,66 @@ using Paramore.Brighter.MessagingGateway.RMQ; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +public class RmqValidateExistingInfrastructureTests : IDisposable { - public class RmqValidateExistingInfrastructureTests : IDisposable - { - private readonly IAmAMessageProducerSync _messageProducer; - private readonly IAmAMessageConsumer _messageConsumer; - private readonly Message _message; + private readonly IAmAMessageProducerSync _messageProducer; + private readonly IAmAMessageConsumerSync _messageConsumer; + private readonly Message _message; - public RmqValidateExistingInfrastructureTests() - { - var routingKey = new RoutingKey(Guid.NewGuid().ToString()); - var queueName = new ChannelName(Guid.NewGuid().ToString()); + public RmqValidateExistingInfrastructureTests() + { + var routingKey = new RoutingKey(Guid.NewGuid().ToString()); + var queueName = new ChannelName(Guid.NewGuid().ToString()); - _message = new Message(new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), - new MessageBody("test content") - ); + _message = new Message(new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), + new MessageBody("test content") + ); - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.brighter.exchange") - }; + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; - _messageProducer = new RmqMessageProducer(rmqConnection, new RmqPublication{MakeChannels = OnMissingChannel.Validate}); - _messageConsumer = new RmqMessageConsumer( - connection: rmqConnection, - queueName: queueName, - routingKey: routingKey, - isDurable: false, - highAvailability: false, - makeChannels: OnMissingChannel.Validate); + _messageProducer = new RmqMessageProducer(rmqConnection, new RmqPublication{MakeChannels = OnMissingChannel.Validate}); + _messageConsumer = new RmqMessageConsumer( + connection: rmqConnection, + queueName: queueName, + routingKey: routingKey, + isDurable: false, + highAvailability: false, + makeChannels: OnMissingChannel.Validate); - //This creates the infrastructure we want - new QueueFactory(rmqConnection, queueName, new RoutingKeys(routingKey)) - .Create(TimeSpan.FromMilliseconds(3000)); - } + //This creates the infrastructure we want + new QueueFactory(rmqConnection, queueName, new RoutingKeys(routingKey)) + .CreateAsync() + .GetAwaiter() + .GetResult(); + } - [Fact] - public void When_infrastructure_exists_can_validate_producer() + [Fact] + public void When_infrastructure_exists_can_validate_producer() + { + var exceptionThrown = false; + try { - var exceptionThrown = false; - try - { - //As we validate and don't create, this would throw due to lack of infrastructure if not already created - _messageProducer.Send(_message); - _messageConsumer.Receive(TimeSpan.FromMilliseconds(10000)); - } - catch (ChannelFailureException cfe) - { - exceptionThrown = true; - } - - exceptionThrown.Should().BeFalse(); + //As we validate and don't create, this would throw due to lack of infrastructure if not already created + _messageProducer.Send(_message); + _messageConsumer.Receive(TimeSpan.FromMilliseconds(10000)); } - - public void Dispose() + catch (ChannelFailureException cfe) { - _messageProducer.Dispose(); - _messageConsumer.Dispose(); + exceptionThrown = true; } + + exceptionThrown.Should().BeFalse(); + } + + public void Dispose() + { + _messageProducer.Dispose(); + _messageConsumer.Dispose(); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_infrastructure_exists_can_validate_async.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_infrastructure_exists_can_validate_async.cs new file mode 100644 index 0000000000..afc8075753 --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_infrastructure_exists_can_validate_async.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.RMQ; +using Xunit; + +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +public class RmqValidateExistingInfrastructureTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessageProducerAsync _messageProducer; + private readonly IAmAMessageConsumerAsync _messageConsumer; + private readonly Message _message; + + public RmqValidateExistingInfrastructureTestsAsync() + { + var routingKey = new RoutingKey(Guid.NewGuid().ToString()); + var queueName = new ChannelName(Guid.NewGuid().ToString()); + + _message = new Message(new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), + new MessageBody("test content") + ); + + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; + + _messageProducer = new RmqMessageProducer(rmqConnection, new RmqPublication{MakeChannels = OnMissingChannel.Validate}); + _messageConsumer = new RmqMessageConsumer( + connection: rmqConnection, + queueName: queueName, + routingKey: routingKey, + isDurable: false, + highAvailability: false, + makeChannels: OnMissingChannel.Validate); + + //This creates the infrastructure we want + new QueueFactory(rmqConnection, queueName, new RoutingKeys(routingKey)) + .CreateAsync() + .GetAwaiter() + .GetResult(); + } + + [Fact] + public async Task When_infrastructure_exists_can_validate_producer() + { + var exceptionThrown = false; + try + { + //As we validate and don't create, this would throw due to lack of infrastructure if not already created + await _messageProducer.SendAsync(_message); + await _messageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(10000)); + } + catch (ChannelFailureException cfe) + { + exceptionThrown = true; + } + + exceptionThrown.Should().BeFalse(); + } + + public void Dispose() + { + ((IAmAMessageProducerSync)_messageProducer).Dispose(); + ((IAmAMessageConsumerSync)_messageConsumer).Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _messageProducer.DisposeAsync(); + await _messageConsumer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_multiple_threads_try_to_post_a_message_at_the_same_time.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_multiple_threads_try_to_post_a_message_at_the_same_time.cs index ae883a7564..d8d036051f 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_multiple_threads_try_to_post_a_message_at_the_same_time.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_multiple_threads_try_to_post_a_message_at_the_same_time.cs @@ -1,27 +1,3 @@ -#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.Linq; using System.Threading.Tasks; @@ -29,53 +5,52 @@ THE SOFTWARE. */ using Paramore.Brighter.MessagingGateway.RMQ; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageProducerSupportsMultipleThreadsTests : IDisposable { - [Trait("Category", "RMQ")] - public class RmqMessageProducerSupportsMultipleThreadsTests : IDisposable + private readonly IAmAMessageProducerSync _messageProducer; + private readonly Message _message; + + public RmqMessageProducerSupportsMultipleThreadsTests() { - private readonly IAmAMessageProducerSync _messageProducer; - private readonly Message _message; + _message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("nonexistenttopic"), + MessageType.MT_COMMAND), + new MessageBody("test content")); - public RmqMessageProducerSupportsMultipleThreadsTests() + var rmqConnection = new RmqMessagingGatewayConnection { - _message = new Message( - new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("nonexistenttopic"), - MessageType.MT_COMMAND), - new MessageBody("test content")); + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.brighter.exchange") - }; - - _messageProducer = new RmqMessageProducer(rmqConnection); - } + _messageProducer = new RmqMessageProducer(rmqConnection); + } - [Fact] - public void When_multiple_threads_try_to_post_a_message_at_the_same_time() + [Fact] + public void When_multiple_threads_try_to_post_a_message_at_the_same_time() + { + bool exceptionHappened = false; + try { - bool exceptionHappened = false; - try - { - Parallel.ForEach(Enumerable.Range(0, 10), _ => - { - _messageProducer.Send(_message); - }); - } - catch (Exception) + Parallel.ForEach(Enumerable.Range(0, 10), _ => { - exceptionHappened = true; - } - - //_should_not_throw - exceptionHappened.Should().BeFalse(); + _messageProducer.Send(_message); + }); } - - public void Dispose() + catch (Exception) { - _messageProducer.Dispose(); + exceptionHappened = true; } + + //_should_not_throw + exceptionHappened.Should().BeFalse(); + } + + public void Dispose() + { + _messageProducer.Dispose(); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_multiple_threads_try_to_post_a_message_at_the_same_time_async.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_multiple_threads_try_to_post_a_message_at_the_same_time_async.cs new file mode 100644 index 0000000000..dbf1e1a5c1 --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_multiple_threads_try_to_post_a_message_at_the_same_time_async.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.RMQ; +using Xunit; + +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageProducerSupportsMultipleThreadsTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessageProducerAsync _messageProducer; + private readonly Message _message; + + public RmqMessageProducerSupportsMultipleThreadsTestsAsync() + { + _message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey("nonexistenttopic"), + MessageType.MT_COMMAND), + new MessageBody("test content")); + + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; + + _messageProducer = new RmqMessageProducer(rmqConnection); + } + + [Fact] + public async Task When_multiple_threads_try_to_post_a_message_at_the_same_time() + { + bool exceptionHappened = false; + try + { + var options = new ParallelOptions { MaxDegreeOfParallelism = 4 }; + await Parallel.ForEachAsync(Enumerable.Range(0, 10), options, async (_, ct) => + { + await _messageProducer.SendAsync(_message, ct); + }); + } + catch (Exception) + { + exceptionHappened = true; + } + + //_should_not_throw + exceptionHappened.Should().BeFalse(); + } + + public void Dispose() + { + ((IAmAMessageProducerSync)_messageProducer).Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_but_no_broker_created.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_but_no_broker_created.cs index 0047169da5..0616512bda 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_but_no_broker_created.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_but_no_broker_created.cs @@ -2,39 +2,38 @@ using Paramore.Brighter.MessagingGateway.RMQ; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +public class RmqBrokerNotPreCreatedTests : IDisposable { - public class RmqBrokerNotPreCreatedTests : IDisposable + private Message _message; + private RmqMessageProducer _messageProducer; + + public RmqBrokerNotPreCreatedTests() { - private Message _message; - private RmqMessageProducer _messageProducer; + _message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), + MessageType.MT_COMMAND), + new MessageBody("test content")); - public RmqBrokerNotPreCreatedTests() + var rmqConnection = new RmqMessagingGatewayConnection { - _message = new Message( - new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), - MessageType.MT_COMMAND), - new MessageBody("test content")); - - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange(Guid.NewGuid().ToString()) - }; + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange(Guid.NewGuid().ToString()) + }; - _messageProducer = new RmqMessageProducer(rmqConnection, new RmqPublication{MakeChannels = OnMissingChannel.Validate}); + _messageProducer = new RmqMessageProducer(rmqConnection, new RmqPublication{MakeChannels = OnMissingChannel.Validate}); - } + } - [Fact] - public void When_posting_a_message_but_no_broker_created() - { - Assert.Throws(() => _messageProducer.Send(_message)); - } + [Fact] + public void When_posting_a_message_but_no_broker_created() + { + Assert.Throws(() => _messageProducer.Send(_message)); + } - public void Dispose() - { - _messageProducer.Dispose(); - } + public void Dispose() + { + _messageProducer.Dispose(); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_but_no_broker_created_async.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_but_no_broker_created_async.cs new file mode 100644 index 0000000000..737e3c4eb9 --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_but_no_broker_created_async.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.RMQ; +using Xunit; + +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +public class RmqBrokerNotPreCreatedTestsAsync : IDisposable +{ + private Message _message; + private RmqMessageProducer _messageProducer; + + public RmqBrokerNotPreCreatedTestsAsync() + { + _message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), + MessageType.MT_COMMAND), + new MessageBody("test content")); + + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange(Guid.NewGuid().ToString()) + }; + + _messageProducer = new RmqMessageProducer(rmqConnection, new RmqPublication{MakeChannels = OnMissingChannel.Validate}); + + } + + [Fact] + public async Task When_posting_a_message_but_no_broker_created() + { + bool exceptionHappened = false; + try + { + await _messageProducer.SendAsync(_message); + } + catch (ChannelFailureException) + { + exceptionHappened = true; + } + + exceptionHappened.Should().BeTrue(); + } + + public void Dispose() + { + _messageProducer.Dispose(); + } +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_to_persist_via_the_messaging_gateway.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_to_persist_via_the_messaging_gateway.cs index 5a6643d1ba..bd1a4745f1 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_to_persist_via_the_messaging_gateway.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_to_persist_via_the_messaging_gateway.cs @@ -4,55 +4,55 @@ using Paramore.Brighter.MessagingGateway.RMQ; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageProducerSendPersistentMessageTests : IDisposable { - [Trait("Category", "RMQ")] - public class RmqMessageProducerSendPersistentMessageTests : IDisposable + private IAmAMessageProducerSync _messageProducer; + private IAmAMessageConsumerSync _messageConsumer; + private Message _message; + + public RmqMessageProducerSendPersistentMessageTests() { - private IAmAMessageProducerSync _messageProducer; - private IAmAMessageConsumer _messageConsumer; - private Message _message; + _message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), + MessageType.MT_COMMAND), + new MessageBody("test content")); - public RmqMessageProducerSendPersistentMessageTests() + var rmqConnection = new RmqMessagingGatewayConnection { - _message = new Message( - new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), - MessageType.MT_COMMAND), - new MessageBody("test content")); - - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.brighter.exchange"), - PersistMessages = true - }; - - _messageProducer = new RmqMessageProducer(rmqConnection); - var queueName = new ChannelName(Guid.NewGuid().ToString()); + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange"), + PersistMessages = true + }; + + _messageProducer = new RmqMessageProducer(rmqConnection); + var queueName = new ChannelName(Guid.NewGuid().ToString()); - _messageConsumer = new RmqMessageConsumer(rmqConnection, queueName, _message.Header.Topic, false); + _messageConsumer = new RmqMessageConsumer(rmqConnection, queueName, _message.Header.Topic, false); - new QueueFactory(rmqConnection, queueName, new RoutingKeys( _message.Header.Topic)) - .Create(TimeSpan.FromMilliseconds(3000)); - } + new QueueFactory(rmqConnection, queueName, new RoutingKeys( _message.Header.Topic)) + .CreateAsync() + .GetAwaiter() + .GetResult(); + } - [Fact] - public void When_posting_a_message_to_persist_via_the_messaging_gateway() - { - // arrange - _messageProducer.Send(_message); + [Fact] + public void When_posting_a_message_to_persist_via_the_messaging_gateway() + { + // arrange + _messageProducer.Send(_message); - // act - var result = _messageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).First(); + // act + var result = _messageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).First(); - // assert - result.Persist.Should().Be(true); - } + // assert + result.Persist.Should().Be(true); + } - public void Dispose() - { - _messageProducer.Dispose(); - } + public void Dispose() + { + _messageProducer.Dispose(); } } - diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_to_persist_via_the_messaging_gateway_async.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_to_persist_via_the_messaging_gateway_async.cs new file mode 100644 index 0000000000..b7f31cfb6f --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_to_persist_via_the_messaging_gateway_async.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.RMQ; +using Xunit; + +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageProducerSendPersistentMessageTestsAsync : IDisposable, IAsyncDisposable +{ + private IAmAMessageProducerAsync _messageProducer; + private IAmAMessageConsumerAsync _messageConsumer; + private Message _message; + + public RmqMessageProducerSendPersistentMessageTestsAsync() + { + _message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), + MessageType.MT_COMMAND), + new MessageBody("test content")); + + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange"), + PersistMessages = true + }; + + _messageProducer = new RmqMessageProducer(rmqConnection); + var queueName = new ChannelName(Guid.NewGuid().ToString()); + + _messageConsumer = new RmqMessageConsumer(rmqConnection, queueName, _message.Header.Topic, false); + + new QueueFactory(rmqConnection, queueName, new RoutingKeys( _message.Header.Topic)) + .CreateAsync() + .GetAwaiter() + .GetResult(); + } + + [Fact] + public async Task When_posting_a_message_to_persist_via_the_messaging_gateway() + { + // arrange + await _messageProducer.SendAsync(_message); + + // act + var result = (await _messageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))).First(); + + // assert + result.Persist.Should().Be(true); + } + + public void Dispose() + { + ((IAmAMessageProducerSync)_messageProducer).Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway.cs index 691fce033e..9f381f1e0d 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway.cs @@ -28,51 +28,51 @@ THE SOFTWARE. */ using Paramore.Brighter.MessagingGateway.RMQ; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageProducerSendMessageTests : IDisposable { - [Trait("Category", "RMQ")] - public class RmqMessageProducerSendMessageTests : IDisposable + private readonly IAmAMessageProducerSync _messageProducer; + private readonly IAmAMessageConsumerSync _messageConsumer; + private readonly Message _message; + + public RmqMessageProducerSendMessageTests() { - private readonly IAmAMessageProducerSync _messageProducer; - private readonly IAmAMessageConsumer _messageConsumer; - private readonly Message _message; + _message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), + MessageType.MT_COMMAND), + new MessageBody("test content")); - public RmqMessageProducerSendMessageTests() + var rmqConnection = new RmqMessagingGatewayConnection { - _message = new Message( - new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), - MessageType.MT_COMMAND), - new MessageBody("test content")); - - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.brighter.exchange") - }; + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; - _messageProducer = new RmqMessageProducer(rmqConnection); - var queueName = new ChannelName(Guid.NewGuid().ToString()); + _messageProducer = new RmqMessageProducer(rmqConnection); + var queueName = new ChannelName(Guid.NewGuid().ToString()); - _messageConsumer = new RmqMessageConsumer(rmqConnection, queueName, _message.Header.Topic, false); + _messageConsumer = new RmqMessageConsumer(rmqConnection, queueName, _message.Header.Topic, false); - new QueueFactory(rmqConnection, queueName, new RoutingKeys(_message.Header.Topic)) - .Create(TimeSpan.FromMilliseconds(3000)); - } + new QueueFactory(rmqConnection, queueName, new RoutingKeys(_message.Header.Topic)) + .CreateAsync() + .GetAwaiter() + .GetResult(); + } - [Fact] - public void When_posting_a_message_via_the_messaging_gateway() - { - _messageProducer.Send(_message); + [Fact] + public void When_posting_a_message_via_the_messaging_gateway() + { + _messageProducer.Send(_message); - var result = _messageConsumer.Receive(TimeSpan.FromMilliseconds(10000)).First(); + var result = _messageConsumer.Receive(TimeSpan.FromMilliseconds(10000)).First(); - //_should_send_a_message_via_rmq_with_the_matching_body - result.Body.Value.Should().Be(_message.Body.Value); - } + result.Body.Value.Should().Be(_message.Body.Value); + } - public void Dispose() - { - _messageProducer.Dispose(); - } + public void Dispose() + { + _messageProducer.Dispose(); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway_async.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway_async.cs new file mode 100644 index 0000000000..995a25457b --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway_async.cs @@ -0,0 +1,84 @@ +#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.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.RMQ; +using Xunit; + +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageProducerSendMessageTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessageProducerAsync _messageProducer; + private readonly IAmAMessageConsumerAsync _messageConsumer; + private readonly Message _message; + + public RmqMessageProducerSendMessageTestsAsync() + { + _message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(Guid.NewGuid().ToString()), + MessageType.MT_COMMAND), + new MessageBody("test content")); + + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") + }; + + _messageProducer = new RmqMessageProducer(rmqConnection); + var queueName = new ChannelName(Guid.NewGuid().ToString()); + + _messageConsumer = new RmqMessageConsumer(rmqConnection, queueName, _message.Header.Topic, false); + + new QueueFactory(rmqConnection, queueName, new RoutingKeys(_message.Header.Topic)) + .CreateAsync() + .GetAwaiter() + .GetResult(); + } + + [Fact] + public async Task When_posting_a_message_via_the_messaging_gateway() + { + await _messageProducer.SendAsync(_message); + + var result = (await _messageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(10000))).First(); + + result.Body.Value.Should().Be(_message.Body.Value); + } + + public void Dispose() + { + ((IAmAMessageProducerSync)_messageProducer).Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_queue_length_causes_a_message_to_be_rejected.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_queue_length_causes_a_message_to_be_rejected.cs index 56d95ee4ff..42cdab9cd5 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_queue_length_causes_a_message_to_be_rejected.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_queue_length_causes_a_message_to_be_rejected.cs @@ -28,78 +28,76 @@ THE SOFTWARE. */ using Paramore.Brighter.MessagingGateway.RMQ; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageProducerQueueLengthTests : IDisposable { - [Trait("Category", "RMQ")] - public class RmqMessageProducerQueueLengthTests : IDisposable - { - private readonly IAmAMessageProducerSync _messageProducer; - private readonly IAmAMessageConsumer _messageConsumer; - private readonly Message _messageOne; - private readonly Message _messageTwo; - private readonly ChannelName _queueName = new(Guid.NewGuid().ToString()); + private readonly IAmAMessageProducerSync _messageProducer; + private readonly IAmAMessageConsumerSync _messageConsumer; + private readonly Message _messageOne; + private readonly Message _messageTwo; + private readonly ChannelName _queueName = new(Guid.NewGuid().ToString()); - public RmqMessageProducerQueueLengthTests() - { - var routingKey = new RoutingKey(Guid.NewGuid().ToString()); + public RmqMessageProducerQueueLengthTests() + { + var routingKey = new RoutingKey(Guid.NewGuid().ToString()); - _messageOne = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, - MessageType.MT_COMMAND), - new MessageBody("test content")); + _messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, + MessageType.MT_COMMAND), + new MessageBody("test content")); - _messageTwo = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, - MessageType.MT_COMMAND), - new MessageBody("test content")); + _messageTwo = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, + MessageType.MT_COMMAND), + new MessageBody("test content")); - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.brighter.exchange"), - }; + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange"), + }; - _messageProducer = new RmqMessageProducer(rmqConnection); - - _messageConsumer = new RmqMessageConsumer( - connection: rmqConnection, - queueName: _queueName, - routingKey: routingKey, - isDurable: false, - highAvailability: false, - batchSize: 5, - maxQueueLength: 1, - makeChannels:OnMissingChannel.Create - ); + _messageProducer = new RmqMessageProducer(rmqConnection); - //create the infrastructure - _messageConsumer.Receive(TimeSpan.Zero); - - } + _messageConsumer = new RmqMessageConsumer( + connection: rmqConnection, + queueName: _queueName, + routingKey: routingKey, + isDurable: false, + highAvailability: false, + batchSize: 5, + maxQueueLength: 1, + makeChannels:OnMissingChannel.Create + ); + } - [Fact] - public void When_rejecting_a_message_due_to_queue_length() - { - _messageProducer.Send(_messageOne); - _messageProducer.Send(_messageTwo); + [Fact] + public void When_rejecting_a_message_due_to_queue_length() + { + //create the infrastructure + _messageConsumer.Receive(TimeSpan.Zero); + + _messageProducer.Send(_messageOne); + _messageProducer.Send(_messageTwo); - //check messages are flowing - absence needs to be expiry - var messages = _messageConsumer.Receive(TimeSpan.FromMilliseconds(5000)); - var message = messages.First(); - _messageConsumer.Acknowledge(message); + //check messages are flowing - absence needs to be expiry + var messages = _messageConsumer.Receive(TimeSpan.FromMilliseconds(5000)); + var message = messages.First(); + _messageConsumer.Acknowledge(message); - //should be the first message + //should be the first message - //try to grab the next message - var nextMessages = _messageConsumer.Receive(TimeSpan.FromMilliseconds(5000)); - message = nextMessages.First(); - message.Header.MessageType.Should().Be(MessageType.MT_NONE); + //try to grab the next message + var nextMessages = _messageConsumer.Receive(TimeSpan.FromMilliseconds(5000)); + message = nextMessages.First(); + message.Header.MessageType.Should().Be(MessageType.MT_NONE); - } + } - public void Dispose() - { - _messageProducer.Dispose(); - } + public void Dispose() + { + _messageProducer.Dispose(); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_queue_length_causes_a_message_to_be_rejected_async.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_queue_length_causes_a_message_to_be_rejected_async.cs new file mode 100644 index 0000000000..925587b773 --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_queue_length_causes_a_message_to_be_rejected_async.cs @@ -0,0 +1,110 @@ +#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.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.RMQ; +using Xunit; + +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageProducerQueueLengthTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessageProducerAsync _messageProducer; + private readonly IAmAMessageConsumerAsync _messageConsumer; + private readonly Message _messageOne; + private readonly Message _messageTwo; + private readonly ChannelName _queueName = new(Guid.NewGuid().ToString()); + + public RmqMessageProducerQueueLengthTestsAsync() + { + var routingKey = new RoutingKey(Guid.NewGuid().ToString()); + + _messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, + MessageType.MT_COMMAND), + new MessageBody("test content")); + + _messageTwo = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, + MessageType.MT_COMMAND), + new MessageBody("test content")); + + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange"), + }; + + _messageProducer = new RmqMessageProducer(rmqConnection); + + _messageConsumer = new RmqMessageConsumer( + connection: rmqConnection, + queueName: _queueName, + routingKey: routingKey, + isDurable: false, + highAvailability: false, + batchSize: 5, + maxQueueLength: 1, + makeChannels:OnMissingChannel.Create + ); + + } + + [Fact] + public async Task When_rejecting_a_message_due_to_queue_length() + { + //create the infrastructure + await _messageConsumer.ReceiveAsync(TimeSpan.Zero); + + await _messageProducer.SendAsync(_messageOne); + await _messageProducer.SendAsync(_messageTwo); + + //check messages are flowing - absence needs to be expiry + var messages = await _messageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + var message = messages.First(); + await _messageConsumer.AcknowledgeAsync(message); + + //should be the first message + + //try to grab the next message + var nextMessages = await _messageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + message = nextMessages.First(); + message.Header.MessageType.Should().Be(MessageType.MT_NONE); + + } + + public void Dispose() + { + ((IAmAMessageProducerSync)_messageProducer).Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_reading_a_delayed_message_via_the_messaging_gateway.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_reading_a_delayed_message_via_the_messaging_gateway.cs index d18a0fb8fd..f38eeb8f53 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_reading_a_delayed_message_via_the_messaging_gateway.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_reading_a_delayed_message_via_the_messaging_gateway.cs @@ -28,94 +28,95 @@ THE SOFTWARE. */ using Paramore.Brighter.MessagingGateway.RMQ; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageProducerDelayedMessageTests : IDisposable { - [Trait("Category", "RMQ")] - public class RmqMessageProducerDelayedMessageTests : IDisposable - { - private readonly IAmAMessageProducerSync _messageProducer; - private readonly IAmAMessageConsumer _messageConsumer; - private readonly Message _message; + private readonly IAmAMessageProducerSync _messageProducer; + private readonly IAmAMessageConsumerSync _messageConsumer; + private readonly Message _message; - public RmqMessageProducerDelayedMessageTests() - { - var routingKey = new RoutingKey(Guid.NewGuid().ToString()); + public RmqMessageProducerDelayedMessageTests() + { + var routingKey = new RoutingKey(Guid.NewGuid().ToString()); - var header = new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND); - var originalMessage = new Message(header, new MessageBody("test3 content")); + var header = new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND); + var originalMessage = new Message(header, new MessageBody("test3 content")); - var mutatedHeader = new MessageHeader(header.MessageId, routingKey, MessageType.MT_COMMAND); - mutatedHeader.Bag.Add(HeaderNames.DELAY_MILLISECONDS, 1000); - _message = new Message(mutatedHeader, originalMessage.Body); + var mutatedHeader = new MessageHeader(header.MessageId, routingKey, MessageType.MT_COMMAND); + mutatedHeader.Bag.Add(HeaderNames.DELAY_MILLISECONDS, 1000); + _message = new Message(mutatedHeader, originalMessage.Body); - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.delay.brighter.exchange", supportDelay: true) - }; + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.delay.brighter.exchange", supportDelay: true) + }; - _messageProducer = new RmqMessageProducer(rmqConnection); + _messageProducer = new RmqMessageProducer(rmqConnection); - var queueName = new ChannelName(Guid.NewGuid().ToString()); + var queueName = new ChannelName(Guid.NewGuid().ToString()); - _messageConsumer = new RmqMessageConsumer(rmqConnection, queueName, routingKey, false); + _messageConsumer = new RmqMessageConsumer(rmqConnection, queueName, routingKey, false); - new QueueFactory(rmqConnection, queueName, new RoutingKeys([routingKey])) - .Create(TimeSpan.FromMilliseconds(3000)); - } + new QueueFactory(rmqConnection, queueName, new RoutingKeys([routingKey])) + .CreateAsync() + .GetAwaiter() + .GetResult(); + } - [Fact] - public void When_reading_a_delayed_message_via_the_messaging_gateway() - { - _messageProducer.SendWithDelay(_message, TimeSpan.FromMilliseconds(3000)); + [Fact] + public void When_reading_a_delayed_message_via_the_messaging_gateway() + { + _messageProducer.SendWithDelay(_message, TimeSpan.FromMilliseconds(3000)); - var immediateResult = _messageConsumer.Receive(TimeSpan.Zero).First(); - var deliveredWithoutWait = immediateResult.Header.MessageType == MessageType.MT_NONE; - immediateResult.Header.HandledCount.Should().Be(0); - immediateResult.Header.Delayed.Should().Be(TimeSpan.Zero); + var immediateResult = _messageConsumer.Receive(TimeSpan.Zero).First(); + var deliveredWithoutWait = immediateResult.Header.MessageType == MessageType.MT_NONE; + immediateResult.Header.HandledCount.Should().Be(0); + immediateResult.Header.Delayed.Should().Be(TimeSpan.Zero); - //_should_have_not_been_able_get_message_before_delay - deliveredWithoutWait.Should().BeTrue(); + //_should_have_not_been_able_get_message_before_delay + deliveredWithoutWait.Should().BeTrue(); - var delayedResult = _messageConsumer.Receive(TimeSpan.FromMilliseconds(10000)).First(); + var delayedResult = _messageConsumer.Receive(TimeSpan.FromMilliseconds(10000)).First(); - //_should_send_a_message_via_rmq_with_the_matching_body - delayedResult.Body.Value.Should().Be(_message.Body.Value); - delayedResult.Header.MessageType.Should().Be(MessageType.MT_COMMAND); - delayedResult.Header.HandledCount.Should().Be(0); - delayedResult.Header.Delayed.Should().Be(TimeSpan.FromMilliseconds(3000)); + //_should_send_a_message_via_rmq_with_the_matching_body + delayedResult.Body.Value.Should().Be(_message.Body.Value); + delayedResult.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + delayedResult.Header.HandledCount.Should().Be(0); + delayedResult.Header.Delayed.Should().Be(TimeSpan.FromMilliseconds(3000)); - _messageConsumer.Acknowledge(delayedResult); - } + _messageConsumer.Acknowledge(delayedResult); + } - [Fact] - public void When_requeing_a_failed_message_with_delay() - { - //send & receive a message - _messageProducer.Send(_message); - var message = _messageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); - message.Header.MessageType.Should().Be(MessageType.MT_COMMAND); - message.Header.HandledCount.Should().Be(0); - message.Header.Delayed.Should().Be(TimeSpan.FromMilliseconds(0)); + [Fact] + public void When_requeing_a_failed_message_with_delay() + { + //send & receive a message + _messageProducer.Send(_message); + var message = _messageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); + message.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + message.Header.HandledCount.Should().Be(0); + message.Header.Delayed.Should().Be(TimeSpan.FromMilliseconds(0)); - _messageConsumer.Acknowledge(message); + _messageConsumer.Acknowledge(message); - //now requeue with a delay - _message.Header.UpdateHandledCount(); - _messageConsumer.Requeue(_message, TimeSpan.FromMilliseconds(1000)); + //now requeue with a delay + _message.Header.UpdateHandledCount(); + _messageConsumer.Requeue(_message, TimeSpan.FromMilliseconds(1000)); - //receive and assert - var message2 = _messageConsumer.Receive(TimeSpan.FromMilliseconds(5000)).Single(); - message2.Header.MessageType.Should().Be(MessageType.MT_COMMAND); - message2.Header.HandledCount.Should().Be(1); + //receive and assert + var message2 = _messageConsumer.Receive(TimeSpan.FromMilliseconds(5000)).Single(); + message2.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + message2.Header.HandledCount.Should().Be(1); - _messageConsumer.Acknowledge(message2); - } + _messageConsumer.Acknowledge(message2); + } - public void Dispose() - { - _messageConsumer.Dispose(); - _messageProducer.Dispose(); - } + public void Dispose() + { + _messageConsumer.Dispose(); + _messageProducer.Dispose(); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_reading_a_delayed_message_via_the_messaging_gateway_async.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_reading_a_delayed_message_via_the_messaging_gateway_async.cs new file mode 100644 index 0000000000..51c55337ca --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_reading_a_delayed_message_via_the_messaging_gateway_async.cs @@ -0,0 +1,129 @@ +#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.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.RMQ; +using Xunit; + +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageProducerDelayedMessageTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessageProducerAsync _messageProducer; + private readonly IAmAMessageConsumerAsync _messageConsumer; + private readonly Message _message; + + public RmqMessageProducerDelayedMessageTestsAsync() + { + var routingKey = new RoutingKey(Guid.NewGuid().ToString()); + + var header = new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND); + var originalMessage = new Message(header, new MessageBody("test3 content")); + + var mutatedHeader = new MessageHeader(header.MessageId, routingKey, MessageType.MT_COMMAND); + mutatedHeader.Bag.Add(HeaderNames.DELAY_MILLISECONDS, 1000); + _message = new Message(mutatedHeader, originalMessage.Body); + + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.delay.brighter.exchange", supportDelay: true) + }; + + _messageProducer = new RmqMessageProducer(rmqConnection); + + var queueName = new ChannelName(Guid.NewGuid().ToString()); + + _messageConsumer = new RmqMessageConsumer(rmqConnection, queueName, routingKey, false); + + new QueueFactory(rmqConnection, queueName, new RoutingKeys([routingKey])) + .CreateAsync() + .GetAwaiter() + .GetResult(); + } + + [Fact] + public async Task When_reading_a_delayed_message_via_the_messaging_gateway() + { + await _messageProducer.SendWithDelayAsync(_message, TimeSpan.FromMilliseconds(3000)); + + var immediateResult = (await _messageConsumer.ReceiveAsync(TimeSpan.Zero)).First(); + var deliveredWithoutWait = immediateResult.Header.MessageType == MessageType.MT_NONE; + immediateResult.Header.HandledCount.Should().Be(0); + immediateResult.Header.Delayed.Should().Be(TimeSpan.Zero); + + //_should_have_not_been_able_get_message_before_delay + deliveredWithoutWait.Should().BeTrue(); + + var delayedResult = (await _messageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(10000))).First(); + + //_should_send_a_message_via_rmq_with_the_matching_body + delayedResult.Body.Value.Should().Be(_message.Body.Value); + delayedResult.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + delayedResult.Header.HandledCount.Should().Be(0); + delayedResult.Header.Delayed.Should().Be(TimeSpan.FromMilliseconds(3000)); + + await _messageConsumer.AcknowledgeAsync(delayedResult); + } + + [Fact] + public async Task When_requeing_a_failed_message_with_delay() + { + //send & receive a message + await _messageProducer.SendAsync(_message); + var message = (await _messageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))).Single(); + message.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + message.Header.HandledCount.Should().Be(0); + message.Header.Delayed.Should().Be(TimeSpan.FromMilliseconds(0)); + + await _messageConsumer.AcknowledgeAsync(message); + + //now requeue with a delay + _message.Header.UpdateHandledCount(); + await _messageConsumer.RequeueAsync(_message, TimeSpan.FromMilliseconds(1000)); + + //receive and assert + var message2 = (await _messageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000))).Single(); + message2.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + message2.Header.HandledCount.Should().Be(1); + + await _messageConsumer.AcknowledgeAsync(message2); + } + + public void Dispose() + { + ((IAmAMessageConsumerSync)_messageConsumer).Dispose(); + ((IAmAMessageProducerSync)_messageProducer).Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _messageProducer.DisposeAsync(); + await _messageConsumer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_rejecting_a_message_to_a_dead_letter_queue.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_rejecting_a_message_to_a_dead_letter_queue.cs index aa547091f5..116e72ee76 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_rejecting_a_message_to_a_dead_letter_queue.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_rejecting_a_message_to_a_dead_letter_queue.cs @@ -28,82 +28,80 @@ THE SOFTWARE. */ using Paramore.Brighter.MessagingGateway.RMQ; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageProducerDLQTests : IDisposable { - [Trait("Category", "RMQ")] - public class RmqMessageProducerDLQTests : IDisposable - { - private readonly IAmAMessageProducerSync _messageProducer; - private readonly IAmAMessageConsumer _messageConsumer; - private readonly Message _message; - private readonly IAmAMessageConsumer _deadLetterConsumer; + private readonly IAmAMessageProducerSync _messageProducer; + private readonly IAmAMessageConsumerSync _messageConsumer; + private readonly Message _message; + private readonly IAmAMessageConsumerSync _deadLetterConsumer; - public RmqMessageProducerDLQTests() - { - var routingKey = new RoutingKey(Guid.NewGuid().ToString()); - - _message = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, - MessageType.MT_COMMAND), - new MessageBody("test content")); - - var queueName = new ChannelName(Guid.NewGuid().ToString()); - var deadLetterQueueName = new ChannelName($"{_message.Header.Topic}.DLQ"); - var deadLetterRoutingKey = new RoutingKey( $"{_message.Header.Topic}.DLQ"); + public RmqMessageProducerDLQTests() + { + var routingKey = new RoutingKey(Guid.NewGuid().ToString()); - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.brighter.exchange"), - DeadLetterExchange = new Exchange("paramore.brighter.exchange.dlq") - }; + _message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, + MessageType.MT_COMMAND), + new MessageBody("test content")); + + var queueName = new ChannelName(Guid.NewGuid().ToString()); + var deadLetterQueueName = new ChannelName($"{_message.Header.Topic}.DLQ"); + var deadLetterRoutingKey = new RoutingKey( $"{_message.Header.Topic}.DLQ"); - _messageProducer = new RmqMessageProducer(rmqConnection); - - _messageConsumer = new RmqMessageConsumer( - connection: rmqConnection, - queueName: queueName, - routingKey: routingKey, - isDurable: false, - highAvailability: false, - deadLetterQueueName: deadLetterQueueName, - deadLetterRoutingKey: deadLetterRoutingKey, - makeChannels:OnMissingChannel.Create - ); - - _deadLetterConsumer = new RmqMessageConsumer( - connection: rmqConnection, - queueName: deadLetterQueueName, - routingKey: deadLetterRoutingKey, - isDurable:false, - makeChannels:OnMissingChannel.Assume - ); - - //create the infrastructure - _messageConsumer.Receive(TimeSpan.FromMilliseconds(0)); - - } - - [Fact] - public void When_rejecting_a_message_to_a_dead_letter_queue() + var rmqConnection = new RmqMessagingGatewayConnection { - _messageProducer.Send(_message); + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange"), + DeadLetterExchange = new Exchange("paramore.brighter.exchange.dlq") + }; + + _messageProducer = new RmqMessageProducer(rmqConnection); + + _messageConsumer = new RmqMessageConsumer( + connection: rmqConnection, + queueName: queueName, + routingKey: routingKey, + isDurable: false, + highAvailability: false, + deadLetterQueueName: deadLetterQueueName, + deadLetterRoutingKey: deadLetterRoutingKey, + makeChannels:OnMissingChannel.Create + ); - var message = _messageConsumer.Receive(TimeSpan.FromMilliseconds(10000)).First(); + _deadLetterConsumer = new RmqMessageConsumer( + connection: rmqConnection, + queueName: deadLetterQueueName, + routingKey: deadLetterRoutingKey, + isDurable:false, + makeChannels:OnMissingChannel.Assume + ); + } + + [Fact] + public void When_rejecting_a_message_to_a_dead_letter_queue() + { + //create the infrastructure + _messageConsumer.Receive(TimeSpan.FromMilliseconds(0)); + + _messageProducer.Send(_message); + + var message = _messageConsumer.Receive(TimeSpan.FromMilliseconds(10000)).First(); - //This will push onto the DLQ - _messageConsumer.Reject(message); + //This will push onto the DLQ + _messageConsumer.Reject(message); - var dlqMessage = _deadLetterConsumer.Receive(TimeSpan.FromMilliseconds(10000)).First(); + var dlqMessage = _deadLetterConsumer.Receive(TimeSpan.FromMilliseconds(10000)).First(); - //assert this is our message - dlqMessage.Id.Should().Be(_message.Id); - message.Body.Value.Should().Be(dlqMessage.Body.Value); - } + //assert this is our message + dlqMessage.Id.Should().Be(_message.Id); + message.Body.Value.Should().Be(dlqMessage.Body.Value); + } - public void Dispose() - { - _messageProducer.Dispose(); - } + public void Dispose() + { + _messageProducer.Dispose(); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_rejecting_a_message_to_a_dead_letter_queue_async.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_rejecting_a_message_to_a_dead_letter_queue_async.cs new file mode 100644 index 0000000000..39c4791f5f --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_rejecting_a_message_to_a_dead_letter_queue_async.cs @@ -0,0 +1,113 @@ +#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.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.RMQ; +using Xunit; + +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageProducerDLQTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessageProducerAsync _messageProducer; + private readonly IAmAMessageConsumerAsync _messageConsumer; + private readonly Message _message; + private readonly IAmAMessageConsumerSync _deadLetterConsumer; + + public RmqMessageProducerDLQTestsAsync() + { + var routingKey = new RoutingKey(Guid.NewGuid().ToString()); + + _message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, + MessageType.MT_COMMAND), + new MessageBody("test content")); + + var queueName = new ChannelName(Guid.NewGuid().ToString()); + var deadLetterQueueName = new ChannelName($"{_message.Header.Topic}.DLQ"); + var deadLetterRoutingKey = new RoutingKey( $"{_message.Header.Topic}.DLQ"); + + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange"), + DeadLetterExchange = new Exchange("paramore.brighter.exchange.dlq") + }; + + _messageProducer = new RmqMessageProducer(rmqConnection); + + _messageConsumer = new RmqMessageConsumer( + connection: rmqConnection, + queueName: queueName, + routingKey: routingKey, + isDurable: false, + highAvailability: false, + deadLetterQueueName: deadLetterQueueName, + deadLetterRoutingKey: deadLetterRoutingKey, + makeChannels:OnMissingChannel.Create + ); + + _deadLetterConsumer = new RmqMessageConsumer( + connection: rmqConnection, + queueName: deadLetterQueueName, + routingKey: deadLetterRoutingKey, + isDurable:false, + makeChannels:OnMissingChannel.Assume + ); + } + + [Fact] + public async Task When_rejecting_a_message_to_a_dead_letter_queue() + { + //create the infrastructure + await _messageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(0)); + + await _messageProducer.SendAsync(_message); + + var message = (await _messageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(10000))).First(); + + //This will push onto the DLQ + await _messageConsumer.RejectAsync(message); + + var dlqMessage = _deadLetterConsumer.Receive(TimeSpan.FromMilliseconds(10000)).First(); + + //assert this is our message + dlqMessage.Id.Should().Be(_message.Id); + message.Body.Value.Should().Be(dlqMessage.Body.Value); + } + + public void Dispose() + { + ((IAmAMessageProducerSync)_messageProducer).Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_resetting_a_connection_that_does_not_exist.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_resetting_a_connection_that_does_not_exist.cs index 0a57280b35..c57c15cc98 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_resetting_a_connection_that_does_not_exist.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_resetting_a_connection_that_does_not_exist.cs @@ -23,31 +23,35 @@ THE SOFTWARE. */ #endregion using System; +using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.MessagingGateway.RMQ; using RabbitMQ.Client; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageGatewayConnectionPoolResetConnectionDoesNotExist { - [Trait("Category", "RMQ")] - public class RMQMessageGatewayConnectionPoolResetConnectionDoesNotExist + private readonly RmqMessageGatewayConnectionPool _connectionPool = new("MyConnectionName", 7); + + [Fact] + public async Task When_resetting_a_connection_that_does_not_exist() { - private readonly RmqMessageGatewayConnectionPool _connectionPool; + var connectionFactory = new ConnectionFactory {HostName = "invalidhost"}; - public RMQMessageGatewayConnectionPoolResetConnectionDoesNotExist() + bool resetConnectionExceptionThrown = false; + try { - _connectionPool = new RmqMessageGatewayConnectionPool("MyConnectionName", 7); + await _connectionPool.ResetConnectionAsync(connectionFactory); } - - [Fact] - public void When_resetting_a_connection_that_does_not_exist() + catch (Exception ) { - var connectionFactory = new ConnectionFactory {HostName = "invalidhost"}; + resetConnectionExceptionThrown = true; + } + + resetConnectionExceptionThrown.Should().BeFalse(); - Action resetConnection = () => _connectionPool.ResetConnection(connectionFactory); - - resetConnection.Should().NotThrow(); - } } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_resetting_a_connection_that_exists.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_resetting_a_connection_that_exists.cs index 4575412b2d..3c2650734f 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_resetting_a_connection_that_exists.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_resetting_a_connection_that_exists.cs @@ -22,36 +22,36 @@ THE SOFTWARE. */ #endregion +using System.Threading.Tasks; using FluentAssertions; using Paramore.Brighter.MessagingGateway.RMQ; using RabbitMQ.Client; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RMQMessageGatewayConnectionPoolResetConnectionExists { - [Trait("Category", "RMQ")] - public class RMQMessageGatewayConnectionPoolResetConnectionExists - { - private readonly RmqMessageGatewayConnectionPool _connectionPool; - private readonly IConnection _originalConnection; + private readonly RmqMessageGatewayConnectionPool _connectionPool; + private readonly IConnection _originalConnection; - public RMQMessageGatewayConnectionPoolResetConnectionExists() - { - _connectionPool = new RmqMessageGatewayConnectionPool("MyConnectionName", 7); + public RMQMessageGatewayConnectionPoolResetConnectionExists() + { + _connectionPool = new RmqMessageGatewayConnectionPool("MyConnectionName", 7); - var connectionFactory = new ConnectionFactory { HostName = "localhost" }; + var connectionFactory = new ConnectionFactory { HostName = "localhost" }; - _originalConnection = _connectionPool.GetConnection(connectionFactory); - } + _originalConnection = _connectionPool.GetConnection(connectionFactory); + } - [Fact] - public void When_resetting_a_connection_that_exists() - { - var connectionFactory = new ConnectionFactory{HostName = "localhost"}; + [Fact] + public async Task When_resetting_a_connection_that_exists() + { + var connectionFactory = new ConnectionFactory{HostName = "localhost"}; - _connectionPool.ResetConnection(connectionFactory); + await _connectionPool.ResetConnectionAsync(connectionFactory); - _connectionPool.GetConnection(connectionFactory).Should().NotBeSameAs(_originalConnection); - } + (await _connectionPool.GetConnectionAsync(connectionFactory)).Should().NotBeSameAs(_originalConnection); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_retry_limits_force_a_message_onto_the_DLQ.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_retry_limits_force_a_message_onto_the_DLQ.cs index 31cc767745..cdf673f1d1 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_retry_limits_force_a_message_onto_the_DLQ.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_retry_limits_force_a_message_onto_the_DLQ.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json; using System.Threading.Tasks; @@ -9,149 +10,150 @@ using Polly.Registry; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +[Trait("Fragile", "CI")] +public class RMQMessageConsumerRetryDLQTests : IDisposable { - [Trait("Category", "RMQ")] - [Trait("Fragile", "CI")] - public class RMQMessageConsumerRetryDLQTests : IDisposable - { - private readonly IAmAMessagePump _messagePump; - private readonly Message _message; - private readonly IAmAChannel _channel; - private readonly RmqMessageProducer _sender; - private readonly RmqMessageConsumer _deadLetterConsumer; - private readonly RmqSubscription _subscription; + private readonly IAmAMessagePump _messagePump; + private readonly Message _message; + private readonly IAmAChannelSync _channel; + private readonly RmqMessageProducer _sender; + private readonly RmqMessageConsumer _deadLetterConsumer; + private readonly RmqSubscription _subscription; - public RMQMessageConsumerRetryDLQTests() + public RMQMessageConsumerRetryDLQTests() + { + string correlationId = Guid.NewGuid().ToString(); + string contentType = "text\\plain"; + var channelName = new ChannelName($"Requeue-Limit-Tests-{Guid.NewGuid().ToString()}"); + var routingKey = new RoutingKey($"Requeue-Limit-Tests-{Guid.NewGuid().ToString()}"); + + //what do we send + var myCommand = new MyDeferredCommand { Value = "Hello Requeue" }; + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + contentType: contentType + ), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + var deadLetterQueueName = new ChannelName($"{Guid.NewGuid().ToString()}.DLQ"); + var deadLetterRoutingKey = new RoutingKey( $"{_message.Header.Topic}.DLQ"); + + _subscription = new RmqSubscription( + name: new SubscriptionName("DLQ Test Subscription"), + channelName: channelName, + routingKey: routingKey, + //after 0 retries fail and move to the DLQ + requeueCount: 0, + //delay before re-queuing + requeueDelay: TimeSpan.FromMilliseconds(50), + deadLetterChannelName: deadLetterQueueName, + deadLetterRoutingKey: deadLetterRoutingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create + ); + + var rmqConnection = new RmqMessagingGatewayConnection { - string correlationId = Guid.NewGuid().ToString(); - string contentType = "text\\plain"; - var channelName = new ChannelName($"Requeue-Limit-Tests-{Guid.NewGuid().ToString()}"); - var routingKey = new RoutingKey($"Requeue-Limit-Tests-{Guid.NewGuid().ToString()}"); - - //what do we send - var myCommand = new MyDeferredCommand { Value = "Hello Requeue" }; - _message = new Message( - new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, - contentType: contentType - ), - new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) - ); - - var deadLetterQueueName = new ChannelName($"{Guid.NewGuid().ToString()}.DLQ"); - var deadLetterRoutingKey = new RoutingKey( $"{_message.Header.Topic}.DLQ"); - - _subscription = new RmqSubscription( - name: new SubscriptionName("DLQ Test Subscription"), - channelName: channelName, - routingKey: routingKey, - //after 2 retries, fail and move to the DLQ - requeueCount: 2, - //delay before re-queuing - requeueDelay: TimeSpan.FromMilliseconds(50), - deadLetterChannelName: deadLetterQueueName, - deadLetterRoutingKey: deadLetterRoutingKey, - makeChannels: OnMissingChannel.Create - ); - - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.brighter.exchange"), - DeadLetterExchange = new Exchange("paramore.brighter.exchange.dlq") - }; - - //how do we send to the queue - _sender = new RmqMessageProducer(rmqConnection, new RmqPublication - { - Topic = routingKey, - RequestType = typeof(MyDeferredCommand) - }); - - //set up our receiver - ChannelFactory channelFactory = new(new RmqMessageConsumerFactory(rmqConnection)); - _channel = channelFactory.CreateChannel(_subscription); - - //how do we handle a command - IHandleRequests handler = new MyDeferredCommandHandler(); - - //hook up routing for the command processor - var subscriberRegistry = new SubscriberRegistry(); - subscriberRegistry.Register(); - - //once we read, how do we dispatch to a handler. N.B. we don't use this for reading here - IAmACommandProcessor commandProcessor = new CommandProcessor( - subscriberRegistry: subscriberRegistry, - handlerFactory: new QuickHandlerFactory(() => handler), - requestContextFactory: new InMemoryRequestContextFactory(), - policyRegistry: new PolicyRegistry() - ); - var provider = new CommandProcessorProvider(commandProcessor); - - //pump messages from a channel to a handler - in essence we are building our own dispatcher in this test - var messageMapperRegistry = new MessageMapperRegistry( - new SimpleMessageMapperFactory(_ => new MyDeferredCommandMessageMapper()), - null - ); + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange"), + DeadLetterExchange = new Exchange("paramore.brighter.exchange.dlq") + }; + + //how do we send to the queue + _sender = new RmqMessageProducer(rmqConnection, new RmqPublication + { + Topic = routingKey, + RequestType = typeof(MyDeferredCommand) + }); + + //set up our receiver + ChannelFactory channelFactory = new(new RmqMessageConsumerFactory(rmqConnection)); + _channel = channelFactory.CreateSyncChannel(_subscription); + + //how do we handle a command + IHandleRequests handler = new MyDeferredCommandHandler(); + + //hook up routing for the command processor + var subscriberRegistry = new SubscriberRegistry(); + subscriberRegistry.Register(); + + //once we read, how do we dispatch to a handler. N.B. we don't use this for reading here + IAmACommandProcessor commandProcessor = new CommandProcessor( + subscriberRegistry: subscriberRegistry, + handlerFactory: new QuickHandlerFactory(() => handler), + requestContextFactory: new InMemoryRequestContextFactory(), + policyRegistry: new PolicyRegistry() + ); + var provider = new CommandProcessorProvider(commandProcessor); + + //pump messages from a channel to a handler - in essence we are building our own dispatcher in this test + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory(_ => new MyDeferredCommandMessageMapper()), + null + ); - messageMapperRegistry.Register(); + messageMapperRegistry.Register(); - _messagePump = new MessagePumpBlocking(provider, messageMapperRegistry, - new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), _channel) - { - Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 - }; - - _deadLetterConsumer = new RmqMessageConsumer( - connection: rmqConnection, - queueName: deadLetterQueueName, - routingKey: deadLetterRoutingKey, - isDurable: false, - makeChannels: OnMissingChannel.Assume - ); - } - - [Fact] - public async Task When_retry_limits_force_a_message_onto_the_dlq() + _messagePump = new Reactor(provider, messageMapperRegistry, + new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), _channel) { - //NOTE: This test is **slow** because it needs to ensure infrastructure and then wait whilst we requeue a message a number of times, - //then propagate to the DLQ - - //start a message pump, let it create infrastructure - var task = Task.Factory.StartNew(() => _messagePump.Run(), TaskCreationOptions.LongRunning); - await Task.Delay(20000); + Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 0 + }; + + _deadLetterConsumer = new RmqMessageConsumer( + connection: rmqConnection, + queueName: deadLetterQueueName, + routingKey: deadLetterRoutingKey, + isDurable: false, + makeChannels: OnMissingChannel.Assume + ); + } - //put something on an SNS topic, which will be delivered to our SQS queue - _sender.Send(_message); + [Fact(Skip = "Breaks due to fault in Task Scheduler running after context has closed")] + [SuppressMessage("Usage", "xUnit1031:Do not use blocking task operations in test method")] + public async Task When_retry_limits_force_a_message_onto_the_dlq() + { + //NOTE: This test is **slow** because it needs to ensure infrastructure and then wait whilst we requeue a message a number of times, + //then propagate to the DLQ + + //start a message pump, let it create infrastructure + var task = Task.Factory.StartNew(() => _messagePump.Run(), TaskCreationOptions.LongRunning); + await Task.Delay(20000); - //Let the message be handled and deferred until it reaches the DLQ - await Task.Delay(20000); + //put something on an SNS topic, which will be delivered to our SQS queue + _sender.Send(_message); - //send a quit message to the pump to terminate it - var quitMessage = MessageFactory.CreateQuitMessage(_subscription.RoutingKey); - _channel.Enqueue(quitMessage); + //Let the message be handled and deferred until it reaches the DLQ + await Task.Delay(20000); - //wait for the pump to stop once it gets a quit message - await Task.WhenAll(task); + //send a quit message to the pump to terminate it + var quitMessage = MessageFactory.CreateQuitMessage(_subscription.RoutingKey); + _channel.Enqueue(quitMessage); - await Task.Delay(5000); + //wait for the pump to stop once it gets a quit message + await Task.WhenAll(task); - //inspect the dlq - var dlqMessage = _deadLetterConsumer.Receive(new TimeSpan(10000)).First(); + await Task.Delay(5000); - //assert this is our message - dlqMessage.Header.MessageType.Should().Be(MessageType.MT_COMMAND); - dlqMessage.Body.Value.Should().Be(_message.Body.Value); + //inspect the dlq + var dlqMessage = _deadLetterConsumer.Receive(new TimeSpan(10000)).First(); - _deadLetterConsumer.Acknowledge(dlqMessage); + //assert this is our message + dlqMessage.Body.Value.Should().Be(_message.Body.Value); - } + _deadLetterConsumer.Acknowledge(dlqMessage); - public void Dispose() - { - _channel.Dispose(); - } + } + public void Dispose() + { + _channel.Dispose(); + _deadLetterConsumer.Dispose(); } + } diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_retry_limits_force_a_message_onto_the_DLQ_async.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_retry_limits_force_a_message_onto_the_DLQ_async.cs new file mode 100644 index 0000000000..2d593c0ed7 --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_retry_limits_force_a_message_onto_the_DLQ_async.cs @@ -0,0 +1,158 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.RMQ; +using Paramore.Brighter.RMQ.Tests.TestDoubles; +using Paramore.Brighter.ServiceActivator; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +[Trait("Fragile", "CI")] +public class RMQMessageConsumerRetryDLQTestsAsync : IDisposable +{ + private readonly IAmAMessagePump _messagePump; + private readonly Message _message; + private readonly IAmAChannelAsync _channel; + private readonly RmqMessageProducer _sender; + private readonly RmqMessageConsumer _deadLetterConsumer; + private readonly RmqSubscription _subscription; + + + public RMQMessageConsumerRetryDLQTestsAsync() + { + string correlationId = Guid.NewGuid().ToString(); + string contentType = "text\\plain"; + var channelName = new ChannelName($"Requeue-Limit-Tests-{Guid.NewGuid().ToString()}"); + var routingKey = new RoutingKey($"Requeue-Limit-Tests-{Guid.NewGuid().ToString()}"); + + //what do we send + var myCommand = new MyDeferredCommand { Value = "Hello Requeue" }; + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + contentType: contentType + ), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + var deadLetterQueueName = new ChannelName($"{Guid.NewGuid().ToString()}.DLQ"); + var deadLetterRoutingKey = new RoutingKey( $"{_message.Header.Topic}.DLQ"); + + _subscription = new RmqSubscription( + name: new SubscriptionName("DLQ Test Subscription"), + channelName: channelName, + routingKey: routingKey, + //after 2 retries, fail and move to the DLQ + requeueCount: 2, + //delay before re-queuing + requeueDelay: TimeSpan.FromMilliseconds(50), + deadLetterChannelName: deadLetterQueueName, + deadLetterRoutingKey: deadLetterRoutingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + ); + + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange"), + DeadLetterExchange = new Exchange("paramore.brighter.exchange.dlq") + }; + + //how do we send to the queue + _sender = new RmqMessageProducer(rmqConnection, new RmqPublication + { + Topic = routingKey, + RequestType = typeof(MyDeferredCommand) + }); + + //set up our receiver + ChannelFactory channelFactory = new(new RmqMessageConsumerFactory(rmqConnection)); + _channel = channelFactory.CreateAsyncChannel(_subscription); + + //how do we handle a command + IHandleRequestsAsync handler = new MyDeferredCommandHandlerAsync(); + + //hook up routing for the command processor + var subscriberRegistry = new SubscriberRegistry(); + subscriberRegistry.RegisterAsync(); + + //once we read, how do we dispatch to a handler. N.B. we don't use this for reading here + IAmACommandProcessor commandProcessor = new CommandProcessor( + subscriberRegistry: subscriberRegistry, + handlerFactory: new QuickHandlerFactoryAsync(() => handler), + requestContextFactory: new InMemoryRequestContextFactory(), + policyRegistry: new PolicyRegistry() + ); + var provider = new CommandProcessorProvider(commandProcessor); + + //pump messages from a channel to a handler - in essence we are building our own dispatcher in this test + var messageMapperRegistry = new MessageMapperRegistry( + null, + new SimpleMessageMapperFactoryAsync(_ => new MyDeferredCommandMessageMapperAsync()) + ); + + messageMapperRegistry.RegisterAsync(); + + _messagePump = new Proactor(provider, messageMapperRegistry, + new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), _channel) + { + Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 + }; + + _deadLetterConsumer = new RmqMessageConsumer( + connection: rmqConnection, + queueName: deadLetterQueueName, + routingKey: deadLetterRoutingKey, + isDurable: false, + makeChannels: OnMissingChannel.Assume + ); + } + + //[Fact(Skip = "Breaks due to fault in Task Scheduler running after context has closed")] + [Fact] + public async Task When_retry_limits_force_a_message_onto_the_dlq() + { + //NOTE: This test is **slow** because it needs to ensure infrastructure and then wait whilst we requeue a message a number of times, + //then propagate to the DLQ + + //start a message pump, let it create infrastructure + var task = Task.Factory.StartNew(() => _messagePump.Run(), TaskCreationOptions.LongRunning); + await Task.Delay(500); + + //put something on an SNS topic, which will be delivered to our SQS queue + await _sender.SendAsync(_message); + + //Let the message be handled and deferred until it reaches the DLQ + await Task.Delay(2000); + + //send a quit message to the pump to terminate it + var quitMessage = MessageFactory.CreateQuitMessage(_subscription.RoutingKey); + _channel.Enqueue(quitMessage); + + //wait for the pump to stop once it gets a quit message + await Task.WhenAll(task); + + await Task.Delay(3000); + + //inspect the dlq + var dlqMessage = (await _deadLetterConsumer.ReceiveAsync(new TimeSpan(10000))).First(); + + //assert this is our message + dlqMessage.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + dlqMessage.Body.Value.Should().Be(_message.Body.Value); + + await _deadLetterConsumer.AcknowledgeAsync(dlqMessage); + + } + + public void Dispose() + { + _channel.Dispose(); + } + +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_ttl_causes_a_message_to_expire.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_ttl_causes_a_message_to_expire.cs index ab49c81482..0d82c6343b 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_ttl_causes_a_message_to_expire.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_ttl_causes_a_message_to_expire.cs @@ -30,73 +30,72 @@ THE SOFTWARE. */ using Polly.Caching; using Xunit; -namespace Paramore.Brighter.RMQ.Tests.MessagingGateway +namespace Paramore.Brighter.RMQ.Tests.MessagingGateway; + +[Trait("Category", "RMQ")] +public class RmqMessageProducerTTLTests : IDisposable { - [Trait("Category", "RMQ")] - public class RmqMessageProducerTTLTests : IDisposable + private readonly IAmAMessageProducerSync _messageProducer; + private readonly IAmAMessageConsumerSync _messageConsumer; + private readonly Message _messageOne; + private readonly Message _messageTwo; + + public RmqMessageProducerTTLTests () { - private readonly IAmAMessageProducerSync _messageProducer; - private readonly IAmAMessageConsumer _messageConsumer; - private readonly Message _messageOne; - private readonly Message _messageTwo; + _messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), + new RoutingKey(Guid.NewGuid().ToString()), MessageType.MT_COMMAND), + new MessageBody("test content")); + + _messageTwo = new Message( + new MessageHeader(Guid.NewGuid().ToString(), + new RoutingKey(Guid.NewGuid().ToString()), MessageType.MT_COMMAND), + new MessageBody("test content")); - public RmqMessageProducerTTLTests () + var rmqConnection = new RmqMessagingGatewayConnection { - _messageOne = new Message( - new MessageHeader(Guid.NewGuid().ToString(), - new RoutingKey(Guid.NewGuid().ToString()), MessageType.MT_COMMAND), - new MessageBody("test content")); - - _messageTwo = new Message( - new MessageHeader(Guid.NewGuid().ToString(), - new RoutingKey(Guid.NewGuid().ToString()), MessageType.MT_COMMAND), - new MessageBody("test content")); - - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), - Exchange = new Exchange("paramore.brighter.exchange"), - }; + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange"), + }; - _messageProducer = new RmqMessageProducer(rmqConnection); - - _messageConsumer = new RmqMessageConsumer( - connection: rmqConnection, - queueName: new ChannelName(Guid.NewGuid().ToString()), - routingKey: _messageOne.Header.Topic, - isDurable: false, - highAvailability: false, - ttl: TimeSpan.FromMilliseconds(10000), - makeChannels:OnMissingChannel.Create - ); - - //create the infrastructure - _messageConsumer.Receive(TimeSpan.Zero); + _messageProducer = new RmqMessageProducer(rmqConnection); + + _messageConsumer = new RmqMessageConsumer( + connection: rmqConnection, + queueName: new ChannelName(Guid.NewGuid().ToString()), + routingKey: _messageOne.Header.Topic, + isDurable: false, + highAvailability: false, + ttl: TimeSpan.FromMilliseconds(10000), + makeChannels:OnMissingChannel.Create + ); + + //create the infrastructure + _messageConsumer.Receive(TimeSpan.Zero); - } + } - [Fact] - public async Task When_rejecting_a_message_to_a_dead_letter_queue() - { - _messageProducer.Send(_messageOne); - _messageProducer.Send(_messageTwo); + [Fact] + public async Task When_rejecting_a_message_to_a_dead_letter_queue() + { + _messageProducer.Send(_messageOne); + _messageProducer.Send(_messageTwo); - //check messages are flowing - absence needs to be expiry - var messageOne = _messageConsumer.Receive(TimeSpan.FromMilliseconds(5000)).First(); - messageOne.Id.Should().Be(_messageOne.Id); + //check messages are flowing - absence needs to be expiry + var messageOne = _messageConsumer.Receive(TimeSpan.FromMilliseconds(5000)).First(); + messageOne.Id.Should().Be(_messageOne.Id); - //Let it expire - await Task.Delay(11000); + //Let it expire + await Task.Delay(11000); - var dlqMessage = _messageConsumer.Receive(TimeSpan.FromMilliseconds(10000)).First(); + var dlqMessage = _messageConsumer.Receive(TimeSpan.FromMilliseconds(10000)).First(); - //assert this is our message - dlqMessage.Header.MessageType.Should().Be(MessageType.MT_NONE); - } + //assert this is our message + dlqMessage.Header.MessageType.Should().Be(MessageType.MT_NONE); + } - public void Dispose() - { - _messageProducer.Dispose(); - } + public void Dispose() + { + _messageProducer.Dispose(); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyCommand.cs b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyCommand.cs index 5149bdb37d..d720dd8dff 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyCommand.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyCommand.cs @@ -24,11 +24,10 @@ THE SOFTWARE. */ using System; -namespace Paramore.Brighter.RMQ.Tests.TestDoubles +namespace Paramore.Brighter.RMQ.Tests.TestDoubles; + +internal class MyCommand : Command { - internal class MyCommand : Command - { - public string Value { get; set; } - public MyCommand() :base(Guid.NewGuid()) {} - } + public string Value { get; set; } + public MyCommand() :base(Guid.NewGuid()) {} } diff --git a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommand.cs b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommand.cs index e1366a278b..c78ebe28de 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommand.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommand.cs @@ -1,11 +1,10 @@ using System; -namespace Paramore.Brighter.RMQ.Tests.TestDoubles +namespace Paramore.Brighter.RMQ.Tests.TestDoubles; + +internal class MyDeferredCommand : Command { - internal class MyDeferredCommand : Command - { - public string Value { get; set; } - public MyDeferredCommand() : base(Guid.NewGuid()) { } + public string Value { get; set; } + public MyDeferredCommand() : base(Guid.NewGuid()) { } - } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommandHandler.cs b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommandHandler.cs index 424f2c8bd7..e627c371f1 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommandHandler.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommandHandler.cs @@ -1,14 +1,13 @@ using Paramore.Brighter.Actions; -namespace Paramore.Brighter.RMQ.Tests.TestDoubles +namespace Paramore.Brighter.RMQ.Tests.TestDoubles; + +internal class MyDeferredCommandHandler : RequestHandler { - internal class MyDeferredCommandHandler : RequestHandler + public int HandledCount { get; set; } = 0; + public override MyDeferredCommand Handle(MyDeferredCommand command) { - public int HandledCount { get; set; } = 0; - public override MyDeferredCommand Handle(MyDeferredCommand command) - { - //Just defer for ever - throw new DeferMessageAction(); - } + //Just defer for ever + throw new DeferMessageAction(); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommandHandlerAsync.cs b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommandHandlerAsync.cs new file mode 100644 index 0000000000..8ca518bcfd --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommandHandlerAsync.cs @@ -0,0 +1,16 @@ +using System.Threading; +using System.Threading.Tasks; +using Paramore.Brighter.Actions; + +namespace Paramore.Brighter.RMQ.Tests.TestDoubles; + +internal class MyDeferredCommandHandlerAsync : RequestHandlerAsync +{ + public int HandledCount { get; set; } = 0; + + public override Task HandleAsync(MyDeferredCommand command, CancellationToken cancellationToken = default) + { + // Just defer forever + throw new DeferMessageAction(); + } +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommandMessageMapper.cs b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommandMessageMapper.cs index c619b5e9e9..6539d290e0 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommandMessageMapper.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommandMessageMapper.cs @@ -1,23 +1,22 @@ using System.Text.Json; using Paramore.Brighter.Extensions; -namespace Paramore.Brighter.RMQ.Tests.TestDoubles +namespace Paramore.Brighter.RMQ.Tests.TestDoubles; + +internal class MyDeferredCommandMessageMapper : IAmAMessageMapper { - internal class MyDeferredCommandMessageMapper : IAmAMessageMapper - { - public IRequestContext Context { get; set; } + public IRequestContext Context { get; set; } - public Message MapToMessage(MyDeferredCommand request, Publication publication) - { - var header = new MessageHeader(messageId: request.Id, topic: publication.Topic, messageType: request.RequestToMessageType()); - var body = new MessageBody(System.Text.Json.JsonSerializer.Serialize(request, new JsonSerializerOptions(JsonSerializerDefaults.General))); - var message = new Message(header, body); - return message; - } + public Message MapToMessage(MyDeferredCommand request, Publication publication) + { + var header = new MessageHeader(messageId: request.Id, topic: publication.Topic, messageType: request.RequestToMessageType()); + var body = new MessageBody(System.Text.Json.JsonSerializer.Serialize(request, new JsonSerializerOptions(JsonSerializerDefaults.General))); + var message = new Message(header, body); + return message; + } - public MyDeferredCommand MapToRequest(Message message) - { - return JsonSerializer.Deserialize(message.Body.Value, JsonSerialisationOptions.Options); - } + public MyDeferredCommand MapToRequest(Message message) + { + return JsonSerializer.Deserialize(message.Body.Value, JsonSerialisationOptions.Options); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommandMessageMapperAsync.cs b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommandMessageMapperAsync.cs new file mode 100644 index 0000000000..be617f4771 --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyDeferredCommandMessageMapperAsync.cs @@ -0,0 +1,28 @@ +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Paramore.Brighter.Extensions; + +namespace Paramore.Brighter.RMQ.Tests.TestDoubles; + +internal class MyDeferredCommandMessageMapperAsync : IAmAMessageMapperAsync +{ + public IRequestContext Context { get; set; } + + public async Task MapToMessageAsync(MyDeferredCommand request, Publication publication, CancellationToken cancellationToken = default) + { + if (publication.Topic is null) throw new InvalidOperationException("Missing publication topic"); + + var header = new MessageHeader(messageId: request.Id, topic: publication.Topic, messageType: request.RequestToMessageType()); + var body = new MessageBody(await Task.Run(() => JsonSerializer.Serialize(request, new JsonSerializerOptions(JsonSerializerDefaults.General)))); + var message = new Message(header, body); + return message; + } + + public async Task MapToRequestAsync(Message message, CancellationToken cancellationToken = default) + { + var command = await Task.Run(() => JsonSerializer.Deserialize(message.Body.Value, JsonSerialisationOptions.Options), cancellationToken); + return command ?? new MyDeferredCommand(); + } +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEvent.cs b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEvent.cs index 6bc4103a77..aa2157b15a 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEvent.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEvent.cs @@ -24,45 +24,44 @@ THE SOFTWARE. */ using System; -namespace Paramore.Brighter.RMQ.Tests.TestDoubles +namespace Paramore.Brighter.RMQ.Tests.TestDoubles; + +internal class MyEvent : Event, IEquatable { - internal class MyEvent : Event, IEquatable - { - public int Data { get; private set; } + public int Data { get; private set; } - public MyEvent() : base(Guid.NewGuid()) - { - Data = 7; - } + public MyEvent() : base(Guid.NewGuid()) + { + Data = 7; + } - public bool Equals(MyEvent other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Data == other.Data; - } + public bool Equals(MyEvent other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Data == other.Data; + } - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((MyEvent)obj); - } + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((MyEvent)obj); + } - public override int GetHashCode() - { - return Data; - } + public override int GetHashCode() + { + return Data; + } - public static bool operator ==(MyEvent left, MyEvent right) - { - return Equals(left, right); - } + public static bool operator ==(MyEvent left, MyEvent right) + { + return Equals(left, right); + } - public static bool operator !=(MyEvent left, MyEvent right) - { - return !Equals(left, right); - } + public static bool operator !=(MyEvent left, MyEvent right) + { + return !Equals(left, right); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEventMessageMapper.cs b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEventMessageMapper.cs index fe53d40f8f..7f54cb5cd1 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEventMessageMapper.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEventMessageMapper.cs @@ -25,23 +25,22 @@ THE SOFTWARE. */ using System.Text.Json; using Paramore.Brighter.Extensions; -namespace Paramore.Brighter.RMQ.Tests.TestDoubles +namespace Paramore.Brighter.RMQ.Tests.TestDoubles; + +internal class MyEventMessageMapper : IAmAMessageMapper { - internal class MyEventMessageMapper : IAmAMessageMapper + public IRequestContext Context { get; set; } + + public Message MapToMessage(MyEvent request, Publication publication) + { + var header = new MessageHeader(request.Id, topic:publication.Topic, request.RequestToMessageType()); + var body = new MessageBody(JsonSerializer.Serialize(request, JsonSerialisationOptions.Options)); + var message = new Message(header, body); + return message; + } + + public MyEvent MapToRequest(Message message) { - public IRequestContext Context { get; set; } - - public Message MapToMessage(MyEvent request, Publication publication) - { - var header = new MessageHeader(request.Id, topic:publication.Topic, request.RequestToMessageType()); - var body = new MessageBody(JsonSerializer.Serialize(request, JsonSerialisationOptions.Options)); - var message = new Message(header, body); - return message; - } - - public MyEvent MapToRequest(Message message) - { - return JsonSerializer.Deserialize(message.Body.Value, JsonSerialisationOptions.Options); - } + return JsonSerializer.Deserialize(message.Body.Value, JsonSerialisationOptions.Options); } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEventMessageMapperAsync.cs b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEventMessageMapperAsync.cs new file mode 100644 index 0000000000..cf9541dd7e --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEventMessageMapperAsync.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter.RMQ.Tests.TestDoubles; + +internal class MyEventMessageMapperAsync : IAmAMessageMapperAsync +{ + public IRequestContext Context { get; set; } + + public Task MapToMessageAsync(MyEvent request, Publication publication, CancellationToken cancellationToken = default) + { + var header = new MessageHeader(messageId: request.Id, topic: publication.Topic, messageType: MessageType.MT_EVENT); + var body = new MessageBody(request.ToString()); + var message = new Message(header, body); + return Task.FromResult(message); + } + + public Task MapToRequestAsync(Message message, CancellationToken cancellationToken = default) + { + var myEvent = new MyEvent { Id = message.Id }; + return Task.FromResult(myEvent); + } +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/QuickHandlerFactory.cs b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/QuickHandlerFactory.cs index a003880d86..a9ca3e4ed7 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/QuickHandlerFactory.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/QuickHandlerFactory.cs @@ -1,20 +1,13 @@ using System; -namespace Paramore.Brighter.RMQ.Tests.TestDoubles +namespace Paramore.Brighter.RMQ.Tests.TestDoubles; + +internal class QuickHandlerFactory(Func handlerAction) : IAmAHandlerFactorySync { - internal class QuickHandlerFactory : IAmAHandlerFactorySync + public IHandleRequests Create(Type handlerType) { - private readonly Func _handlerAction; - - public QuickHandlerFactory(Func handlerAction) - { - _handlerAction = handlerAction; - } - public IHandleRequests Create(Type handlerType) - { - return _handlerAction(); - } - - public void Release(IHandleRequests handler) { } + return handlerAction(); } + + public void Release(IHandleRequests handler) { } } diff --git a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/QuickHandlerFactoryAsync.cs b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/QuickHandlerFactoryAsync.cs new file mode 100644 index 0000000000..6d18972646 --- /dev/null +++ b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/QuickHandlerFactoryAsync.cs @@ -0,0 +1,13 @@ +using System; + +namespace Paramore.Brighter.RMQ.Tests.TestDoubles; + +internal class QuickHandlerFactoryAsync(Func handlerAction) : IAmAHandlerFactoryAsync +{ + public IHandleRequestsAsync Create(Type handlerType) + { + return handlerAction(); + } + + public void Release(IHandleRequestsAsync handler) { } +} diff --git a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/TestDoubleRmqMessageConsumer.cs b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/TestDoubleRmqMessageConsumer.cs index ddc3c49ffd..6ed529ac22 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/TestDoubleRmqMessageConsumer.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/TestDoubleRmqMessageConsumer.cs @@ -23,57 +23,58 @@ THE SOFTWARE. */ #endregion using System; +using System.Threading; +using System.Threading.Tasks; using Paramore.Brighter.MessagingGateway.RMQ; using RabbitMQ.Client; +using RabbitMQ.Client.Events; using RabbitMQ.Client.Exceptions; -namespace Paramore.Brighter.RMQ.Tests.TestDoubles +namespace Paramore.Brighter.RMQ.Tests.TestDoubles; +/* + * Use to force a failure mirroring a RabbitMQ subscription failure for testing flow of failure + */ + +internal class BrokerUnreachableRmqMessageConsumer : RmqMessageConsumer { - /* - * Use to force a failure mirroring a RabbitMQ subscription failure for testing flow of failure - */ + public BrokerUnreachableRmqMessageConsumer(RmqMessagingGatewayConnection connection, ChannelName queueName, RoutingKey routingKey, bool isDurable, ushort preFetchSize, bool isHighAvailability) + : base(connection, queueName, routingKey, isDurable, isHighAvailability) { } - internal class BrokerUnreachableRmqMessageConsumer : RmqMessageConsumer + protected override Task EnsureChannelAsync(CancellationToken ct = default) { - public BrokerUnreachableRmqMessageConsumer(RmqMessagingGatewayConnection connection, ChannelName queueName, RoutingKey routingKey, bool isDurable, ushort preFetchSize, bool isHighAvailability) - : base(connection, queueName, routingKey, isDurable, isHighAvailability) { } - - protected override void ConnectToBroker(OnMissingChannel makeExchange = OnMissingChannel.Create) - { - throw new BrokerUnreachableException(new Exception("Force Test Failure")); - } + throw new BrokerUnreachableException(new Exception("Force Test Failure")); } +} - internal class AlreadyClosedRmqMessageConsumer : RmqMessageConsumer - { - public AlreadyClosedRmqMessageConsumer(RmqMessagingGatewayConnection connection, ChannelName queueName, RoutingKey routingKey, bool isDurable, ushort preFetchSize, bool isHighAvailability) - : base(connection, queueName, routingKey, isDurable, isHighAvailability) { } +internal class AlreadyClosedRmqMessageConsumer : RmqMessageConsumer +{ + public AlreadyClosedRmqMessageConsumer(RmqMessagingGatewayConnection connection, ChannelName queueName, RoutingKey routingKey, bool isDurable, ushort preFetchSize, bool isHighAvailability) + : base(connection, queueName, routingKey, isDurable, isHighAvailability) { } - protected override void EnsureChannel() - { - throw new AlreadyClosedException(new ShutdownEventArgs(ShutdownInitiator.Application, 0, "test")); - } + protected override Task EnsureChannelAsync(CancellationToken ct = default) + { + throw new AlreadyClosedException(new ShutdownEventArgs(ShutdownInitiator.Application, 0, "test")); } +} - internal class OperationInterruptedRmqMessageConsumer : RmqMessageConsumer - { - public OperationInterruptedRmqMessageConsumer(RmqMessagingGatewayConnection connection, ChannelName queueName, RoutingKey routingKey, bool isDurable, ushort preFetchSize, bool isHighAvailability) - : base(connection, queueName, routingKey, isDurable,isHighAvailability) { } +internal class OperationInterruptedRmqMessageConsumer : RmqMessageConsumer +{ + public OperationInterruptedRmqMessageConsumer(RmqMessagingGatewayConnection connection, ChannelName queueName, RoutingKey routingKey, bool isDurable, ushort preFetchSize, bool isHighAvailability) + : base(connection, queueName, routingKey, isDurable,isHighAvailability) { } - protected override void EnsureChannel() - { - throw new OperationInterruptedException(new ShutdownEventArgs(ShutdownInitiator.Application, 0, "test")); - } + protected override Task EnsureChannelAsync(CancellationToken ct = default) + { + throw new OperationInterruptedException(new ShutdownEventArgs(ShutdownInitiator.Application, 0, "test")); } +} - internal class NotSupportedRmqMessageConsumer : RmqMessageConsumer - { - public NotSupportedRmqMessageConsumer(RmqMessagingGatewayConnection connection, ChannelName queueName, RoutingKey routingKey, bool isDurable, ushort preFetchSize, bool isHighAvailability) - : base(connection, queueName, routingKey, isDurable, isHighAvailability) { } +internal class NotSupportedRmqMessageConsumer : RmqMessageConsumer +{ + public NotSupportedRmqMessageConsumer(RmqMessagingGatewayConnection connection, ChannelName queueName, RoutingKey routingKey, bool isDurable, ushort preFetchSize, bool isHighAvailability) + : base(connection, queueName, routingKey, isDurable, isHighAvailability) { } - protected override void EnsureChannel() - { - throw new NotSupportedException(); - } + protected override Task EnsureChannelAsync(CancellationToken ct = default) + { + throw new NotSupportedException(); } } diff --git a/tests/Paramore.Brighter.Redis.Tests/Catch.cs b/tests/Paramore.Brighter.Redis.Tests/Catch.cs index 654ab65d88..450b6fbbb8 100644 --- a/tests/Paramore.Brighter.Redis.Tests/Catch.cs +++ b/tests/Paramore.Brighter.Redis.Tests/Catch.cs @@ -32,15 +32,15 @@ namespace Paramore.Brighter.Redis.Tests [DebuggerStepThrough] public static class Catch { - public static Exception Exception(Action action) + public static Exception? Exception(Action action) { - Exception exception = null; + Exception? exception = null; try { action(); } - catch (Exception e) + catch (Exception? e) { exception = e; } diff --git a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/RedisFixture.cs b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/RedisFixture.cs index 6f1bdfc632..3237230f70 100644 --- a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/RedisFixture.cs +++ b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/RedisFixture.cs @@ -1,12 +1,13 @@ using System; +using System.Threading.Tasks; using Paramore.Brighter.MessagingGateway.Redis; namespace Paramore.Brighter.Redis.Tests.MessagingGateway { - public class RedisFixture : IDisposable + public class RedisFixture : IAsyncDisposable, IDisposable { - private const string QueueName = "test"; - protected const string Topic = "test"; + private ChannelName _queueName = new ChannelName("test"); + private readonly RoutingKey _topic = new RoutingKey("test"); public readonly RedisMessageProducer MessageProducer; public readonly RedisMessageConsumer MessageConsumer; @@ -14,8 +15,8 @@ public RedisFixture() { RedisMessagingGatewayConfiguration configuration = RedisMessagingGatewayConfiguration(); - MessageProducer = new RedisMessageProducer(configuration, new RedisMessagePublication {Topic = new RoutingKey(Topic)}); - MessageConsumer = new RedisMessageConsumer(configuration, QueueName, Topic); + MessageProducer = new RedisMessageProducer(configuration, new RedisMessagePublication {Topic = _topic}); + MessageConsumer = new RedisMessageConsumer(configuration, _queueName, _topic); } public static RedisMessagingGatewayConfiguration RedisMessagingGatewayConfiguration() @@ -36,5 +37,12 @@ public void Dispose() MessageConsumer.Dispose(); MessageProducer.Dispose(); } + + public async ValueTask DisposeAsync() + { + await MessageConsumer.PurgeAsync(); + await MessageConsumer.DisposeAsync(); + await MessageProducer.DisposeAsync(); + } } } diff --git a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_a_message_consumer_throws_a_socket_exception_when_connecting_to_the_server.cs b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_a_message_consumer_throws_a_socket_exception_when_connecting_to_the_server.cs index 61e8b6fc6c..0084404f69 100644 --- a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_a_message_consumer_throws_a_socket_exception_when_connecting_to_the_server.cs +++ b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_a_message_consumer_throws_a_socket_exception_when_connecting_to_the_server.cs @@ -5,42 +5,38 @@ using ServiceStack.Redis; using Xunit; -namespace Paramore.Brighter.Redis.Tests.MessagingGateway +namespace Paramore.Brighter.Redis.Tests.MessagingGateway; + +[Collection("Redis Shared Pool")] //shared connection pool so run sequentially +[Trait("Category", "Redis")] +public class RedisMessageConsumerRedisNotAvailableTests : IDisposable { - [Collection("Redis Shared Pool")] //shared connection pool so run sequentially - [Trait("Category", "Redis")] - public class RedisMessageConsumerRedisNotAvailableTests : IDisposable - { - private const string QueueName = "test"; - private const string Topic = "test"; - private readonly RedisMessageConsumer _messageConsumer; - private Exception _exception; + private readonly ChannelName _queueName = new ChannelName("test"); + private readonly RoutingKey _topic = new RoutingKey("test"); + private readonly RedisMessageConsumer _messageConsumer; + private Exception? _exception; - public RedisMessageConsumerRedisNotAvailableTests() - { - var configuration = RedisFixture.RedisMessagingGatewayConfiguration(); + public RedisMessageConsumerRedisNotAvailableTests() + { + var configuration = RedisFixture.RedisMessagingGatewayConfiguration(); - _messageConsumer = new RedisMessageConsumerSocketErrorOnGetClient(configuration, QueueName, Topic); + _messageConsumer = new RedisMessageConsumerSocketErrorOnGetClient(configuration, _queueName, _topic); - } + } - [Fact] - public void When_a_message_consumer_throws_a_socket_exception_when_connecting_to_the_server() - { - _exception = Catch.Exception(() => _messageConsumer.Receive(TimeSpan.FromMilliseconds(1000))); - - //_should_return_a_channel_failure_exception - _exception.Should().BeOfType(); + [Fact] + public void When_a_message_consumer_throws_a_socket_exception_when_connecting_to_the_server() + { + _exception = Catch.Exception(() => _messageConsumer.Receive(TimeSpan.FromMilliseconds(1000))); - //_should_return_an_explaining_inner_exception - _exception.InnerException.Should().BeOfType(); + _exception.Should().BeOfType(); + _exception?.InnerException.Should().BeOfType(); - } + } - public void Dispose() - { - _messageConsumer.Purge(); - _messageConsumer.Dispose(); - } + public void Dispose() + { + _messageConsumer.Purge(); + _messageConsumer.Dispose(); } } diff --git a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_a_message_consumer_throws_a_socket_exception_when_connecting_to_the_server_async.cs b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_a_message_consumer_throws_a_socket_exception_when_connecting_to_the_server_async.cs new file mode 100644 index 0000000000..72aff87747 --- /dev/null +++ b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_a_message_consumer_throws_a_socket_exception_when_connecting_to_the_server_async.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.Redis; +using Paramore.Brighter.Redis.Tests.TestDoubles; +using ServiceStack.Redis; +using Xunit; + +namespace Paramore.Brighter.Redis.Tests.MessagingGateway; + +[Collection("Redis Shared Pool")] //shared connection pool so run sequentially +[Trait("Category", "Redis")] +public class RedisMessageConsumerRedisNotAvailableTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly ChannelName _queueName = new ChannelName("test"); + private readonly RoutingKey _topic = new RoutingKey("test"); + private readonly RedisMessageConsumer _messageConsumer; + private Exception? _exception; + + public RedisMessageConsumerRedisNotAvailableTestsAsync() + { + var configuration = RedisFixture.RedisMessagingGatewayConfiguration(); + + _messageConsumer = new RedisMessageConsumerSocketErrorOnGetClient(configuration, _queueName, _topic); + } + + [Fact] + public async Task When_a_message_consumer_throws_a_socket_exception_when_connecting_to_the_server_async() + { + _exception = await Catch.ExceptionAsync(() => _messageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))); + + _exception.Should().BeOfType(); + _exception?.InnerException.Should().BeOfType(); + } + + public async ValueTask DisposeAsync() + { + await _messageConsumer.PurgeAsync(); + await _messageConsumer.DisposeAsync(); + } + + public void Dispose() + { + _messageConsumer.Purge(); + _messageConsumer.Dispose(); + } +} diff --git a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_a_message_consumer_throws_a_timeout_exception_when_connecting_to_the_pool.cs b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_a_message_consumer_throws_a_timeout_exception_when_connecting_to_the_pool.cs index eca442cc86..cc18e2a26f 100644 --- a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_a_message_consumer_throws_a_timeout_exception_when_connecting_to_the_pool.cs +++ b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_a_message_consumer_throws_a_timeout_exception_when_connecting_to_the_pool.cs @@ -4,41 +4,37 @@ using Paramore.Brighter.Redis.Tests.TestDoubles; using Xunit; -namespace Paramore.Brighter.Redis.Tests.MessagingGateway +namespace Paramore.Brighter.Redis.Tests.MessagingGateway; + +[Collection("Redis Shared Pool")] //shared connection pool so run sequentially +[Trait("Category", "Redis")] +public class RedisMessageConsumerOperationInterruptedTests : IDisposable { - [Collection("Redis Shared Pool")] //shared connection pool so run sequentially - [Trait("Category", "Redis")] - public class RedisMessageConsumerOperationInterruptedTests : IDisposable - { - private const string QueueName = "test"; - private const string Topic = "test"; - private readonly RedisMessageConsumer _messageConsumer; - private Exception _exception; + private readonly ChannelName _queueName = new("test"); + private readonly RoutingKey _topic = new("test"); + private readonly RedisMessageConsumer _messageConsumer; + private Exception? _exception; - public RedisMessageConsumerOperationInterruptedTests() - { - var configuration = RedisFixture.RedisMessagingGatewayConfiguration(); + public RedisMessageConsumerOperationInterruptedTests() + { + var configuration = RedisFixture.RedisMessagingGatewayConfiguration(); - _messageConsumer = new RedisMessageConsumerTimeoutOnGetClient(configuration, QueueName, Topic); - } + _messageConsumer = new RedisMessageConsumerTimeoutOnGetClient(configuration, _queueName, _topic); + } - [Fact] - public void When_a_message_consumer_throws_a_timeout_exception_when_getting_a_client_from_the_pool() - { - _exception = Catch.Exception(() => _messageConsumer.Receive(TimeSpan.FromMilliseconds(1000))); - - //_should_return_a_channel_failure_exception - _exception.Should().BeOfType(); + [Fact] + public void When_a_message_consumer_throws_a_timeout_exception_when_getting_a_client_from_the_pool() + { + _exception = Catch.Exception(() => _messageConsumer.Receive(TimeSpan.FromMilliseconds(1000))); - //_should_return_an_explaining_inner_exception - _exception.InnerException.Should().BeOfType(); + _exception.Should().BeOfType(); + _exception?.InnerException.Should().BeOfType(); - } + } - public void Dispose() - { - _messageConsumer.Purge(); - _messageConsumer.Dispose(); - } + public void Dispose() + { + _messageConsumer.Purge(); + _messageConsumer.Dispose(); } } diff --git a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_a_message_consumer_throws_a_timeout_exception_when_connecting_to_the_pool_async.cs b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_a_message_consumer_throws_a_timeout_exception_when_connecting_to_the_pool_async.cs new file mode 100644 index 0000000000..e80248e469 --- /dev/null +++ b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_a_message_consumer_throws_a_timeout_exception_when_connecting_to_the_pool_async.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.MessagingGateway.Redis; +using Paramore.Brighter.Redis.Tests.TestDoubles; +using Xunit; + +namespace Paramore.Brighter.Redis.Tests.MessagingGateway; + +[Collection("Redis Shared Pool")] //shared connection pool so run sequentially +[Trait("Category", "Redis")] +public class RedisMessageConsumerOperationInterruptedTestsAsync : IAsyncDisposable +{ + private readonly ChannelName _queueName = new("test"); + private readonly RoutingKey _topic = new("test"); + private readonly RedisMessageConsumer _messageConsumer; + private Exception? _exception; + + public RedisMessageConsumerOperationInterruptedTestsAsync() + { + var configuration = RedisFixture.RedisMessagingGatewayConfiguration(); + + _messageConsumer = new RedisMessageConsumerTimeoutOnGetClient(configuration, _queueName, _topic); + } + + [Fact] + public async Task When_a_message_consumer_throws_a_timeout_exception_when_getting_a_client_from_the_pool_async() + { + _exception = await Catch.ExceptionAsync(() => _messageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))); + + _exception.Should().BeOfType(); + _exception?.InnerException.Should().BeOfType(); + } + + public async ValueTask DisposeAsync() + { + await _messageConsumer.PurgeAsync(); + await _messageConsumer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_overriding_client_configuration_via_the_gateway.cs b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_overriding_client_configuration_via_the_gateway.cs index fd6b5a39ad..92213e5157 100644 --- a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_overriding_client_configuration_via_the_gateway.cs +++ b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_overriding_client_configuration_via_the_gateway.cs @@ -1,75 +1,49 @@ using System; using FluentAssertions; using Paramore.Brighter.MessagingGateway.Redis; +using Paramore.Brighter.Redis.Tests.TestDoubles; using ServiceStack.Redis; using Xunit; -namespace Paramore.Brighter.Redis.Tests.MessagingGateway -{ - [Collection("Redis Shared Pool")] //shared connection pool so run sequentially - [Trait("Category", "Redis")] - public class RedisGatewayConfigurationTests - { - [Fact] - public void When_overriding_client_configuration_via_the_gateway() - { - var configuration = new RedisMessagingGatewayConfiguration - { - BackoffMultiplier = 5, - BufferPoolMaxSize = 1024, - DeactivatedClientsExpiry = TimeSpan.Zero, - DefaultConnectTimeout = 10, - DefaultIdleTimeOutSecs = 360, - DefaultReceiveTimeout = 30, - DefaultRetryTimeout = 10, - DefaultSendTimeout = 10, - DisableVerboseLogging = false, - HostLookupTimeoutMs = 400, - MaxPoolSize = 50, - MessageTimeToLive = TimeSpan.FromMinutes(30), - VerifyMasterConnections = false - }; - - using var gateway = new TestRedisGateway(configuration); - //Redis Config is static, so we can just look at the values we should have initialized - RedisConfig.BackOffMultiplier.Should().Be(configuration.BackoffMultiplier.Value); - RedisConfig.BackOffMultiplier.Should().Be(configuration.BackoffMultiplier.Value); - RedisConfig.DeactivatedClientsExpiry.Should().Be(configuration.DeactivatedClientsExpiry.Value); - RedisConfig.DefaultConnectTimeout.Should().Be(configuration.DefaultConnectTimeout.Value); - RedisConfig.DefaultIdleTimeOutSecs.Should().Be(configuration.DefaultIdleTimeOutSecs.Value); - RedisConfig.DefaultReceiveTimeout.Should().Be(configuration.DefaultReceiveTimeout.Value); - RedisConfig.DefaultSendTimeout.Should().Be(configuration.DefaultSendTimeout.Value); - RedisConfig.EnableVerboseLogging.Should().Be(!configuration.DisableVerboseLogging.Value); - RedisConfig.HostLookupTimeoutMs.Should().Be(configuration.HostLookupTimeoutMs.Value); - RedisConfig.DefaultMaxPoolSize.Should().Be(configuration.MaxPoolSize.Value); - gateway.MessageTimeToLive.Should().Be(configuration.MessageTimeToLive.Value); - RedisConfig.VerifyMasterConnections.Should().Be(configuration.VerifyMasterConnections.Value); - } - +namespace Paramore.Brighter.Redis.Tests.MessagingGateway; - } - - /// - /// There are some properties we want to test, use a test wrapper to expose them, instead of leaking from - /// run-time classes - /// - public class TestRedisGateway : RedisMessageGateway, IDisposable +[Collection("Redis Shared Pool")] //shared connection pool so run sequentially +[Trait("Category", "Redis")] +public class RedisGatewayConfigurationTests +{ + [Fact] + public void When_overriding_client_configuration_via_the_gateway() { - public TestRedisGateway(RedisMessagingGatewayConfiguration redisMessagingGatewayConfiguration) - : base(redisMessagingGatewayConfiguration) + var configuration = new RedisMessagingGatewayConfiguration { - OverrideRedisClientDefaults(); - } - + BackoffMultiplier = 5, + BufferPoolMaxSize = 1024, + DeactivatedClientsExpiry = TimeSpan.Zero, + DefaultConnectTimeout = 10, + DefaultIdleTimeOutSecs = 360, + DefaultReceiveTimeout = 30, + DefaultRetryTimeout = 10, + DefaultSendTimeout = 10, + DisableVerboseLogging = false, + HostLookupTimeoutMs = 400, + MaxPoolSize = 50, + MessageTimeToLive = TimeSpan.FromMinutes(30), + VerifyMasterConnections = false + }; - public new TimeSpan MessageTimeToLive => base.MessageTimeToLive; - - public void Dispose() - { - DisposePool(); - Pool = null; - RedisConfig.Reset(); - GC.SuppressFinalize(this); - } + using var gateway = new TestRedisGateway(configuration, RoutingKey.Empty); + //Redis Config is static, so we can just look at the values we should have initialized + RedisConfig.BackOffMultiplier.Should().Be(configuration.BackoffMultiplier.Value); + RedisConfig.BackOffMultiplier.Should().Be(configuration.BackoffMultiplier.Value); + RedisConfig.DeactivatedClientsExpiry.Should().Be(configuration.DeactivatedClientsExpiry.Value); + RedisConfig.DefaultConnectTimeout.Should().Be(configuration.DefaultConnectTimeout.Value); + RedisConfig.DefaultIdleTimeOutSecs.Should().Be(configuration.DefaultIdleTimeOutSecs.Value); + RedisConfig.DefaultReceiveTimeout.Should().Be(configuration.DefaultReceiveTimeout.Value); + RedisConfig.DefaultSendTimeout.Should().Be(configuration.DefaultSendTimeout.Value); + RedisConfig.EnableVerboseLogging.Should().Be(!configuration.DisableVerboseLogging.Value); + RedisConfig.HostLookupTimeoutMs.Should().Be(configuration.HostLookupTimeoutMs.Value); + RedisConfig.DefaultMaxPoolSize.Should().Be(configuration.MaxPoolSize.Value); + gateway.MessageTimeToLive.Should().Be(configuration.MessageTimeToLive.Value); + RedisConfig.VerifyMasterConnections.Should().Be(configuration.VerifyMasterConnections.Value); } } diff --git a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_parsing_a_good_redis_message_to_brighter.cs b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_parsing_a_good_redis_message_to_brighter.cs index c79eace64b..031b6311d6 100644 --- a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_parsing_a_good_redis_message_to_brighter.cs +++ b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_parsing_a_good_redis_message_to_brighter.cs @@ -3,31 +3,30 @@ using Paramore.Brighter.MessagingGateway.Redis; using Xunit; -namespace Paramore.Brighter.Redis.Tests.MessagingGateway +namespace Paramore.Brighter.Redis.Tests.MessagingGateway; + +[Collection("Redis Shared Pool")] //shared connection pool so run sequentially +[Trait("Category", "Redis")] +public class RedisGoodMessageParsingTests { - [Collection("Redis Shared Pool")] //shared connection pool so run sequentially - [Trait("Category", "Redis")] - public class RedisGoodMessageParsingTests - { - private const string GoodMessage = - "\n"; + private const string GoodMessage = + "\n"; - [Fact] - public void When_parsing_a_good_redis_message_to_brighter() - { - var redisMessageCreator = new RedisMessageCreator(); + [Fact] + public void When_parsing_a_good_redis_message_to_brighter() + { + var redisMessageCreator = new RedisMessageCreator(); - Message message = redisMessageCreator.CreateMessage(GoodMessage); + Message message = redisMessageCreator.CreateMessage(GoodMessage); - message.Id.Should().Be("18669550-2069-48c5-923d-74a2e79c0748"); - message.Header.TimeStamp.Should().Be(DateTime.Parse("2018-02-07T09:38:36Z")); - message.Header.Topic.Value.Should().Be("test"); - message.Header.MessageType.Should().Be(MessageType.MT_COMMAND); - message.Header.HandledCount.Should().Be(3); - message.Header.Delayed.Should().Be(TimeSpan.FromMilliseconds(200)); - message.Header.CorrelationId.Should().Be("0AF88BBC-07FD-4FC3-9CA7-BF68415A2535"); - message.Header.ContentType.Should().Be("text/plain"); - message.Header.ReplyTo.Should().Be("reply.queue"); - } + message.Id.Should().Be("18669550-2069-48c5-923d-74a2e79c0748"); + message.Header.TimeStamp.Should().Be(DateTime.Parse("2018-02-07T09:38:36Z")); + message.Header.Topic.Value.Should().Be("test"); + message.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + message.Header.HandledCount.Should().Be(3); + message.Header.Delayed.Should().Be(TimeSpan.FromMilliseconds(200)); + message.Header.CorrelationId.Should().Be("0AF88BBC-07FD-4FC3-9CA7-BF68415A2535"); + message.Header.ContentType.Should().Be("text/plain"); + message.Header.ReplyTo.Should().Be("reply.queue"); } } diff --git a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway.cs b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway.cs index 0f9dd16afe..2b25eff89b 100644 --- a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway.cs +++ b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway.cs @@ -3,37 +3,35 @@ using FluentAssertions; using Xunit; -namespace Paramore.Brighter.Redis.Tests.MessagingGateway +namespace Paramore.Brighter.Redis.Tests.MessagingGateway; + +[Collection("Redis Shared Pool")] //shared connection pool so run sequentially +[Trait("Category", "Redis")] +public class RedisMessageProducerSendTests : IClassFixture { - [Collection("Redis Shared Pool")] //shared connection pool so run sequentially - [Trait("Category", "Redis")] - public class RedisMessageProducerSendTests : IClassFixture - { - private readonly RedisFixture _redisFixture; - private readonly Message _message; + private readonly RedisFixture _redisFixture; + private readonly Message _message; - public RedisMessageProducerSendTests(RedisFixture redisFixture) - { - const string topic = "test"; - _redisFixture = redisFixture; - _message = new Message( - new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(topic), MessageType.MT_COMMAND), - new MessageBody("test content") - ); - } + public RedisMessageProducerSendTests(RedisFixture redisFixture) + { + const string topic = "test"; + _redisFixture = redisFixture; + _message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(topic), MessageType.MT_COMMAND), + new MessageBody("test content") + ); + } - [Fact] - public void When_posting_a_message_via_the_messaging_gateway() - { - _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)); //Need to receive to subscribe to feed, before we send a message. This returns an empty message we discard - _redisFixture.MessageProducer.Send(_message); - var sentMessage = _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); - var messageBody = sentMessage.Body.Value; - _redisFixture.MessageConsumer.Acknowledge(sentMessage); + [Fact] + public void When_posting_a_message_via_the_messaging_gateway() + { + _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)); //Need to receive to subscribe to feed, before we send a message. This returns an empty message we discard + _redisFixture.MessageProducer.Send(_message); + var sentMessage = _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); + var messageBody = sentMessage.Body.Value; + _redisFixture.MessageConsumer.Acknowledge(sentMessage); - //_should_send_a_message_via_restms_with_the_matching_body - messageBody.Should().Be(_message.Body.Value); - } + messageBody.Should().Be(_message.Body.Value); } } diff --git a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway_async.cs b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway_async.cs new file mode 100644 index 0000000000..21d963d7df --- /dev/null +++ b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway_async.cs @@ -0,0 +1,37 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace Paramore.Brighter.Redis.Tests.MessagingGateway; + +[Collection("Redis Shared Pool")] //shared connection pool so run sequentially +[Trait("Category", "Redis")] +public class RedisMessageProducerSendTestsAsync : IClassFixture +{ + private readonly RedisFixture _redisFixture; + private readonly Message _message; + + public RedisMessageProducerSendTestsAsync(RedisFixture redisFixture) + { + const string topic = "test"; + _redisFixture = redisFixture; + _message = new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(topic), MessageType.MT_COMMAND), + new MessageBody("test content") + ); + } + + [Fact] + public async Task When_posting_a_message_via_the_messaging_gateway_async() + { + await _redisFixture.MessageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000)); //Need to receive to subscribe to feed, before we send a message. This returns an empty message we discard + await _redisFixture.MessageProducer.SendAsync(_message); + var sentMessage = (await _redisFixture.MessageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))).Single(); + var messageBody = sentMessage.Body.Value; + await _redisFixture.MessageConsumer.AcknowledgeAsync(sentMessage); + + messageBody.Should().Be(_message.Body.Value); + } +} diff --git a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_posting_multiple_messages_via_the_messaging_gateway.cs b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_posting_multiple_messages_via_the_messaging_gateway.cs index 7c0be327c4..296a52474a 100644 --- a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_posting_multiple_messages_via_the_messaging_gateway.cs +++ b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_posting_multiple_messages_via_the_messaging_gateway.cs @@ -3,56 +3,55 @@ using FluentAssertions; using Xunit; -namespace Paramore.Brighter.Redis.Tests.MessagingGateway +namespace Paramore.Brighter.Redis.Tests.MessagingGateway; + +[Collection("Redis Shared Pool")] //shared connection pool so run sequentially +[Trait("Category", "Redis")] +public class RedisMessageProducerMultipleSendTests : IClassFixture { - [Collection("Redis Shared Pool")] //shared connection pool so run sequentially - [Trait("Category", "Redis")] - public class RedisMessageProducerMultipleSendTests : IClassFixture - { - private readonly RedisFixture _redisFixture; - private readonly Message _messageOne; - private readonly Message _messageTwo; + private readonly RedisFixture _redisFixture; + private readonly Message _messageOne; + private readonly Message _messageTwo; - public RedisMessageProducerMultipleSendTests(RedisFixture redisFixture) - { - const string topic = "test"; - _redisFixture = redisFixture; - var routingKey = new RoutingKey(topic); + public RedisMessageProducerMultipleSendTests(RedisFixture redisFixture) + { + const string topic = "test"; + _redisFixture = redisFixture; + var routingKey = new RoutingKey(topic); - _messageOne = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), - new MessageBody("test content") - ); + _messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), + new MessageBody("test content") + ); - _messageTwo = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), - new MessageBody("more test content") - ); - } + _messageTwo = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), + new MessageBody("more test content") + ); + } - [Fact] - public void When_posting_a_message_via_the_messaging_gateway() - { - //Need to receive to subscribe to feed, before we send a message. This returns an empty message we discard - _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)); + [Fact] + public void When_posting_a_message_via_the_messaging_gateway() + { + //Need to receive to subscribe to feed, before we send a message. This returns an empty message we discard + _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)); - //Send a sequence of messages, we want to check that ordering is preserved - _redisFixture.MessageProducer.Send(_messageOne); - _redisFixture.MessageProducer.Send(_messageTwo); + //Send a sequence of messages, we want to check that ordering is preserved + _redisFixture.MessageProducer.Send(_messageOne); + _redisFixture.MessageProducer.Send(_messageTwo); - //Now receive, and confirm order off is order on - var sentMessageOne = _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); - var messageBodyOne = sentMessageOne.Body.Value; - _redisFixture.MessageConsumer.Acknowledge(sentMessageOne); + //Now receive, and confirm order off is order on + var sentMessageOne = _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); + var messageBodyOne = sentMessageOne.Body.Value; + _redisFixture.MessageConsumer.Acknowledge(sentMessageOne); - var sentMessageTwo = _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); - var messageBodyTwo = sentMessageTwo.Body.Value; - _redisFixture.MessageConsumer.Acknowledge(sentMessageTwo); + var sentMessageTwo = _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); + var messageBodyTwo = sentMessageTwo.Body.Value; + _redisFixture.MessageConsumer.Acknowledge(sentMessageTwo); - //_should_send_a_message_via_restms_with_the_matching_body - messageBodyOne.Should().Be(_messageOne.Body.Value); - messageBodyTwo.Should().Be(_messageTwo.Body.Value); - } + //_should_send_a_message_via_restms_with_the_matching_body + messageBodyOne.Should().Be(_messageOne.Body.Value); + messageBodyTwo.Should().Be(_messageTwo.Body.Value); } } diff --git a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_posting_multiple_messages_via_the_messaging_gateway_async.cs b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_posting_multiple_messages_via_the_messaging_gateway_async.cs new file mode 100644 index 0000000000..84d23865b7 --- /dev/null +++ b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_posting_multiple_messages_via_the_messaging_gateway_async.cs @@ -0,0 +1,57 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace Paramore.Brighter.Redis.Tests.MessagingGateway; + +[Collection("Redis Shared Pool")] //shared connection pool so run sequentially +[Trait("Category", "Redis")] +public class RedisMessageProducerMultipleSendTestsAsync : IClassFixture +{ + private readonly RedisFixture _redisFixture; + private readonly Message _messageOne; + private readonly Message _messageTwo; + + public RedisMessageProducerMultipleSendTestsAsync(RedisFixture redisFixture) + { + const string topic = "test"; + _redisFixture = redisFixture; + var routingKey = new RoutingKey(topic); + + _messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), + new MessageBody("test content") + ); + + _messageTwo = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), + new MessageBody("more test content") + ); + } + + [Fact] + public async Task When_posting_multiple_messages_via_the_messaging_gateway_async() + { + // Need to receive to subscribe to feed, before we send a message. This returns an empty message we discard + await _redisFixture.MessageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000)); + + // Send a sequence of messages, we want to check that ordering is preserved + await _redisFixture.MessageProducer.SendAsync(_messageOne); + await _redisFixture.MessageProducer.SendAsync(_messageTwo); + + // Now receive, and confirm order off is order on + var sentMessageOne = (await _redisFixture.MessageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))).Single(); + var messageBodyOne = sentMessageOne.Body.Value; + await _redisFixture.MessageConsumer.AcknowledgeAsync(sentMessageOne); + + var sentMessageTwo = (await _redisFixture.MessageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))).Single(); + var messageBodyTwo = sentMessageTwo.Body.Value; + await _redisFixture.MessageConsumer.AcknowledgeAsync(sentMessageTwo); + + // _should_send_a_message_via_restms_with_the_matching_body + messageBodyOne.Should().Be(_messageOne.Body.Value); + messageBodyTwo.Should().Be(_messageTwo.Body.Value); + } +} diff --git a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_requeing_a_failed_message.cs b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_requeing_a_failed_message.cs index 14cfd14ff2..a505c175cc 100644 --- a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_requeing_a_failed_message.cs +++ b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_requeing_a_failed_message.cs @@ -3,64 +3,63 @@ using FluentAssertions; using Xunit; -namespace Paramore.Brighter.Redis.Tests.MessagingGateway +namespace Paramore.Brighter.Redis.Tests.MessagingGateway; + +[Collection("Redis Shared Pool")] //shared connection pool so run sequentially +[Trait("Category", "Redis")] +[Trait("Fragile", "CI")] +public class RedisRequeueMessageTests : IClassFixture { - [Collection("Redis Shared Pool")] //shared connection pool so run sequentially - [Trait("Category", "Redis")] - [Trait("Fragile", "CI")] - public class RedisRequeueMessageTests : IClassFixture - { - private readonly RedisFixture _redisFixture; - private readonly Message _messageOne; - private readonly Message _messageTwo; + private readonly RedisFixture _redisFixture; + private readonly Message _messageOne; + private readonly Message _messageTwo; - public RedisRequeueMessageTests(RedisFixture redisFixture) - { - const string topic = "test"; - _redisFixture = redisFixture; - var routingKey = new RoutingKey(topic); + public RedisRequeueMessageTests(RedisFixture redisFixture) + { + const string topic = "test"; + _redisFixture = redisFixture; + var routingKey = new RoutingKey(topic); - _messageOne = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), - new MessageBody("test content") - ); + _messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), + new MessageBody("test content") + ); - _messageTwo = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), - new MessageBody("more test content") - ); - } + _messageTwo = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), + new MessageBody("more test content") + ); + } - [Fact] - public void When_requeing_a_failed_message() - { - //Need to receive to subscribe to feed, before we send a message. This returns an empty message we discard - _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)); + [Fact] + public void When_requeing_a_failed_message() + { + //Need to receive to subscribe to feed, before we send a message. This returns an empty message we discard + _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)); - //Send a sequence of messages, we want to check that ordering is preserved - _redisFixture.MessageProducer.Send(_messageOne); - _redisFixture.MessageProducer.Send(_messageTwo); + //Send a sequence of messages, we want to check that ordering is preserved + _redisFixture.MessageProducer.Send(_messageOne); + _redisFixture.MessageProducer.Send(_messageTwo); - //Now receive, the first message - var sentMessageOne = _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); + //Now receive, the first message + var sentMessageOne = _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); - //now requeue the first message - _redisFixture.MessageConsumer.Requeue(_messageOne, TimeSpan.FromMilliseconds(300)); + //now requeue the first message + _redisFixture.MessageConsumer.Requeue(_messageOne, TimeSpan.FromMilliseconds(300)); - //try receiving again; messageTwo should come first - var sentMessageTwo = _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); - var messageBodyTwo = sentMessageTwo.Body.Value; - _redisFixture.MessageConsumer.Acknowledge(sentMessageTwo); + //try receiving again; messageTwo should come first + var sentMessageTwo = _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); + var messageBodyTwo = sentMessageTwo.Body.Value; + _redisFixture.MessageConsumer.Acknowledge(sentMessageTwo); - sentMessageOne = _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); - var messageBodyOne = sentMessageOne.Body.Value; - _redisFixture.MessageConsumer.Acknowledge(sentMessageOne); + sentMessageOne = _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); + var messageBodyOne = sentMessageOne.Body.Value; + _redisFixture.MessageConsumer.Acknowledge(sentMessageOne); - //_should_send_a_message_via_restms_with_the_matching_body - messageBodyOne.Should().Be(_messageOne.Body.Value); - messageBodyTwo.Should().Be(_messageTwo.Body.Value); - } + //_should_send_a_message_via_restms_with_the_matching_body + messageBodyOne.Should().Be(_messageOne.Body.Value); + messageBodyTwo.Should().Be(_messageTwo.Body.Value); } } diff --git a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_requeing_a_failed_message_async.cs b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_requeing_a_failed_message_async.cs new file mode 100644 index 0000000000..d9f7bd203d --- /dev/null +++ b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_requeing_a_failed_message_async.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace Paramore.Brighter.Redis.Tests.MessagingGateway; + +[Collection("Redis Shared Pool")] //shared connection pool so run sequentially +[Trait("Category", "Redis")] +[Trait("Fragile", "CI")] +public class RedisRequeueMessageTestsAsync : IClassFixture, IAsyncDisposable +{ + private readonly RedisFixture _redisFixture; + private readonly Message _messageOne; + private readonly Message _messageTwo; + + public RedisRequeueMessageTestsAsync(RedisFixture redisFixture) + { + const string topic = "test"; + _redisFixture = redisFixture; + var routingKey = new RoutingKey(topic); + + _messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), + new MessageBody("test content") + ); + + _messageTwo = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND), + new MessageBody("more test content") + ); + } + + [Fact] + public async Task When_requeing_a_failed_message_async() + { + // Need to receive to subscribe to feed, before we send a message. This returns an empty message we discard + await _redisFixture.MessageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000)); + + // Send a sequence of messages, we want to check that ordering is preserved + await _redisFixture.MessageProducer.SendAsync(_messageOne); + await _redisFixture.MessageProducer.SendAsync(_messageTwo); + + // Now receive, the first message + var sentMessageOne = (await _redisFixture.MessageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))).Single(); + + // Now requeue the first message + await _redisFixture.MessageConsumer.RequeueAsync(_messageOne, TimeSpan.FromMilliseconds(300)); + + // Try receiving again; messageTwo should come first + var sentMessageTwo = (await _redisFixture.MessageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))).Single(); + var messageBodyTwo = sentMessageTwo.Body.Value; + await _redisFixture.MessageConsumer.AcknowledgeAsync(sentMessageTwo); + + sentMessageOne = (await _redisFixture.MessageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))).Single(); + var messageBodyOne = sentMessageOne.Body.Value; + await _redisFixture.MessageConsumer.AcknowledgeAsync(sentMessageOne); + + // _should_send_a_message_via_restms_with_the_matching_body + messageBodyOne.Should().Be(_messageOne.Body.Value); + messageBodyTwo.Should().Be(_messageTwo.Body.Value); + } + + public async ValueTask DisposeAsync() + { + await _redisFixture.MessageConsumer.DisposeAsync(); + await _redisFixture.MessageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_requeing_a_failed_message_with_delay.cs b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_requeing_a_failed_message_with_delay.cs index 00683541ed..7cde126a19 100644 --- a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_requeing_a_failed_message_with_delay.cs +++ b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_requeing_a_failed_message_with_delay.cs @@ -3,44 +3,43 @@ using FluentAssertions; using Xunit; -namespace Paramore.Brighter.Redis.Tests.MessagingGateway +namespace Paramore.Brighter.Redis.Tests.MessagingGateway; + +[Collection("Redis Shared Pool")] //shared connection pool so run sequentially +[Trait("Category", "Redis")] +public class RedisRequeueWithDelayTests : IClassFixture { - [Collection("Redis Shared Pool")] //shared connection pool so run sequentially - [Trait("Category", "Redis")] - public class RedisRequeueWithDelayTests : IClassFixture - { - private readonly RedisFixture _redisFixture; - private readonly Message _messageOne; + private readonly RedisFixture _redisFixture; + private readonly Message _messageOne; - public RedisRequeueWithDelayTests(RedisFixture redisFixture) - { - const string topic = "test"; - _redisFixture = redisFixture; - _messageOne = new Message( - new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(topic), MessageType.MT_COMMAND), - new MessageBody("test content") - ); - } + public RedisRequeueWithDelayTests(RedisFixture redisFixture) + { + const string topic = "test"; + _redisFixture = redisFixture; + _messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(topic), MessageType.MT_COMMAND), + new MessageBody("test content") + ); + } - [Fact] - public void When_requeing_a_failed_message_with_delay() - { - //clear the queue, and ensure it exists - _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)); + [Fact] + public void When_requeing_a_failed_message_with_delay() + { + //clear the queue, and ensure it exists + _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)); - //send & receive a message - _redisFixture.MessageProducer.Send(_messageOne); - var message = _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); - message.Header.HandledCount.Should().Be(0); - message.Header.Delayed.Should().Be(TimeSpan.Zero); + //send & receive a message + _redisFixture.MessageProducer.Send(_messageOne); + var message = _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); + message.Header.HandledCount.Should().Be(0); + message.Header.Delayed.Should().Be(TimeSpan.Zero); - //now requeue with a delay - _redisFixture.MessageConsumer.Requeue(_messageOne, TimeSpan.FromMilliseconds(1000)); + //now requeue with a delay + _redisFixture.MessageConsumer.Requeue(_messageOne, TimeSpan.FromMilliseconds(1000)); - //receive and assert - message = _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); - message.Header.HandledCount.Should().Be(1); - message.Header.Delayed.Should().Be(TimeSpan.FromMilliseconds(1000)); - } + //receive and assert + message = _redisFixture.MessageConsumer.Receive(TimeSpan.FromMilliseconds(1000)).Single(); + message.Header.HandledCount.Should().Be(1); + message.Header.Delayed.Should().Be(TimeSpan.FromMilliseconds(1000)); } } diff --git a/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_requeing_a_failed_message_with_delay_async.cs b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_requeing_a_failed_message_with_delay_async.cs new file mode 100644 index 0000000000..db6fa646e6 --- /dev/null +++ b/tests/Paramore.Brighter.Redis.Tests/MessagingGateway/When_requeing_a_failed_message_with_delay_async.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace Paramore.Brighter.Redis.Tests.MessagingGateway; + +[Collection("Redis Shared Pool")] //shared connection pool so run sequentially +[Trait("Category", "Redis")] +public class RedisRequeueWithDelayTestsAsync : IClassFixture +{ + private readonly RedisFixture _redisFixture; + private readonly Message _messageOne; + + public RedisRequeueWithDelayTestsAsync(RedisFixture redisFixture) + { + const string topic = "test"; + _redisFixture = redisFixture; + _messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), new RoutingKey(topic), MessageType.MT_COMMAND), + new MessageBody("test content") + ); + } + + [Fact] + public async Task When_requeing_a_failed_message_with_delay_async() + { + // Clear the queue, and ensure it exists + await _redisFixture.MessageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000)); + + // Send & receive a message + await _redisFixture.MessageProducer.SendAsync(_messageOne); + var message = (await _redisFixture.MessageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))).Single(); + message.Header.HandledCount.Should().Be(0); + message.Header.Delayed.Should().Be(TimeSpan.Zero); + + // Now requeue with a delay + await _redisFixture.MessageConsumer.RequeueAsync(_messageOne, TimeSpan.FromMilliseconds(1000)); + + // Receive and assert + message = (await _redisFixture.MessageConsumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))).Single(); + message.Header.HandledCount.Should().Be(1); + message.Header.Delayed.Should().Be(TimeSpan.FromMilliseconds(1000)); + } +} diff --git a/tests/Paramore.Brighter.Redis.Tests/TestDoubles/RedisMessageConsumerSocketErrorOnGetClient.cs b/tests/Paramore.Brighter.Redis.Tests/TestDoubles/RedisMessageConsumerSocketErrorOnGetClient.cs index 0e6130a09b..e1778f861d 100644 --- a/tests/Paramore.Brighter.Redis.Tests/TestDoubles/RedisMessageConsumerSocketErrorOnGetClient.cs +++ b/tests/Paramore.Brighter.Redis.Tests/TestDoubles/RedisMessageConsumerSocketErrorOnGetClient.cs @@ -2,25 +2,20 @@ using Paramore.Brighter.MessagingGateway.Redis; using ServiceStack.Redis; -namespace Paramore.Brighter.Redis.Tests.TestDoubles -{ - public class RedisMessageConsumerSocketErrorOnGetClient : RedisMessageConsumer - { - private const string SocketException = - "localhost:6379"; - - public RedisMessageConsumerSocketErrorOnGetClient( - RedisMessagingGatewayConfiguration redisMessagingGatewayConfiguration, - string queueName, - string topic) - : base(redisMessagingGatewayConfiguration, queueName, topic) - { - } +namespace Paramore.Brighter.Redis.Tests.TestDoubles; - protected override IRedisClient GetClient() - { - throw new RedisException(SocketException, new SocketException((int) SocketError.AccessDenied)); - } +public class RedisMessageConsumerSocketErrorOnGetClient( + RedisMessagingGatewayConfiguration redisMessagingGatewayConfiguration, + ChannelName queueName, + RoutingKey topic) + : RedisMessageConsumer(redisMessagingGatewayConfiguration, queueName, topic) +{ + private const string SocketException = + "localhost:6379"; + protected override IRedisClient GetClient() + { + throw new RedisException(SocketException, new SocketException((int) SocketError.AccessDenied)); } -} \ No newline at end of file + +} diff --git a/tests/Paramore.Brighter.Redis.Tests/TestDoubles/RedisMessageConsumerTimeoutOnGetClient.cs b/tests/Paramore.Brighter.Redis.Tests/TestDoubles/RedisMessageConsumerTimeoutOnGetClient.cs index 64cef06fac..9a44f57e57 100644 --- a/tests/Paramore.Brighter.Redis.Tests/TestDoubles/RedisMessageConsumerTimeoutOnGetClient.cs +++ b/tests/Paramore.Brighter.Redis.Tests/TestDoubles/RedisMessageConsumerTimeoutOnGetClient.cs @@ -2,24 +2,19 @@ using Paramore.Brighter.MessagingGateway.Redis; using ServiceStack.Redis; -namespace Paramore.Brighter.Redis.Tests.TestDoubles +namespace Paramore.Brighter.Redis.Tests.TestDoubles; + +public class RedisMessageConsumerTimeoutOnGetClient( + RedisMessagingGatewayConfiguration redisMessagingGatewayConfiguration, + ChannelName queueName, + RoutingKey topic) + : RedisMessageConsumer(redisMessagingGatewayConfiguration, queueName, topic) { - public class RedisMessageConsumerTimeoutOnGetClient : RedisMessageConsumer - { - private const string PoolTimeoutError = - "Redis Timeout expired. The timeout period elapsed prior to obtaining a subscription from the pool. This may have occurred because all pooled connections were in use."; - - public RedisMessageConsumerTimeoutOnGetClient( - RedisMessagingGatewayConfiguration redisMessagingGatewayConfiguration, - string queueName, - string topic) - : base(redisMessagingGatewayConfiguration, queueName, topic) - { - } + private const string PoolTimeoutError = + "Redis Timeout expired. The timeout period elapsed prior to obtaining a subscription from the pool. This may have occurred because all pooled connections were in use."; - protected override IRedisClient GetClient() - { - throw new TimeoutException(PoolTimeoutError); - } + protected override IRedisClient GetClient() + { + throw new TimeoutException(PoolTimeoutError); } -} \ No newline at end of file +} diff --git a/tests/Paramore.Brighter.Redis.Tests/TestDoubles/TestDoublesRedisMessageConsumer.cs b/tests/Paramore.Brighter.Redis.Tests/TestDoubles/TestDoublesRedisMessageConsumer.cs deleted file mode 100644 index 2f1003b877..0000000000 --- a/tests/Paramore.Brighter.Redis.Tests/TestDoubles/TestDoublesRedisMessageConsumer.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Paramore.Brighter.Redis.Tests.TestDoubles -{ - public class ConnectingToPoolFailedRedisMessageConsumer - { - //TODO: What kind of exceptions are thrown, and from where? - } -} \ No newline at end of file diff --git a/tests/Paramore.Brighter.Redis.Tests/TestDoubles/TestRedisGateway.cs b/tests/Paramore.Brighter.Redis.Tests/TestDoubles/TestRedisGateway.cs new file mode 100644 index 0000000000..3394b09538 --- /dev/null +++ b/tests/Paramore.Brighter.Redis.Tests/TestDoubles/TestRedisGateway.cs @@ -0,0 +1,27 @@ +using System; +using Paramore.Brighter.MessagingGateway.Redis; +using ServiceStack.Redis; + +namespace Paramore.Brighter.Redis.Tests.TestDoubles; + +/// +/// There are some properties we want to test, use a test wrapper to expose them, instead of leaking from +/// run-time classes +/// +public class TestRedisGateway : RedisMessageGateway, IDisposable +{ + public TestRedisGateway(RedisMessagingGatewayConfiguration redisMessagingGatewayConfiguration, RoutingKey topic) + : base(redisMessagingGatewayConfiguration, topic) + { + OverrideRedisClientDefaults(); + } + + + public new TimeSpan MessageTimeToLive => base.MessageTimeToLive; + + public void Dispose() + { + DisposePool(); + RedisConfig.Reset(); + } +}