Implemented QueueArgumentsAttribute (untested)

This commit is contained in:
Mark van Renswoude 2022-11-17 16:47:07 +01:00
parent c75f893da8
commit 7143ad3c2f
29 changed files with 281 additions and 133 deletions

View File

@ -24,7 +24,7 @@ namespace ExampleLib
private readonly IDependencyContainer dependencyResolver;
private readonly int expectedDoneCount;
private int doneCount;
private readonly TaskCompletionSource<bool> doneSignal = new TaskCompletionSource<bool>();
private readonly TaskCompletionSource<bool> doneSignal = new();
/// <param name="dependencyResolver">Uses Tapeti's IDependencyContainer interface so you can easily switch an example to your favourite IoC container</param>

View File

@ -304,7 +304,7 @@ namespace Tapeti.Flow.Default
private readonly ITapetiConfig config;
private readonly FlowProvider flowProvider;
private readonly List<RequestInfo> requests = new List<RequestInfo>();
private readonly List<RequestInfo> requests = new();
public ParallelRequestBuilder(ITapetiConfig config, FlowProvider flowProvider)

View File

@ -29,9 +29,9 @@ namespace Tapeti.Flow.Default
}
}
private readonly ConcurrentDictionary<Guid, CachedFlowState> flowStates = new ConcurrentDictionary<Guid, CachedFlowState>();
private readonly ConcurrentDictionary<Guid, Guid> continuationLookup = new ConcurrentDictionary<Guid, Guid>();
private readonly LockCollection<Guid> locks = new LockCollection<Guid>(EqualityComparer<Guid>.Default);
private readonly ConcurrentDictionary<Guid, CachedFlowState> flowStates = new();
private readonly ConcurrentDictionary<Guid, Guid> continuationLookup = new();
private readonly LockCollection<Guid> locks = new(EqualityComparer<Guid>.Default);
private HashSet<string> validatedMethods;
private readonly IFlowRepository repository;

View File

@ -60,7 +60,7 @@ namespace Tapeti.Flow.FlowHelpers
internal volatile LockItem Next;
private readonly Dictionary<T, LockItem> locks;
private readonly TaskCompletionSource<IDisposable> tcs = new TaskCompletionSource<IDisposable>(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly TaskCompletionSource<IDisposable> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly T key;
public LockItem(Dictionary<T, LockItem> locks, T key)

View File

@ -18,7 +18,7 @@ namespace Tapeti.Flow.FlowHelpers
}
private static readonly Regex DeserializeRegex = new Regex("^(?<method>.+?)@(?<assembly>.+?):(?<type>.+?)$");
private static readonly Regex DeserializeRegex = new("^(?<method>.+?)@(?<assembly>.+?):(?<type>.+?)$");
/// <summary>

View File

@ -10,7 +10,7 @@ namespace Tapeti.Serilog.Default
public class DiagnosticContext : IDiagnosticContext
{
private readonly global::Serilog.ILogger logger;
private readonly List<LogEventProperty> properties = new List<LogEventProperty>();
private readonly List<LogEventProperty> properties = new();
/// <summary>

View File

@ -129,10 +129,11 @@ namespace Tapeti.Serilog
}
/// <inheritdoc />
public void QueueExistsWarning(string queueName, Dictionary<string, string> arguments)
public void QueueExistsWarning(string queueName, IReadOnlyDictionary<string, string> existingArguments, IReadOnlyDictionary<string, string> arguments)
{
seriLogger.Warning("Tapeti: durable queue {queueName} exists with incompatible x-arguments ({arguments}) and will not be redeclared, queue will be consumed as-is",
seriLogger.Warning("Tapeti: durable queue {queueName} exists with incompatible x-arguments ({existingArguments} vs. {arguments}) and will not be redeclared, queue will be consumed as-is",
queueName,
existingArguments,
arguments);
}

View File

@ -8,6 +8,12 @@
<NoWarn>1701;1702</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Core\**" />
<EmbeddedResource Remove="Core\**" />
<None Remove="Core\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
@ -22,8 +28,4 @@
<ProjectReference Include="..\Tapeti\Tapeti.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Core\" />
</ItemGroup>
</Project>

View File

@ -33,7 +33,7 @@ namespace Tapeti.Transient
/// <inheritdoc />
public async ValueTask Apply(IBindingTarget target)
{
QueueName = await target.BindDynamicDirect(dynamicQueuePrefix);
QueueName = await target.BindDynamicDirect(dynamicQueuePrefix, null);
router.TransientResponseQueueName = QueueName;
}

View File

@ -13,7 +13,7 @@ namespace Tapeti.Transient
internal class TransientRouter
{
private readonly int defaultTimeoutMs;
private readonly ConcurrentDictionary<Guid, TaskCompletionSource<object>> map = new ConcurrentDictionary<Guid, TaskCompletionSource<object>>();
private readonly ConcurrentDictionary<Guid, TaskCompletionSource<object>> map = new();
/// <summary>
/// The generated name of the dynamic queue to which responses should be sent.

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Tapeti.Config
@ -80,7 +81,8 @@ namespace Tapeti.Config
/// </summary>
/// <param name="messageClass">The message class to be bound to the queue</param>
/// <param name="queueName">The name of the durable queue</param>
ValueTask BindDurable(Type messageClass, string queueName);
/// <param name="arguments">Optional arguments</param>
ValueTask BindDurable(Type messageClass, string queueName, IReadOnlyDictionary<string, string> arguments);
/// <summary>
/// Binds the messageClass to a dynamic auto-delete queue.
@ -91,15 +93,17 @@ namespace Tapeti.Config
/// </remarks>
/// <param name="messageClass">The message class to be bound to the queue</param>
/// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param>
/// <param name="arguments">Optional arguments</param>
/// <returns>The generated name of the dynamic queue</returns>
ValueTask<string> BindDynamic(Type messageClass, string queuePrefix = null);
ValueTask<string> BindDynamic(Type messageClass, string queuePrefix, IReadOnlyDictionary<string, string> arguments);
/// <summary>
/// Declares a durable queue but does not add a binding for a messageClass' routing key.
/// Used for direct-to-queue messages.
/// </summary>
/// <param name="queueName">The name of the durable queue</param>
ValueTask BindDurableDirect(string queueName);
/// <param name="arguments">Optional arguments</param>
ValueTask BindDurableDirect(string queueName, IReadOnlyDictionary<string, string> arguments);
/// <summary>
/// Declares a dynamic queue but does not add a binding for a messageClass' routing key.
@ -107,16 +111,18 @@ namespace Tapeti.Config
/// </summary>
/// <param name="messageClass">The message class which will be handled on the queue. It is not actually bound to the queue.</param>
/// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param>
/// <param name="arguments">Optional arguments</param>
/// <returns>The generated name of the dynamic queue</returns>
ValueTask<string> BindDynamicDirect(Type messageClass = null, string queuePrefix = null);
ValueTask<string> BindDynamicDirect(Type messageClass, string queuePrefix, IReadOnlyDictionary<string, string> arguments);
/// <summary>
/// Declares a dynamic queue but does not add a binding for a messageClass' routing key.
/// Used for direct-to-queue messages. Guarantees a unique queue.
/// </summary>
/// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param>
/// <param name="arguments">Optional arguments</param>
/// <returns>The generated name of the dynamic queue</returns>
ValueTask<string> BindDynamicDirect(string queuePrefix = null);
ValueTask<string> BindDynamicDirect(string queuePrefix, IReadOnlyDictionary<string, string> arguments);
/// <summary>
/// Marks the specified durable queue as having an obsolete binding. If after all bindings have subscribed, the queue only contains obsolete

View File

@ -74,11 +74,11 @@ namespace Tapeti.Connection
/// <summary>
/// Starts a consumer for the specified queue, using the provided bindings to handle messages.
/// </summary>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
/// <param name="queueName"></param>
/// <param name="consumer">The consumer implementation which will receive the messages from the queue</param>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
/// <returns>The consumer tag as returned by BasicConsume.</returns>
Task<TapetiConsumerTag> Consume(CancellationToken cancellationToken, string queueName, IConsumer consumer);
Task<TapetiConsumerTag> Consume(string queueName, IConsumer consumer, CancellationToken cancellationToken);
/// <summary>
/// Stops the consumer with the specified tag.
@ -89,40 +89,43 @@ namespace Tapeti.Connection
/// <summary>
/// Creates a durable queue if it does not already exist, and updates the bindings.
/// </summary>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
/// <param name="queueName">The name of the queue to create</param>
/// <param name="bindings">A list of bindings. Any bindings already on the queue which are not in this list will be removed</param>
Task DurableQueueDeclare(CancellationToken cancellationToken, string queueName, IEnumerable<QueueBinding> bindings);
/// <param name="arguments">Optional arguments</param>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
Task DurableQueueDeclare(string queueName, IEnumerable<QueueBinding> bindings, IReadOnlyDictionary<string, string> arguments, CancellationToken cancellationToken);
/// <summary>
/// Verifies a durable queue exists. Will raise an exception if it does not.
/// </summary>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
/// <param name="queueName">The name of the queue to verify</param>
Task DurableQueueVerify(CancellationToken cancellationToken, string queueName);
/// <param name="arguments">Optional arguments</param>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
Task DurableQueueVerify(string queueName, IReadOnlyDictionary<string, string> arguments, CancellationToken cancellationToken);
/// <summary>
/// Deletes a durable queue.
/// </summary>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
/// <param name="queueName">The name of the queue to delete</param>
/// <param name="onlyIfEmpty">If true, the queue will only be deleted if it is empty otherwise all bindings will be removed. If false, the queue is deleted even if there are queued messages.</param>
Task DurableQueueDelete(CancellationToken cancellationToken, string queueName, bool onlyIfEmpty = true);
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
Task DurableQueueDelete(string queueName, bool onlyIfEmpty, CancellationToken cancellationToken);
/// <summary>
/// Creates a dynamic queue.
/// </summary>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
/// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param>
Task<string> DynamicQueueDeclare(CancellationToken cancellationToken, string queuePrefix = null);
/// <param name="arguments">Optional arguments</param>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
Task<string> DynamicQueueDeclare(string queuePrefix, IReadOnlyDictionary<string, string> arguments, CancellationToken cancellationToken);
/// <summary>
/// Add a binding to a dynamic queue.
/// </summary>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
/// <param name="queueName">The name of the dynamic queue previously created using DynamicQueueDeclare</param>
/// <param name="binding">The binding to add to the dynamic queue</param>
Task DynamicQueueBind(CancellationToken cancellationToken, string queueName, QueueBinding binding);
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
Task DynamicQueueBind(string queueName, QueueBinding binding, CancellationToken cancellationToken);
/// <summary>
/// Closes the connection to RabbitMQ gracefully.

View File

@ -20,7 +20,7 @@ namespace Tapeti.Connection
internal class TapetiChannel
{
private readonly Func<IModel> modelFactory;
private readonly object taskQueueLock = new object();
private readonly object taskQueueLock = new();
private SingleThreadTaskQueue taskQueue;
private readonly ModelProvider modelProvider;

View File

@ -13,6 +13,7 @@ using RabbitMQ.Client.Exceptions;
using Tapeti.Config;
using Tapeti.Default;
using Tapeti.Exceptions;
using Tapeti.Helpers;
namespace Tapeti.Connection
{
@ -50,7 +51,7 @@ namespace Tapeti.Connection
private readonly HttpClient managementClient;
// These fields must be locked using connectionLock
private readonly object connectionLock = new object();
private readonly object connectionLock = new();
private long connectionReference;
private RabbitMQ.Client.IConnection connection;
private IModel consumeChannelModel;
@ -61,12 +62,12 @@ namespace Tapeti.Connection
// These fields are for use in a single TapetiChannel's queue only!
private ulong lastDeliveryTag;
private readonly HashSet<string> deletedQueues = new HashSet<string>();
private readonly HashSet<string> deletedQueues = new();
// These fields must be locked using confirmLock, since the callbacks for BasicAck/BasicReturn can run in a different thread
private readonly object confirmLock = new object();
private readonly Dictionary<ulong, ConfirmMessageInfo> confirmMessages = new Dictionary<ulong, ConfirmMessageInfo>();
private readonly Dictionary<string, ReturnInfo> returnRoutingKeys = new Dictionary<string, ReturnInfo>();
private readonly object confirmLock = new();
private readonly Dictionary<ulong, ConfirmMessageInfo> confirmMessages = new();
private readonly Dictionary<string, ReturnInfo> returnRoutingKeys = new();
private class ConfirmMessageInfo
@ -198,7 +199,7 @@ namespace Tapeti.Connection
/// <inheritdoc />
public async Task<TapetiConsumerTag> Consume(CancellationToken cancellationToken, string queueName, IConsumer consumer)
public async Task<TapetiConsumerTag> Consume(string queueName, IConsumer consumer, CancellationToken cancellationToken)
{
if (deletedQueues.Contains(queueName))
return null;
@ -285,7 +286,7 @@ namespace Tapeti.Connection
}
private async Task<bool> GetDurableQueueDeclareRequired(string queueName)
private async Task<bool> GetDurableQueueDeclareRequired(string queueName, IReadOnlyDictionary<string, string> arguments)
{
var existingQueue = await GetQueueInfo(queueName);
if (existingQueue == null)
@ -294,18 +295,22 @@ namespace Tapeti.Connection
if (!existingQueue.Durable || existingQueue.AutoDelete || existingQueue.Exclusive)
throw new InvalidOperationException($"Durable queue {queueName} already exists with incompatible parameters, durable = {existingQueue.Durable} (expected True), autoDelete = {existingQueue.AutoDelete} (expected False), exclusive = {existingQueue.Exclusive} (expected False)");
if (existingQueue.Arguments.Count <= 0)
if (arguments == null && existingQueue.Arguments.Count == 0)
return true;
(logger as IBindingLogger)?.QueueExistsWarning(queueName, existingQueue.Arguments);
if (existingQueue.Arguments.NullSafeSameValues(arguments))
return true;
(logger as IBindingLogger)?.QueueExistsWarning(queueName, existingQueue.Arguments, arguments);
return false;
}
/// <inheritdoc />
public async Task DurableQueueDeclare(CancellationToken cancellationToken, string queueName, IEnumerable<QueueBinding> bindings)
public async Task DurableQueueDeclare(string queueName, IEnumerable<QueueBinding> bindings, IReadOnlyDictionary<string, string> arguments, CancellationToken cancellationToken)
{
var declareRequired = await GetDurableQueueDeclareRequired(queueName);
var declareRequired = await GetDurableQueueDeclareRequired(queueName, arguments);
var existingBindings = (await GetQueueBindings(queueName)).ToList();
var currentBindings = bindings.ToList();
@ -319,7 +324,7 @@ namespace Tapeti.Connection
if (declareRequired)
{
bindingLogger?.QueueDeclare(queueName, true, false);
channel.QueueDeclare(queueName, true, false, false);
channel.QueueDeclare(queueName, true, false, false, GetDeclareArguments(arguments));
}
foreach (var binding in currentBindings.Except(existingBindings))
@ -337,10 +342,20 @@ namespace Tapeti.Connection
});
}
/// <inheritdoc />
public async Task DurableQueueVerify(CancellationToken cancellationToken, string queueName)
private static IDictionary<string, object> GetDeclareArguments(IReadOnlyDictionary<string, string> arguments)
{
if (!await GetDurableQueueDeclareRequired(queueName))
if (arguments == null || arguments.Count == 0)
return null;
return arguments.ToDictionary(p => p.Key, p => (object)Encoding.UTF8.GetBytes(p.Value));
}
/// <inheritdoc />
public async Task DurableQueueVerify(string queueName, IReadOnlyDictionary<string, string> arguments, CancellationToken cancellationToken)
{
if (!await GetDurableQueueDeclareRequired(queueName, arguments))
return;
await GetTapetiChannel(TapetiChannelType.Consume).Queue(channel =>
@ -355,7 +370,7 @@ namespace Tapeti.Connection
/// <inheritdoc />
public async Task DurableQueueDelete(CancellationToken cancellationToken, string queueName, bool onlyIfEmpty = true)
public async Task DurableQueueDelete(string queueName, bool onlyIfEmpty, CancellationToken cancellationToken)
{
if (!onlyIfEmpty)
{
@ -440,7 +455,7 @@ namespace Tapeti.Connection
/// <inheritdoc />
public async Task<string> DynamicQueueDeclare(CancellationToken cancellationToken, string queuePrefix = null)
public async Task<string> DynamicQueueDeclare(string queuePrefix, IReadOnlyDictionary<string, string> arguments, CancellationToken cancellationToken)
{
string queueName = null;
var bindingLogger = logger as IBindingLogger;
@ -454,11 +469,11 @@ namespace Tapeti.Connection
{
queueName = queuePrefix + "." + Guid.NewGuid().ToString("N");
bindingLogger?.QueueDeclare(queueName, false, false);
channel.QueueDeclare(queueName);
channel.QueueDeclare(queueName, arguments: GetDeclareArguments(arguments));
}
else
{
queueName = channel.QueueDeclare().QueueName;
queueName = channel.QueueDeclare(arguments: GetDeclareArguments(arguments)).QueueName;
bindingLogger?.QueueDeclare(queueName, false, false);
}
});
@ -467,7 +482,7 @@ namespace Tapeti.Connection
}
/// <inheritdoc />
public async Task DynamicQueueBind(CancellationToken cancellationToken, string queueName, QueueBinding binding)
public async Task DynamicQueueBind(string queueName, QueueBinding binding, CancellationToken cancellationToken)
{
await GetTapetiChannel(TapetiChannelType.Consume).Queue(channel =>
{
@ -523,7 +538,7 @@ namespace Tapeti.Connection
}
private static readonly List<HttpStatusCode> TransientStatusCodes = new List<HttpStatusCode>()
private static readonly List<HttpStatusCode> TransientStatusCodes = new()
{
HttpStatusCode.GatewayTimeout,
HttpStatusCode.RequestTimeout,
@ -675,7 +690,7 @@ namespace Tapeti.Connection
}
private readonly HashSet<string> declaredExchanges = new HashSet<string>();
private readonly HashSet<string> declaredExchanges = new();
private void DeclareExchange(IModel channel, string exchange)
{
@ -842,7 +857,7 @@ namespace Tapeti.Connection
GetTapetiChannel(TapetiChannelType.Consume).QueueRetryable(_ => { });
};
capturedPublishChannelModel.ModelShutdown += (sender, args) =>
capturedPublishChannelModel.ModelShutdown += (_, _) =>
{
lock (connectionLock)
{

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tapeti.Config;
using Tapeti.Helpers;
namespace Tapeti.Connection
{
@ -13,7 +14,7 @@ namespace Tapeti.Connection
private readonly Func<ITapetiClient> clientFactory;
private readonly ITapetiConfig config;
private bool consuming;
private readonly List<TapetiConsumerTag> consumerTags = new List<TapetiConsumerTag>();
private readonly List<TapetiConsumerTag> consumerTags = new();
private CancellationTokenSource initializeCancellationTokenSource;
@ -149,7 +150,7 @@ namespace Tapeti.Connection
var queueName = group.Key;
var consumer = new TapetiConsumer(cancellationToken, config, queueName, group);
return await clientFactory().Consume(cancellationToken, queueName, consumer);
return await clientFactory().Consume(queueName, consumer, cancellationToken);
}))).Where(t => t != null));
}
@ -165,9 +166,10 @@ namespace Tapeti.Connection
{
public string QueueName;
public List<Type> MessageClasses;
public IReadOnlyDictionary<string, string> Arguments;
}
private readonly Dictionary<string, List<DynamicQueueInfo>> dynamicQueues = new Dictionary<string, List<DynamicQueueInfo>>();
private readonly Dictionary<string, List<DynamicQueueInfo>> dynamicQueues = new();
protected CustomBindingTarget(Func<ITapetiClient> clientFactory, IRoutingKeyStrategy routingKeyStrategy, IExchangeStrategy exchangeStrategy, CancellationToken cancellationToken)
@ -185,38 +187,38 @@ namespace Tapeti.Connection
}
public abstract ValueTask BindDurable(Type messageClass, string queueName);
public abstract ValueTask BindDurableDirect(string queueName);
public abstract ValueTask BindDurable(Type messageClass, string queueName, IReadOnlyDictionary<string, string> arguments);
public abstract ValueTask BindDurableDirect(string queueName, IReadOnlyDictionary<string, string> arguments);
public abstract ValueTask BindDurableObsolete(string queueName);
public async ValueTask<string> BindDynamic(Type messageClass, string queuePrefix = null)
public async ValueTask<string> BindDynamic(Type messageClass, string queuePrefix, IReadOnlyDictionary<string, string> arguments)
{
var result = await DeclareDynamicQueue(messageClass, queuePrefix);
var result = await DeclareDynamicQueue(messageClass, queuePrefix, arguments);
if (!result.IsNewMessageClass)
return result.QueueName;
var routingKey = RoutingKeyStrategy.GetRoutingKey(messageClass);
var exchange = ExchangeStrategy.GetExchange(messageClass);
await ClientFactory().DynamicQueueBind(CancellationToken, result.QueueName, new QueueBinding(exchange, routingKey));
await ClientFactory().DynamicQueueBind(result.QueueName, new QueueBinding(exchange, routingKey), CancellationToken);
return result.QueueName;
}
public async ValueTask<string> BindDynamicDirect(Type messageClass, string queuePrefix = null)
public async ValueTask<string> BindDynamicDirect(Type messageClass, string queuePrefix, IReadOnlyDictionary<string, string> arguments)
{
var result = await DeclareDynamicQueue(messageClass, queuePrefix);
var result = await DeclareDynamicQueue(messageClass, queuePrefix, arguments);
return result.QueueName;
}
public async ValueTask<string> BindDynamicDirect(string queuePrefix = null)
public async ValueTask<string> BindDynamicDirect(string queuePrefix, IReadOnlyDictionary<string, string> arguments)
{
// If we don't know the routing key, always create a new queue to ensure there is no overlap.
// Keep it out of the dynamicQueues dictionary, so it can't be re-used later on either.
return await ClientFactory().DynamicQueueDeclare(CancellationToken, queuePrefix);
return await ClientFactory().DynamicQueueDeclare(queuePrefix, arguments, CancellationToken);
}
@ -226,7 +228,7 @@ namespace Tapeti.Connection
public bool IsNewMessageClass;
}
private async Task<DeclareDynamicQueueResult> DeclareDynamicQueue(Type messageClass, string queuePrefix)
private async Task<DeclareDynamicQueueResult> DeclareDynamicQueue(Type messageClass, string queuePrefix, IReadOnlyDictionary<string, string> arguments)
{
// Group by prefix
var key = queuePrefix ?? "";
@ -241,7 +243,7 @@ namespace Tapeti.Connection
foreach (var existingQueueInfo in prefixQueues)
{
// ReSharper disable once InvertIf
if (!existingQueueInfo.MessageClasses.Contains(messageClass))
if (!existingQueueInfo.MessageClasses.Contains(messageClass) && existingQueueInfo.Arguments.NullSafeSameValues(arguments))
{
// Allow this routing key in the existing dynamic queue
var result = new DeclareDynamicQueueResult
@ -258,11 +260,12 @@ namespace Tapeti.Connection
}
// Declare a new queue
var queueName = await ClientFactory().DynamicQueueDeclare(CancellationToken, queuePrefix);
var queueName = await ClientFactory().DynamicQueueDeclare(queuePrefix, arguments, CancellationToken);
var queueInfo = new DynamicQueueInfo
{
QueueName = queueName,
MessageClasses = new List<Type> { messageClass }
MessageClasses = new List<Type> { messageClass },
Arguments = arguments
};
prefixQueues.Add(queueInfo);
@ -278,8 +281,15 @@ namespace Tapeti.Connection
private class DeclareDurableQueuesBindingTarget : CustomBindingTarget
{
private readonly Dictionary<string, List<Type>> durableQueues = new Dictionary<string, List<Type>>();
private readonly HashSet<string> obsoleteDurableQueues = new HashSet<string>();
private struct DurableQueueInfo
{
public List<Type> MessageClasses;
public IReadOnlyDictionary<string, string> Arguments;
}
private readonly Dictionary<string, DurableQueueInfo> durableQueues = new();
private readonly HashSet<string> obsoleteDurableQueues = new();
public DeclareDurableQueuesBindingTarget(Func<ITapetiClient> clientFactory, IRoutingKeyStrategy routingKeyStrategy, IExchangeStrategy exchangeStrategy, CancellationToken cancellationToken) : base(clientFactory, routingKeyStrategy, exchangeStrategy, cancellationToken)
@ -287,29 +297,50 @@ namespace Tapeti.Connection
}
public override ValueTask BindDurable(Type messageClass, string queueName)
public override ValueTask BindDurable(Type messageClass, string queueName, IReadOnlyDictionary<string, string> arguments)
{
// Collect the message classes per queue so we can determine afterwards
// if any of the bindings currently set on the durable queue are no
// longer valid and should be removed.
if (!durableQueues.TryGetValue(queueName, out var messageClasses))
if (!durableQueues.TryGetValue(queueName, out var durableQueueInfo))
{
durableQueues.Add(queueName, new List<Type>
durableQueues.Add(queueName, new DurableQueueInfo
{
messageClass
MessageClasses = new List<Type>
{
messageClass
},
Arguments = arguments
});
}
else if (!messageClasses.Contains(messageClass))
messageClasses.Add(messageClass);
else
{
if (!durableQueueInfo.Arguments.NullSafeSameValues(arguments))
throw new TopologyConfigurationException($"Multiple conflicting QueueArguments attributes specified for queue {queueName}");
if (!durableQueueInfo.MessageClasses.Contains(messageClass))
durableQueueInfo.MessageClasses.Add(messageClass);
}
return default;
}
}
public override ValueTask BindDurableDirect(string queueName)
public override ValueTask BindDurableDirect(string queueName, IReadOnlyDictionary<string, string> arguments)
{
if (!durableQueues.ContainsKey(queueName))
durableQueues.Add(queueName, new List<Type>());
if (!durableQueues.TryGetValue(queueName, out var durableQueueInfo))
{
durableQueues.Add(queueName, new DurableQueueInfo
{
MessageClasses = new List<Type>(),
Arguments = arguments
});
}
else
{
if (!durableQueueInfo.Arguments.NullSafeSameValues(arguments))
throw new TopologyConfigurationException($"Multiple conflicting QueueArguments attributes specified for queue {queueName}");
}
return default;
}
@ -334,7 +365,7 @@ namespace Tapeti.Connection
{
await Task.WhenAll(durableQueues.Select(async queue =>
{
var bindings = queue.Value.Select(messageClass =>
var bindings = queue.Value.MessageClasses.Select(messageClass =>
{
var exchange = ExchangeStrategy.GetExchange(messageClass);
var routingKey = RoutingKeyStrategy.GetRoutingKey(messageClass);
@ -342,7 +373,7 @@ namespace Tapeti.Connection
return new QueueBinding(exchange, routingKey);
});
await client.DurableQueueDeclare(CancellationToken, queue.Key, bindings);
await client.DurableQueueDeclare(queue.Key, bindings, queue.Value.Arguments, CancellationToken);
}));
}
@ -351,7 +382,7 @@ namespace Tapeti.Connection
{
await Task.WhenAll(obsoleteDurableQueues.Except(durableQueues.Keys).Select(async queue =>
{
await client.DurableQueueDelete(CancellationToken, queue);
await client.DurableQueueDelete(queue, true, CancellationToken);
}));
}
}
@ -359,7 +390,7 @@ namespace Tapeti.Connection
private class PassiveDurableQueuesBindingTarget : CustomBindingTarget
{
private readonly HashSet<string> durableQueues = new HashSet<string>();
private readonly HashSet<string> durableQueues = new();
public PassiveDurableQueuesBindingTarget(Func<ITapetiClient> clientFactory, IRoutingKeyStrategy routingKeyStrategy, IExchangeStrategy exchangeStrategy, CancellationToken cancellationToken) : base(clientFactory, routingKeyStrategy, exchangeStrategy, cancellationToken)
@ -367,14 +398,14 @@ namespace Tapeti.Connection
}
public override async ValueTask BindDurable(Type messageClass, string queueName)
public override async ValueTask BindDurable(Type messageClass, string queueName, IReadOnlyDictionary<string, string> arguments)
{
await VerifyDurableQueue(queueName);
await VerifyDurableQueue(queueName, arguments);
}
public override async ValueTask BindDurableDirect(string queueName)
public override async ValueTask BindDurableDirect(string queueName, IReadOnlyDictionary<string, string> arguments)
{
await VerifyDurableQueue(queueName);
await VerifyDurableQueue(queueName, arguments);
}
public override ValueTask BindDurableObsolete(string queueName)
@ -383,12 +414,12 @@ namespace Tapeti.Connection
}
private async Task VerifyDurableQueue(string queueName)
private async Task VerifyDurableQueue(string queueName, IReadOnlyDictionary<string, string> arguments)
{
if (!durableQueues.Add(queueName))
return;
await ClientFactory().DurableQueueVerify(CancellationToken, queueName);
await ClientFactory().DurableQueueVerify(queueName, arguments, CancellationToken);
}
}
@ -400,12 +431,12 @@ namespace Tapeti.Connection
}
public override ValueTask BindDurable(Type messageClass, string queueName)
public override ValueTask BindDurable(Type messageClass, string queueName, IReadOnlyDictionary<string, string> arguments)
{
return default;
}
public override ValueTask BindDurableDirect(string queueName)
public override ValueTask BindDurableDirect(string queueName, IReadOnlyDictionary<string, string> arguments)
{
return default;
}

View File

@ -81,7 +81,13 @@ namespace Tapeti.Default
}
/// <inheritdoc />
public void QueueExistsWarning(string queueName, Dictionary<string, string> arguments)
public void QueueExistsWarning(string queueName, IReadOnlyDictionary<string, string> existingArguments, IReadOnlyDictionary<string, string> arguments)
{
Console.WriteLine($"[Tapeti] Durable queue {queueName} exists with incompatible x-arguments ({GetArgumentsText(existingArguments)} vs. {GetArgumentsText(arguments)}) and will not be redeclared, queue will be consumed as-is");
}
private static string GetArgumentsText(IReadOnlyDictionary<string, string> arguments)
{
var argumentsText = new StringBuilder();
foreach (var pair in arguments)
@ -91,10 +97,11 @@ namespace Tapeti.Default
argumentsText.Append($"{pair.Key} = {pair.Value}");
}
Console.WriteLine($"[Tapeti] Durable queue {queueName} exists with incompatible x-arguments ({argumentsText}) and will not be redeclared, queue will be consumed as-is");
return argumentsText.ToString();
}
/// <inheritdoc />
public void QueueBind(string queueName, bool durable, string exchange, string routingKey)
{

View File

@ -9,7 +9,7 @@ namespace Tapeti.Default
internal class ControllerBindingContext : IControllerBindingContext
{
private BindingTargetMode? bindingTargetMode;
private readonly List<IControllerMiddlewareBase> middleware = new List<IControllerMiddlewareBase>();
private readonly List<IControllerMiddlewareBase> middleware = new();
private readonly List<ControllerBindingParameter> parameters;
private readonly ControllerBindingResult result;

View File

@ -117,10 +117,10 @@ namespace Tapeti.Default
{
case BindingTargetMode.Default:
if (bindingInfo.QueueInfo.QueueType == QueueType.Dynamic)
QueueName = await target.BindDynamic(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name);
QueueName = await target.BindDynamic(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name, bindingInfo.QueueInfo.QueueArguments);
else
{
await target.BindDurable(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name);
await target.BindDurable(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name, bindingInfo.QueueInfo.QueueArguments);
QueueName = bindingInfo.QueueInfo.Name;
}
@ -128,10 +128,10 @@ namespace Tapeti.Default
case BindingTargetMode.Direct:
if (bindingInfo.QueueInfo.QueueType == QueueType.Dynamic)
QueueName = await target.BindDynamicDirect(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name);
QueueName = await target.BindDynamicDirect(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name, bindingInfo.QueueInfo.QueueArguments);
else
{
await target.BindDurableDirect(bindingInfo.QueueInfo.Name);
await target.BindDurableDirect(bindingInfo.QueueInfo.Name, bindingInfo.QueueInfo.QueueArguments);
QueueName = bindingInfo.QueueInfo.Name;
}
@ -316,7 +316,11 @@ namespace Tapeti.Default
/// </summary>
public string Name { get; set; }
/// <summary>
/// Optional arguments (x-arguments) passed when declaring the queue.
/// </summary>
public IReadOnlyDictionary<string, string> QueueArguments { get; set; }
/// <summary>
/// Determines if the QueueInfo properties contain a valid combination.
/// </summary>

View File

@ -16,8 +16,8 @@ namespace Tapeti.Default
private const string ClassTypeHeader = "classType";
private readonly ConcurrentDictionary<string, Type> deserializedTypeNames = new ConcurrentDictionary<string, Type>();
private readonly ConcurrentDictionary<Type, string> serializedTypeNames = new ConcurrentDictionary<Type, string>();
private readonly ConcurrentDictionary<string, Type> deserializedTypeNames = new();
private readonly ConcurrentDictionary<Type, string> serializedTypeNames = new();
private readonly JsonSerializerSettings serializerSettings;

View File

@ -8,7 +8,7 @@ namespace Tapeti.Default
{
internal class MessageContext : IMessageContext
{
private readonly Dictionary<Type, IMessageContextPayload> payloads = new Dictionary<Type, IMessageContextPayload>();
private readonly Dictionary<Type, IMessageContextPayload> payloads = new();
/// <inheritdoc />
@ -117,7 +117,7 @@ namespace Tapeti.Default
// ReSharper disable once InconsistentNaming
public class KeyValuePayload : IMessageContextPayload, IDisposable, IAsyncDisposable
{
private readonly Dictionary<string, object> items = new Dictionary<string, object>();
private readonly Dictionary<string, object> items = new();
public KeyValuePayload(string key, object value)

View File

@ -10,7 +10,7 @@ namespace Tapeti.Default
/// </summary>
public class MessageProperties : IMessageProperties
{
private readonly Dictionary<string, string> headers = new Dictionary<string, string>();
private readonly Dictionary<string, string> headers = new();
/// <inheritdoc />

View File

@ -13,7 +13,7 @@ namespace Tapeti.Default
/// </example>
public class NamespaceMatchExchangeStrategy : IExchangeStrategy
{
private static readonly Regex NamespaceRegex = new Regex("^(Messaging\\.)?(?<exchange>[^\\.]+)", RegexOptions.Compiled | RegexOptions.Singleline);
private static readonly Regex NamespaceRegex = new("^(Messaging\\.)?(?<exchange>[^\\.]+)", RegexOptions.Compiled | RegexOptions.Singleline);
/// <inheritdoc />

View File

@ -28,9 +28,9 @@ namespace Tapeti.Default
(?(?<=[A-Z])[A-Z](?=[a-z])|[A-Z])
)";
private static readonly Regex SeparatorRegex = new Regex(SeparatorPattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
private static readonly Regex SeparatorRegex = new(SeparatorPattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
private static readonly ConcurrentDictionary<Type, string> RoutingKeyCache = new ConcurrentDictionary<Type, string>();
private static readonly ConcurrentDictionary<Type, string> RoutingKeyCache = new();
/// <inheritdoc />

View File

@ -0,0 +1,30 @@
using System.Collections.Generic;
namespace Tapeti.Helpers
{
/// <summary>
/// Provides extension methods for dictionaries.
/// </summary>
public static class DictionaryHelper
{
/// <summary>
/// Checks if two dictionaries are considered compatible. If either is null they are considered empty.
/// </summary>
public static bool NullSafeSameValues(this IReadOnlyDictionary<string, string> arguments1, IReadOnlyDictionary<string, string> arguments2)
{
if (arguments1 == null || arguments2 == null)
return (arguments1 == null || arguments1.Count == 0) && (arguments2 == null || arguments2.Count == 0);
if (arguments1.Count != arguments2.Count)
return false;
foreach (var pair in arguments1)
{
if (!arguments2.TryGetValue(pair.Key, out var value2) || value2 != arguments1[pair.Key])
return false;
}
return true;
}
}
}

View File

@ -138,8 +138,9 @@ namespace Tapeti
/// If the queue already exists but should be compatible QueueDeclare will be called instead.
/// </summary>
/// <param name="queueName">The name of the queue that is declared</param>
/// <param name="arguments">The x-arguments of the existing queue</param>
void QueueExistsWarning(string queueName, Dictionary<string, string> arguments);
/// <param name="existingArguments">The x-arguments of the existing queue</param>
/// <param name="arguments">The x-arguments of the queue that would be declared</param>
void QueueExistsWarning(string queueName, IReadOnlyDictionary<string, string> existingArguments, IReadOnlyDictionary<string, string> arguments);
/// <summary>
/// Called before a binding is added to a queue.

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
@ -42,6 +42,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Tapeti.Annotations" Version="3.0.0" />
<PackageReference Include="Tapeti.Annotations" Version="3.*-*" />
</ItemGroup>
</Project>

View File

@ -18,7 +18,7 @@ namespace Tapeti
public class TapetiConfig : ITapetiConfigBuilder, ITapetiConfigBuilderAccess
{
private Config config;
private readonly List<IControllerBindingMiddleware> bindingMiddleware = new List<IControllerBindingMiddleware>();
private readonly List<IControllerBindingMiddleware> bindingMiddleware = new();
/// <inheritdoc />
@ -225,9 +225,9 @@ namespace Tapeti
/// <inheritdoc />
internal class Config : ITapetiConfig
{
private readonly ConfigFeatures features = new ConfigFeatures();
private readonly ConfigMiddleware middleware = new ConfigMiddleware();
private readonly ConfigBindings bindings = new ConfigBindings();
private readonly ConfigFeatures features = new();
private readonly ConfigMiddleware middleware = new();
private readonly ConfigBindings bindings = new();
public IDependencyResolver DependencyResolver { get; }
public ITapetiConfigFeatues Features => features;
@ -291,8 +291,8 @@ namespace Tapeti
internal class ConfigMiddleware : ITapetiConfigMiddleware
{
private readonly List<IMessageMiddleware> messageMiddleware = new List<IMessageMiddleware>();
private readonly List<IPublishMiddleware> publishMiddleware = new List<IPublishMiddleware>();
private readonly List<IMessageMiddleware> messageMiddleware = new();
private readonly List<IPublishMiddleware> publishMiddleware = new();
public IReadOnlyList<IMessageMiddleware> Message => messageMiddleware;

View File

@ -1,4 +1,5 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Tapeti.Annotations;
@ -79,7 +80,7 @@ namespace Tapeti
}
var methodQueueInfo = GetQueueInfo(method) ?? controllerQueueInfo;
if (!(methodQueueInfo is { IsValid: true }))
if (methodQueueInfo is not { IsValid: true })
throw new TopologyConfigurationException(
$"Method {method.Name} or controller {controller.Name} requires a queue attribute");
@ -136,12 +137,59 @@ namespace Tapeti
if (dynamicQueueAttribute != null && durableQueueAttribute != null)
throw new TopologyConfigurationException($"Cannot combine static and dynamic queue attributes on controller {member.DeclaringType?.Name} method {member.Name}");
var queueArgumentsAttribute = member.GetCustomAttribute<QueueArgumentsAttribute>();
if (dynamicQueueAttribute != null)
return new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Dynamic, Name = dynamicQueueAttribute.Prefix };
return new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Dynamic, Name = dynamicQueueAttribute.Prefix, QueueArguments = GetQueueArguments(queueArgumentsAttribute) };
return durableQueueAttribute != null
? new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Durable, Name = durableQueueAttribute.Name }
? new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Durable, Name = durableQueueAttribute.Name, QueueArguments = GetQueueArguments(queueArgumentsAttribute) }
: null;
}
private static IReadOnlyDictionary<string, string> GetQueueArguments(QueueArgumentsAttribute queueArgumentsAttribute)
{
if (queueArgumentsAttribute == null)
return null;
#if NETSTANDARD2_1_OR_GREATER
var arguments = new Dictionary<string, string>(queueArgumentsAttribute.CustomArguments);
#else
var arguments = new Dictionary<string, string>();
foreach (var pair in queueArgumentsAttribute.CustomArguments)
arguments.Add(pair.Key, pair.Value);
#endif
if (queueArgumentsAttribute.MaxLength > 0)
arguments.Add(@"x-max-length", queueArgumentsAttribute.MaxLength.ToString());
if (queueArgumentsAttribute.MaxLengthBytes > 0)
arguments.Add(@"x-max-length-bytes", queueArgumentsAttribute.MaxLengthBytes.ToString());
if (queueArgumentsAttribute.MessageTTL > 0)
arguments.Add(@"x-message-ttl", queueArgumentsAttribute.MessageTTL.ToString());
switch (queueArgumentsAttribute.Overflow)
{
case RabbitMQOverflow.NotSpecified:
break;
case RabbitMQOverflow.DropHead:
arguments.Add(@"x-overflow", @"drop-head");
break;
case RabbitMQOverflow.RejectPublish:
arguments.Add(@"x-overflow", @"reject-publish");
break;
case RabbitMQOverflow.RejectPublishDeadletter:
arguments.Add(@"x-overflow", @"reject-publish-dlx");
break;
default:
throw new ArgumentOutOfRangeException(nameof(queueArgumentsAttribute.Overflow), queueArgumentsAttribute.Overflow, "Unsupported Overflow value");
}
return arguments.Count > 0 ? arguments : null;
}
}
}

View File

@ -12,10 +12,10 @@ namespace Tapeti.Tasks
/// </summary>
public class SingleThreadTaskQueue : IDisposable
{
private readonly object previousTaskLock = new object();
private readonly object previousTaskLock = new();
private Task previousTask = Task.CompletedTask;
private readonly Lazy<SingleThreadTaskScheduler> singleThreadScheduler = new Lazy<SingleThreadTaskScheduler>();
private readonly Lazy<SingleThreadTaskScheduler> singleThreadScheduler = new();
/// <summary>
@ -70,7 +70,7 @@ namespace Tapeti.Tasks
public override int MaximumConcurrencyLevel => 1;
private readonly Queue<Task> scheduledTasks = new Queue<Task>();
private readonly Queue<Task> scheduledTasks = new();
private bool disposed;