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