2022-02-13 09:36:04 +00:00
|
|
|
using System;
|
2016-12-11 14:08:58 +00:00
|
|
|
using System.Collections.Generic;
|
2019-02-13 11:00:34 +00:00
|
|
|
using System.Linq;
|
2019-08-13 18:30:04 +00:00
|
|
|
using System.Net;
|
|
|
|
using System.Net.Http;
|
2019-08-18 09:06:33 +00:00
|
|
|
using System.Text;
|
2019-01-24 21:52:21 +00:00
|
|
|
using System.Threading;
|
2016-11-17 16:33:27 +00:00
|
|
|
using System.Threading.Tasks;
|
2019-08-13 18:30:04 +00:00
|
|
|
using Newtonsoft.Json;
|
2022-11-22 12:20:47 +00:00
|
|
|
using Newtonsoft.Json.Linq;
|
2016-11-16 22:11:05 +00:00
|
|
|
using RabbitMQ.Client;
|
2019-02-13 11:00:34 +00:00
|
|
|
using RabbitMQ.Client.Events;
|
2016-11-20 13:34:50 +00:00
|
|
|
using RabbitMQ.Client.Exceptions;
|
2016-12-11 14:08:58 +00:00
|
|
|
using Tapeti.Config;
|
2019-08-13 18:30:04 +00:00
|
|
|
using Tapeti.Default;
|
2019-01-24 21:52:21 +00:00
|
|
|
using Tapeti.Exceptions;
|
2022-11-17 15:47:07 +00:00
|
|
|
using Tapeti.Helpers;
|
2016-11-16 22:11:05 +00:00
|
|
|
|
|
|
|
namespace Tapeti.Connection
|
|
|
|
{
|
2021-07-18 11:27:10 +00:00
|
|
|
internal enum TapetiChannelType
|
|
|
|
{
|
|
|
|
Consume,
|
|
|
|
Publish
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
/// <summary>
|
|
|
|
/// Implementation of ITapetiClient for the RabbitMQ Client library
|
|
|
|
/// </summary>
|
2019-08-15 09:26:55 +00:00
|
|
|
internal class TapetiClient : ITapetiClient
|
2016-11-16 22:11:05 +00:00
|
|
|
{
|
2017-02-21 21:08:05 +00:00
|
|
|
private const int ReconnectDelay = 5000;
|
2020-03-17 13:57:27 +00:00
|
|
|
private const int MandatoryReturnTimeout = 300000;
|
2019-02-13 11:00:34 +00:00
|
|
|
private const int MinimumConnectedReconnectDelay = 1000;
|
2017-02-21 21:08:05 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
private readonly TapetiConnectionParams connectionParams;
|
|
|
|
|
|
|
|
private readonly ITapetiConfig config;
|
2019-01-08 15:36:52 +00:00
|
|
|
private readonly ILogger logger;
|
2019-08-13 18:30:04 +00:00
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Receives events when the connection state changes.
|
|
|
|
/// </summary>
|
2022-11-23 08:13:38 +00:00
|
|
|
public IConnectionEventListener? ConnectionEventListener { get; set; }
|
2016-11-16 22:11:05 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
private readonly TapetiChannel consumeChannel;
|
|
|
|
private readonly TapetiChannel publishChannel;
|
|
|
|
private readonly HttpClient managementClient;
|
2019-01-24 21:52:21 +00:00
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
// These fields must be locked using connectionLock
|
2022-11-17 15:47:07 +00:00
|
|
|
private readonly object connectionLock = new();
|
2021-09-21 14:17:09 +00:00
|
|
|
private long connectionReference;
|
2022-11-23 08:13:38 +00:00
|
|
|
private RabbitMQ.Client.IConnection? connection;
|
|
|
|
private IModel? consumeChannelModel;
|
|
|
|
private IModel? publishChannelModel;
|
2019-08-08 09:56:10 +00:00
|
|
|
private bool isClosing;
|
2019-02-13 11:00:34 +00:00
|
|
|
private bool isReconnect;
|
|
|
|
private DateTime connectedDateTime;
|
2019-05-20 13:22:40 +00:00
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
// These fields are for use in a single TapetiChannel's queue only!
|
|
|
|
private ulong lastDeliveryTag;
|
2022-11-17 15:47:07 +00:00
|
|
|
private readonly HashSet<string> deletedQueues = new();
|
2021-07-18 11:27:10 +00:00
|
|
|
|
|
|
|
// These fields must be locked using confirmLock, since the callbacks for BasicAck/BasicReturn can run in a different thread
|
2022-11-17 15:47:07 +00:00
|
|
|
private readonly object confirmLock = new();
|
|
|
|
private readonly Dictionary<ulong, ConfirmMessageInfo> confirmMessages = new();
|
|
|
|
private readonly Dictionary<string, ReturnInfo> returnRoutingKeys = new();
|
2019-02-13 11:00:34 +00:00
|
|
|
|
|
|
|
|
|
|
|
private class ConfirmMessageInfo
|
|
|
|
{
|
2022-11-23 08:13:38 +00:00
|
|
|
public string ReturnKey { get; }
|
|
|
|
public TaskCompletionSource<int> CompletionSource { get; }
|
|
|
|
|
|
|
|
|
|
|
|
public ConfirmMessageInfo(string returnKey, TaskCompletionSource<int> completionSource)
|
|
|
|
{
|
|
|
|
ReturnKey = returnKey;
|
|
|
|
CompletionSource = completionSource;
|
|
|
|
}
|
2019-02-13 11:00:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private class ReturnInfo
|
|
|
|
{
|
|
|
|
public uint RefCount;
|
|
|
|
public int FirstReplyCode;
|
|
|
|
}
|
|
|
|
|
2016-11-20 13:34:50 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
public TapetiClient(ITapetiConfig config, TapetiConnectionParams connectionParams)
|
2016-11-20 13:34:50 +00:00
|
|
|
{
|
2017-02-12 20:43:30 +00:00
|
|
|
this.config = config;
|
2019-08-13 18:30:04 +00:00
|
|
|
this.connectionParams = connectionParams;
|
2017-02-07 15:13:33 +00:00
|
|
|
|
2019-01-08 15:36:52 +00:00
|
|
|
logger = config.DependencyResolver.Resolve<ILogger>();
|
2016-11-17 16:33:27 +00:00
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
consumeChannel = new TapetiChannel(() => GetModel(TapetiChannelType.Consume));
|
|
|
|
publishChannel = new TapetiChannel(() => GetModel(TapetiChannelType.Publish));
|
|
|
|
|
2016-11-17 16:33:27 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
var handler = new HttpClientHandler
|
|
|
|
{
|
|
|
|
Credentials = new NetworkCredential(connectionParams.Username, connectionParams.Password)
|
|
|
|
};
|
2016-11-20 13:34:50 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
managementClient = new HttpClient(handler)
|
|
|
|
{
|
|
|
|
Timeout = TimeSpan.FromSeconds(30)
|
|
|
|
};
|
2017-01-31 11:01:08 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
managementClient.DefaultRequestHeaders.Add("Connection", "close");
|
2016-11-17 16:33:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
/// <inheritdoc />
|
2022-11-23 08:13:38 +00:00
|
|
|
public async Task Publish(byte[] body, IMessageProperties properties, string? exchange, string routingKey, bool mandatory)
|
2016-11-17 16:33:27 +00:00
|
|
|
{
|
2019-08-14 18:48:40 +00:00
|
|
|
if (string.IsNullOrEmpty(routingKey))
|
|
|
|
throw new ArgumentNullException(nameof(routingKey));
|
2017-02-12 14:18:12 +00:00
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
|
|
|
|
await GetTapetiChannel(TapetiChannelType.Publish).QueueWithProvider(async channelProvider =>
|
2016-11-20 13:34:50 +00:00
|
|
|
{
|
2022-11-23 08:13:38 +00:00
|
|
|
Task<int>? publishResultTask = null;
|
|
|
|
var messageInfo = new ConfirmMessageInfo(GetReturnKey(exchange ?? string.Empty, routingKey), new TaskCompletionSource<int>());
|
2016-11-16 22:11:05 +00:00
|
|
|
|
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
channelProvider.WithRetryableChannel(channel =>
|
2016-12-11 14:08:58 +00:00
|
|
|
{
|
2022-11-23 08:13:38 +00:00
|
|
|
if (exchange != null)
|
|
|
|
DeclareExchange(channel, exchange);
|
2019-08-15 13:55:45 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
// The delivery tag is lost after a reconnect, register under the new tag
|
|
|
|
if (config.Features.PublisherConfirms)
|
2016-12-11 14:08:58 +00:00
|
|
|
{
|
2019-08-13 18:30:04 +00:00
|
|
|
lastDeliveryTag++;
|
2019-02-13 11:00:34 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
Monitor.Enter(confirmLock);
|
|
|
|
try
|
2017-02-15 21:05:01 +00:00
|
|
|
{
|
2019-08-13 18:30:04 +00:00
|
|
|
confirmMessages.Add(lastDeliveryTag, messageInfo);
|
2019-02-13 11:00:34 +00:00
|
|
|
}
|
2019-08-13 18:30:04 +00:00
|
|
|
finally
|
2019-02-13 11:00:34 +00:00
|
|
|
{
|
2019-08-13 18:30:04 +00:00
|
|
|
Monitor.Exit(confirmLock);
|
2019-02-13 11:00:34 +00:00
|
|
|
}
|
2019-08-13 18:30:04 +00:00
|
|
|
|
|
|
|
publishResultTask = messageInfo.CompletionSource.Task;
|
2017-07-21 12:14:19 +00:00
|
|
|
}
|
2019-08-13 18:30:04 +00:00
|
|
|
else
|
|
|
|
mandatory = false;
|
|
|
|
|
2019-08-14 18:48:40 +00:00
|
|
|
try
|
|
|
|
{
|
|
|
|
var publishProperties = new RabbitMQMessageProperties(channel.CreateBasicProperties(), properties);
|
2022-11-23 08:13:38 +00:00
|
|
|
channel.BasicPublish(exchange ?? string.Empty, routingKey, mandatory, publishProperties.BasicProperties, body);
|
2019-08-14 18:48:40 +00:00
|
|
|
}
|
|
|
|
catch
|
|
|
|
{
|
|
|
|
messageInfo.CompletionSource.SetCanceled();
|
|
|
|
publishResultTask = null;
|
|
|
|
|
|
|
|
throw;
|
|
|
|
}
|
2019-08-13 18:30:04 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (publishResultTask == null)
|
|
|
|
return;
|
|
|
|
|
|
|
|
var delayCancellationTokenSource = new CancellationTokenSource();
|
|
|
|
var signalledTask = await Task.WhenAny(
|
|
|
|
publishResultTask,
|
|
|
|
Task.Delay(MandatoryReturnTimeout, delayCancellationTokenSource.Token));
|
|
|
|
|
|
|
|
if (signalledTask != publishResultTask)
|
|
|
|
throw new TimeoutException(
|
|
|
|
$"Timeout while waiting for basic.return for message with exchange '{exchange}' and routing key '{routingKey}'");
|
|
|
|
|
|
|
|
delayCancellationTokenSource.Cancel();
|
|
|
|
|
|
|
|
if (publishResultTask.IsCanceled)
|
|
|
|
throw new NackException(
|
|
|
|
$"Mandatory message with with exchange '{exchange}' and routing key '{routingKey}' was nacked");
|
|
|
|
|
|
|
|
var replyCode = publishResultTask.Result;
|
|
|
|
|
2022-11-22 12:20:47 +00:00
|
|
|
switch (replyCode)
|
|
|
|
{
|
|
|
|
// There is no RabbitMQ.Client.Framing.Constants value for this "No route" reply code
|
|
|
|
// at the time of writing...
|
|
|
|
case 312:
|
|
|
|
throw new NoRouteException(
|
|
|
|
$"Mandatory message with exchange '{exchange}' and routing key '{routingKey}' does not have a route");
|
|
|
|
|
|
|
|
case > 0:
|
|
|
|
throw new NoRouteException(
|
|
|
|
$"Mandatory message with exchange '{exchange}' and routing key '{routingKey}' could not be delivered, reply code: {replyCode}");
|
|
|
|
}
|
2019-08-13 18:30:04 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
2022-11-23 08:13:38 +00:00
|
|
|
public async Task<TapetiConsumerTag?> Consume(string queueName, IConsumer consumer, CancellationToken cancellationToken)
|
2019-08-13 18:30:04 +00:00
|
|
|
{
|
2019-08-20 09:47:53 +00:00
|
|
|
if (deletedQueues.Contains(queueName))
|
2021-01-15 08:57:46 +00:00
|
|
|
return null;
|
2019-08-20 09:47:53 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
if (string.IsNullOrEmpty(queueName))
|
|
|
|
throw new ArgumentNullException(nameof(queueName));
|
|
|
|
|
2019-10-10 14:03:12 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
long capturedConnectionReference = -1;
|
2022-11-23 08:13:38 +00:00
|
|
|
string? consumerTag = null;
|
2021-01-15 08:57:46 +00:00
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
await GetTapetiChannel(TapetiChannelType.Consume).QueueRetryable(channel =>
|
2019-10-10 14:03:12 +00:00
|
|
|
{
|
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
|
|
return;
|
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
capturedConnectionReference = Interlocked.Read(ref connectionReference);
|
|
|
|
var basicConsumer = new TapetiBasicConsumer(consumer, capturedConnectionReference, Respond);
|
2021-01-15 08:57:46 +00:00
|
|
|
consumerTag = channel.BasicConsume(queueName, false, basicConsumer);
|
|
|
|
});
|
|
|
|
|
2022-11-23 08:13:38 +00:00
|
|
|
return consumerTag == null
|
|
|
|
? null
|
|
|
|
: new TapetiConsumerTag(capturedConnectionReference, consumerTag);
|
2021-01-15 08:57:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
2021-09-21 14:17:09 +00:00
|
|
|
public async Task Cancel(TapetiConsumerTag consumerTag)
|
2021-01-15 08:57:46 +00:00
|
|
|
{
|
2021-09-21 14:17:09 +00:00
|
|
|
if (isClosing || string.IsNullOrEmpty(consumerTag.ConsumerTag))
|
|
|
|
return;
|
|
|
|
|
|
|
|
var capturedConnectionReference = Interlocked.Read(ref connectionReference);
|
|
|
|
|
|
|
|
// If the connection was re-established in the meantime, don't respond with an
|
|
|
|
// invalid deliveryTag. The message will be requeued.
|
|
|
|
if (capturedConnectionReference != consumerTag.ConnectionReference)
|
2021-01-15 08:57:46 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
// No need for a retryable channel here, if the connection is lost
|
|
|
|
// so is the consumer.
|
2021-07-18 11:27:10 +00:00
|
|
|
await GetTapetiChannel(TapetiChannelType.Consume).Queue(channel =>
|
2021-01-15 08:57:46 +00:00
|
|
|
{
|
2021-09-21 14:17:09 +00:00
|
|
|
// Check again as a reconnect may have occured in the meantime
|
|
|
|
var currentConnectionReference = Interlocked.Read(ref connectionReference);
|
|
|
|
if (currentConnectionReference != consumerTag.ConnectionReference)
|
|
|
|
return;
|
|
|
|
|
|
|
|
channel.BasicCancel(consumerTag.ConsumerTag);
|
2019-01-25 13:52:09 +00:00
|
|
|
});
|
2016-11-20 13:34:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
private async Task Respond(long expectedConnectionReference, ulong deliveryTag, ConsumeResult result)
|
2016-11-20 13:34:50 +00:00
|
|
|
{
|
2021-07-18 11:27:10 +00:00
|
|
|
await GetTapetiChannel(TapetiChannelType.Consume).Queue(channel =>
|
2016-11-16 22:11:05 +00:00
|
|
|
{
|
2021-09-21 14:17:09 +00:00
|
|
|
// If the connection was re-established in the meantime, don't respond with an
|
|
|
|
// invalid deliveryTag. The message will be requeued.
|
|
|
|
var currentConnectionReference = Interlocked.Read(ref connectionReference);
|
|
|
|
if (currentConnectionReference != connectionReference)
|
|
|
|
return;
|
|
|
|
|
2019-02-13 11:00:34 +00:00
|
|
|
// No need for a retryable channel here, if the connection is lost we can't
|
|
|
|
// use the deliveryTag anymore.
|
2019-08-14 10:20:53 +00:00
|
|
|
switch (result)
|
2016-11-20 13:34:50 +00:00
|
|
|
{
|
2019-08-14 10:20:53 +00:00
|
|
|
case ConsumeResult.Success:
|
|
|
|
case ConsumeResult.ExternalRequeue:
|
2021-01-15 08:57:46 +00:00
|
|
|
channel.BasicAck(deliveryTag, false);
|
2016-11-20 13:34:50 +00:00
|
|
|
break;
|
|
|
|
|
2019-08-14 10:20:53 +00:00
|
|
|
case ConsumeResult.Error:
|
2021-01-15 08:57:46 +00:00
|
|
|
channel.BasicNack(deliveryTag, false, false);
|
2016-11-20 13:34:50 +00:00
|
|
|
break;
|
|
|
|
|
2019-08-14 10:20:53 +00:00
|
|
|
case ConsumeResult.Requeue:
|
2021-01-15 08:57:46 +00:00
|
|
|
channel.BasicNack(deliveryTag, false, true);
|
2016-11-20 13:34:50 +00:00
|
|
|
break;
|
2019-02-13 11:00:34 +00:00
|
|
|
|
|
|
|
default:
|
2019-08-14 10:20:53 +00:00
|
|
|
throw new ArgumentOutOfRangeException(nameof(result), result, null);
|
2016-11-20 13:34:50 +00:00
|
|
|
}
|
2019-01-25 13:52:09 +00:00
|
|
|
});
|
2016-11-20 13:34:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-11-23 08:13:38 +00:00
|
|
|
private async Task<bool> GetDurableQueueDeclareRequired(string queueName, IRabbitMQArguments? arguments)
|
2021-09-02 11:58:01 +00:00
|
|
|
{
|
|
|
|
var existingQueue = await GetQueueInfo(queueName);
|
|
|
|
if (existingQueue == null)
|
|
|
|
return true;
|
|
|
|
|
|
|
|
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)");
|
|
|
|
|
2022-11-22 12:20:47 +00:00
|
|
|
var existingArguments = ConvertJsonArguments(existingQueue.Arguments);
|
|
|
|
if (existingArguments.NullSafeSameValues(arguments))
|
2022-11-17 15:47:07 +00:00
|
|
|
return true;
|
|
|
|
|
2022-11-22 12:20:47 +00:00
|
|
|
(logger as IBindingLogger)?.QueueExistsWarning(queueName, existingArguments, arguments);
|
2021-09-02 11:58:01 +00:00
|
|
|
return false;
|
|
|
|
}
|
2022-11-17 15:47:07 +00:00
|
|
|
|
|
|
|
|
2022-11-23 08:13:38 +00:00
|
|
|
private static RabbitMQArguments? ConvertJsonArguments(IReadOnlyDictionary<string, JObject>? arguments)
|
2022-11-22 12:20:47 +00:00
|
|
|
{
|
|
|
|
if (arguments == null)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
var result = new RabbitMQArguments();
|
|
|
|
foreach (var pair in arguments)
|
|
|
|
{
|
|
|
|
// ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault - by design
|
|
|
|
object value = pair.Value.Type switch
|
|
|
|
{
|
|
|
|
JTokenType.Integer => pair.Value.Value<int>(),
|
|
|
|
JTokenType.Float => pair.Value.Value<double>(),
|
|
|
|
JTokenType.String => Encoding.UTF8.GetBytes(pair.Value.Value<string>() ?? string.Empty),
|
|
|
|
JTokenType.Boolean => pair.Value.Value<bool>(),
|
|
|
|
_ => throw new ArgumentOutOfRangeException(nameof(arguments))
|
|
|
|
};
|
|
|
|
|
|
|
|
result.Add(pair.Key, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-09-02 11:58:01 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
/// <inheritdoc />
|
2022-11-23 08:13:38 +00:00
|
|
|
public async Task DurableQueueDeclare(string queueName, IEnumerable<QueueBinding> bindings, IRabbitMQArguments? arguments, CancellationToken cancellationToken)
|
2019-08-13 18:30:04 +00:00
|
|
|
{
|
2022-11-17 15:47:07 +00:00
|
|
|
var declareRequired = await GetDurableQueueDeclareRequired(queueName, arguments);
|
2021-09-02 11:58:01 +00:00
|
|
|
|
2019-10-10 14:03:12 +00:00
|
|
|
var existingBindings = (await GetQueueBindings(queueName)).ToList();
|
|
|
|
var currentBindings = bindings.ToList();
|
2020-03-05 09:27:46 +00:00
|
|
|
var bindingLogger = logger as IBindingLogger;
|
2019-10-10 14:03:12 +00:00
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
await GetTapetiChannel(TapetiChannelType.Consume).Queue(channel =>
|
2019-08-13 18:30:04 +00:00
|
|
|
{
|
2019-10-10 14:03:12 +00:00
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
|
|
return;
|
2019-08-13 18:30:04 +00:00
|
|
|
|
2021-09-02 11:58:01 +00:00
|
|
|
if (declareRequired)
|
|
|
|
{
|
|
|
|
bindingLogger?.QueueDeclare(queueName, true, false);
|
2022-11-17 15:47:07 +00:00
|
|
|
channel.QueueDeclare(queueName, true, false, false, GetDeclareArguments(arguments));
|
2021-09-02 11:58:01 +00:00
|
|
|
}
|
2020-03-05 09:27:46 +00:00
|
|
|
|
2019-10-10 14:03:12 +00:00
|
|
|
foreach (var binding in currentBindings.Except(existingBindings))
|
|
|
|
{
|
|
|
|
DeclareExchange(channel, binding.Exchange);
|
2020-03-05 09:27:46 +00:00
|
|
|
bindingLogger?.QueueBind(queueName, true, binding.Exchange, binding.RoutingKey);
|
2019-10-10 14:03:12 +00:00
|
|
|
channel.QueueBind(queueName, binding.Exchange, binding.RoutingKey);
|
|
|
|
}
|
2019-08-13 18:30:04 +00:00
|
|
|
|
2019-10-10 14:03:12 +00:00
|
|
|
foreach (var deletedBinding in existingBindings.Except(currentBindings))
|
2020-03-05 09:27:46 +00:00
|
|
|
{
|
|
|
|
bindingLogger?.QueueUnbind(queueName, deletedBinding.Exchange, deletedBinding.RoutingKey);
|
2019-10-10 14:03:12 +00:00
|
|
|
channel.QueueUnbind(queueName, deletedBinding.Exchange, deletedBinding.RoutingKey);
|
2020-03-05 09:27:46 +00:00
|
|
|
}
|
2019-08-13 18:30:04 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-11-17 15:47:07 +00:00
|
|
|
|
2022-11-23 08:13:38 +00:00
|
|
|
private static IDictionary<string, object>? GetDeclareArguments(IRabbitMQArguments? arguments)
|
2022-11-17 15:47:07 +00:00
|
|
|
{
|
2022-11-22 12:20:47 +00:00
|
|
|
return arguments == null || arguments.Count == 0
|
|
|
|
? null
|
|
|
|
: arguments.ToDictionary(p => p.Key, p => p.Value);
|
2022-11-17 15:47:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
/// <inheritdoc />
|
2022-11-23 08:13:38 +00:00
|
|
|
public async Task DurableQueueVerify(string queueName, IRabbitMQArguments? arguments, CancellationToken cancellationToken)
|
2019-08-13 18:30:04 +00:00
|
|
|
{
|
2022-11-17 15:47:07 +00:00
|
|
|
if (!await GetDurableQueueDeclareRequired(queueName, arguments))
|
2021-09-02 11:58:01 +00:00
|
|
|
return;
|
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
await GetTapetiChannel(TapetiChannelType.Consume).Queue(channel =>
|
2019-08-20 09:47:53 +00:00
|
|
|
{
|
2019-10-10 14:03:12 +00:00
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
|
|
return;
|
|
|
|
|
2020-03-05 09:27:46 +00:00
|
|
|
(logger as IBindingLogger)?.QueueDeclare(queueName, true, true);
|
2019-08-20 09:47:53 +00:00
|
|
|
channel.QueueDeclarePassive(queueName);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
2022-11-17 15:47:07 +00:00
|
|
|
public async Task DurableQueueDelete(string queueName, bool onlyIfEmpty, CancellationToken cancellationToken)
|
2019-08-20 09:47:53 +00:00
|
|
|
{
|
|
|
|
if (!onlyIfEmpty)
|
|
|
|
{
|
|
|
|
uint deletedMessages = 0;
|
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
await GetTapetiChannel(TapetiChannelType.Consume).Queue(channel =>
|
2019-08-13 18:30:04 +00:00
|
|
|
{
|
2019-10-10 14:03:12 +00:00
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
|
|
return;
|
|
|
|
|
2019-08-20 09:47:53 +00:00
|
|
|
deletedMessages = channel.QueueDelete(queueName);
|
2019-08-13 18:30:04 +00:00
|
|
|
});
|
2019-08-20 09:47:53 +00:00
|
|
|
|
|
|
|
deletedQueues.Add(queueName);
|
2020-03-05 09:27:46 +00:00
|
|
|
(logger as IBindingLogger)?.QueueObsolete(queueName, true, deletedMessages);
|
2019-08-20 09:47:53 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
await GetTapetiChannel(TapetiChannelType.Consume).QueueWithProvider(async channelProvider =>
|
2019-08-20 09:47:53 +00:00
|
|
|
{
|
|
|
|
bool retry;
|
|
|
|
do
|
|
|
|
{
|
2019-10-10 14:03:12 +00:00
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
|
|
break;
|
|
|
|
|
2019-08-20 09:47:53 +00:00
|
|
|
retry = false;
|
|
|
|
|
|
|
|
// Get queue information from the Management API, since the AMQP operations will
|
|
|
|
// throw an error if the queue does not exist or still contains messages and resets
|
|
|
|
// the connection. The resulting reconnect will cause subscribers to reset.
|
|
|
|
var queueInfo = await GetQueueInfo(queueName);
|
|
|
|
if (queueInfo == null)
|
|
|
|
{
|
|
|
|
deletedQueues.Add(queueName);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (queueInfo.Messages == 0)
|
|
|
|
{
|
|
|
|
// Still pass onlyIfEmpty to prevent concurrency issues if a message arrived between
|
|
|
|
// the call to the Management API and deleting the queue. Because the QueueWithRetryableChannel
|
|
|
|
// includes the GetQueueInfo, the next time around it should have Messages > 0
|
|
|
|
try
|
|
|
|
{
|
2021-07-18 11:27:10 +00:00
|
|
|
channelProvider.WithChannel(channel =>
|
|
|
|
{
|
|
|
|
channel.QueueDelete(queueName, false, true);
|
|
|
|
});
|
2019-08-20 09:47:53 +00:00
|
|
|
|
|
|
|
deletedQueues.Add(queueName);
|
2020-03-05 09:27:46 +00:00
|
|
|
(logger as IBindingLogger)?.QueueObsolete(queueName, true, 0);
|
2019-08-20 09:47:53 +00:00
|
|
|
}
|
|
|
|
catch (OperationInterruptedException e)
|
|
|
|
{
|
2021-05-29 19:51:58 +00:00
|
|
|
if (e.ShutdownReason.ReplyCode == Constants.PreconditionFailed)
|
2019-08-20 09:47:53 +00:00
|
|
|
retry = true;
|
|
|
|
else
|
|
|
|
throw;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Remove all bindings instead
|
|
|
|
var existingBindings = (await GetQueueBindings(queueName)).ToList();
|
|
|
|
|
|
|
|
if (existingBindings.Count > 0)
|
|
|
|
{
|
2021-07-18 11:27:10 +00:00
|
|
|
channelProvider.WithChannel(channel =>
|
|
|
|
{
|
|
|
|
foreach (var binding in existingBindings)
|
|
|
|
channel.QueueUnbind(queueName, binding.Exchange, binding.RoutingKey);
|
|
|
|
});
|
2019-08-20 09:47:53 +00:00
|
|
|
}
|
|
|
|
|
2020-03-05 09:27:46 +00:00
|
|
|
(logger as IBindingLogger)?.QueueObsolete(queueName, false, queueInfo.Messages);
|
2019-08-20 09:47:53 +00:00
|
|
|
}
|
|
|
|
} while (retry);
|
2019-08-13 18:30:04 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-08-20 09:47:53 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
/// <inheritdoc />
|
2022-11-23 08:13:38 +00:00
|
|
|
public async Task<string> DynamicQueueDeclare(string? queuePrefix, IRabbitMQArguments? arguments, CancellationToken cancellationToken)
|
2019-08-13 18:30:04 +00:00
|
|
|
{
|
2022-11-23 08:13:38 +00:00
|
|
|
string? queueName = null;
|
2020-03-05 09:27:46 +00:00
|
|
|
var bindingLogger = logger as IBindingLogger;
|
2019-08-13 18:30:04 +00:00
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
await GetTapetiChannel(TapetiChannelType.Consume).Queue(channel =>
|
2019-08-13 18:30:04 +00:00
|
|
|
{
|
2019-10-10 14:03:12 +00:00
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
|
|
return;
|
|
|
|
|
2019-08-20 09:47:53 +00:00
|
|
|
if (!string.IsNullOrEmpty(queuePrefix))
|
2019-08-13 18:30:04 +00:00
|
|
|
{
|
2019-08-20 09:47:53 +00:00
|
|
|
queueName = queuePrefix + "." + Guid.NewGuid().ToString("N");
|
2020-03-05 09:27:46 +00:00
|
|
|
bindingLogger?.QueueDeclare(queueName, false, false);
|
2022-11-17 15:47:07 +00:00
|
|
|
channel.QueueDeclare(queueName, arguments: GetDeclareArguments(arguments));
|
2019-08-20 09:47:53 +00:00
|
|
|
}
|
|
|
|
else
|
2020-03-05 09:27:46 +00:00
|
|
|
{
|
2022-11-17 15:47:07 +00:00
|
|
|
queueName = channel.QueueDeclare(arguments: GetDeclareArguments(arguments)).QueueName;
|
2020-03-05 09:27:46 +00:00
|
|
|
bindingLogger?.QueueDeclare(queueName, false, false);
|
|
|
|
}
|
2019-08-13 18:30:04 +00:00
|
|
|
});
|
|
|
|
|
2022-11-23 08:13:38 +00:00
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
if (queueName == null)
|
|
|
|
throw new InvalidOperationException("Failed to declare dynamic queue");
|
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
return queueName;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
2022-11-17 15:47:07 +00:00
|
|
|
public async Task DynamicQueueBind(string queueName, QueueBinding binding, CancellationToken cancellationToken)
|
2019-08-13 18:30:04 +00:00
|
|
|
{
|
2021-07-18 11:27:10 +00:00
|
|
|
await GetTapetiChannel(TapetiChannelType.Consume).Queue(channel =>
|
2019-08-13 18:30:04 +00:00
|
|
|
{
|
2019-10-10 14:03:12 +00:00
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
|
|
return;
|
|
|
|
|
2020-03-05 09:27:46 +00:00
|
|
|
DeclareExchange(channel, binding.Exchange);
|
|
|
|
(logger as IBindingLogger)?.QueueBind(queueName, false, binding.Exchange, binding.RoutingKey);
|
|
|
|
channel.QueueBind(queueName, binding.Exchange, binding.RoutingKey);
|
2019-08-13 18:30:04 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
public async Task Close()
|
2016-11-20 13:34:50 +00:00
|
|
|
{
|
2022-11-23 08:13:38 +00:00
|
|
|
IModel? capturedConsumeModel;
|
|
|
|
IModel? capturedPublishModel;
|
|
|
|
RabbitMQ.Client.IConnection? capturedConnection;
|
2016-11-16 22:11:05 +00:00
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
lock (connectionLock)
|
2016-11-20 13:34:50 +00:00
|
|
|
{
|
2019-08-08 09:56:10 +00:00
|
|
|
isClosing = true;
|
2021-07-18 11:27:10 +00:00
|
|
|
capturedConsumeModel = consumeChannelModel;
|
|
|
|
capturedPublishModel = publishChannelModel;
|
|
|
|
capturedConnection = connection;
|
2019-08-08 09:56:10 +00:00
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
consumeChannelModel = null;
|
|
|
|
publishChannelModel = null;
|
|
|
|
connection = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Empty the queue
|
|
|
|
await consumeChannel.Reset();
|
|
|
|
await publishChannel.Reset();
|
|
|
|
|
|
|
|
// No need to close the channels as the connection will be closed
|
2021-09-21 14:17:09 +00:00
|
|
|
capturedConsumeModel?.Dispose();
|
|
|
|
capturedPublishModel?.Dispose();
|
2021-07-18 11:27:10 +00:00
|
|
|
|
|
|
|
// ReSharper disable once InvertIf
|
|
|
|
if (capturedConnection != null)
|
|
|
|
{
|
|
|
|
try
|
2016-11-20 13:34:50 +00:00
|
|
|
{
|
2021-07-18 11:27:10 +00:00
|
|
|
capturedConnection.Close();
|
2016-11-20 13:34:50 +00:00
|
|
|
}
|
2021-07-18 11:27:10 +00:00
|
|
|
finally
|
2016-11-20 13:34:50 +00:00
|
|
|
{
|
2021-07-18 11:27:10 +00:00
|
|
|
capturedConnection.Dispose();
|
2016-11-20 13:34:50 +00:00
|
|
|
}
|
2021-07-18 11:27:10 +00:00
|
|
|
}
|
2016-11-16 22:11:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-11-17 15:47:07 +00:00
|
|
|
private static readonly List<HttpStatusCode> TransientStatusCodes = new()
|
2017-01-31 11:01:08 +00:00
|
|
|
{
|
2019-08-13 18:30:04 +00:00
|
|
|
HttpStatusCode.GatewayTimeout,
|
|
|
|
HttpStatusCode.RequestTimeout,
|
|
|
|
HttpStatusCode.ServiceUnavailable
|
|
|
|
};
|
2017-01-31 11:01:08 +00:00
|
|
|
|
2019-08-20 09:47:53 +00:00
|
|
|
|
|
|
|
private class ManagementQueueInfo
|
2019-08-13 18:30:04 +00:00
|
|
|
{
|
2021-09-02 11:58:01 +00:00
|
|
|
[JsonProperty("name")]
|
2022-11-23 08:13:38 +00:00
|
|
|
public string? Name { get; set; }
|
2021-09-02 11:58:01 +00:00
|
|
|
|
|
|
|
[JsonProperty("vhost")]
|
2022-11-23 08:13:38 +00:00
|
|
|
public string? VHost { get; set; }
|
2021-09-02 11:58:01 +00:00
|
|
|
|
|
|
|
[JsonProperty("durable")]
|
|
|
|
public bool Durable { get; set; }
|
|
|
|
|
|
|
|
[JsonProperty("auto_delete")]
|
|
|
|
public bool AutoDelete { get; set; }
|
|
|
|
|
|
|
|
[JsonProperty("exclusive")]
|
|
|
|
public bool Exclusive { get; set; }
|
|
|
|
|
|
|
|
[JsonProperty("arguments")]
|
2022-11-23 08:13:38 +00:00
|
|
|
public Dictionary<string, JObject>? Arguments { get; set; }
|
2021-09-02 11:58:01 +00:00
|
|
|
|
2019-08-20 09:47:53 +00:00
|
|
|
[JsonProperty("messages")]
|
|
|
|
public uint Messages { get; set; }
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2022-11-23 08:13:38 +00:00
|
|
|
private async Task<ManagementQueueInfo?> GetQueueInfo(string queueName)
|
2019-08-20 09:47:53 +00:00
|
|
|
{
|
|
|
|
var virtualHostPath = Uri.EscapeDataString(connectionParams.VirtualHost);
|
|
|
|
var queuePath = Uri.EscapeDataString(queueName);
|
|
|
|
|
|
|
|
return await WithRetryableManagementAPI($"queues/{virtualHostPath}/{queuePath}", async response =>
|
|
|
|
{
|
|
|
|
if (response.StatusCode == HttpStatusCode.NotFound)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
|
|
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
|
|
return JsonConvert.DeserializeObject<ManagementQueueInfo>(content);
|
|
|
|
});
|
|
|
|
}
|
2019-08-13 18:30:04 +00:00
|
|
|
|
|
|
|
|
|
|
|
private class ManagementBinding
|
|
|
|
{
|
|
|
|
[JsonProperty("source")]
|
2022-11-23 08:13:38 +00:00
|
|
|
public string? Source { get; set; }
|
2017-01-31 11:01:08 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
[JsonProperty("vhost")]
|
2022-11-23 08:13:38 +00:00
|
|
|
public string? Vhost { get; set; }
|
2017-01-31 11:01:08 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
[JsonProperty("destination")]
|
2022-11-23 08:13:38 +00:00
|
|
|
public string? Destination { get; set; }
|
2019-05-20 13:22:40 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
[JsonProperty("destination_type")]
|
2022-11-23 08:13:38 +00:00
|
|
|
public string? DestinationType { get; set; }
|
2019-02-13 11:00:34 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
[JsonProperty("routing_key")]
|
2022-11-23 08:13:38 +00:00
|
|
|
public string? RoutingKey { get; set; }
|
2019-01-24 21:52:21 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
[JsonProperty("arguments")]
|
2022-11-23 08:13:38 +00:00
|
|
|
public Dictionary<string, string>? Arguments { get; set; }
|
2019-02-13 11:00:34 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
[JsonProperty("properties_key")]
|
2022-11-23 08:13:38 +00:00
|
|
|
public string? PropertiesKey { get; set; }
|
2019-08-13 18:30:04 +00:00
|
|
|
}
|
2019-01-24 21:52:21 +00:00
|
|
|
|
2021-09-02 11:58:01 +00:00
|
|
|
|
2019-08-13 18:30:04 +00:00
|
|
|
private async Task<IEnumerable<QueueBinding>> GetQueueBindings(string queueName)
|
|
|
|
{
|
|
|
|
var virtualHostPath = Uri.EscapeDataString(connectionParams.VirtualHost);
|
|
|
|
var queuePath = Uri.EscapeDataString(queueName);
|
2019-08-20 09:47:53 +00:00
|
|
|
|
|
|
|
return await WithRetryableManagementAPI($"queues/{virtualHostPath}/{queuePath}/bindings", async response =>
|
|
|
|
{
|
|
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
|
|
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
|
|
var bindings = JsonConvert.DeserializeObject<IEnumerable<ManagementBinding>>(content);
|
|
|
|
|
|
|
|
// Filter out the binding to an empty source, which is always present for direct-to-queue routing
|
2021-05-29 19:51:58 +00:00
|
|
|
return bindings?
|
2022-11-23 08:13:38 +00:00
|
|
|
.Where(binding => !string.IsNullOrEmpty(binding.Source) && !string.IsNullOrEmpty(binding.RoutingKey))
|
|
|
|
.Select(binding => new QueueBinding(binding.Source!, binding.RoutingKey!))
|
2021-05-29 19:51:58 +00:00
|
|
|
?? Enumerable.Empty<QueueBinding>();
|
2019-08-20 09:47:53 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly TimeSpan[] ExponentialBackoff =
|
|
|
|
{
|
|
|
|
TimeSpan.FromSeconds(1),
|
|
|
|
TimeSpan.FromSeconds(2),
|
|
|
|
TimeSpan.FromSeconds(3),
|
|
|
|
TimeSpan.FromSeconds(5),
|
|
|
|
TimeSpan.FromSeconds(8),
|
|
|
|
TimeSpan.FromSeconds(13),
|
|
|
|
TimeSpan.FromSeconds(21),
|
|
|
|
TimeSpan.FromSeconds(34),
|
|
|
|
TimeSpan.FromSeconds(55)
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<T> WithRetryableManagementAPI<T>(string path, Func<HttpResponseMessage, Task<T>> handleResponse)
|
|
|
|
{
|
2021-09-02 11:58:01 +00:00
|
|
|
// Workaround for: https://github.com/dotnet/runtime/issues/23581#issuecomment-354391321
|
|
|
|
// "localhost" can cause a 1 second delay *per call*. Not an issue in production scenarios, but annoying while debugging.
|
|
|
|
var hostName = connectionParams.HostName;
|
|
|
|
if (hostName.Equals("localhost", StringComparison.InvariantCultureIgnoreCase))
|
|
|
|
hostName = "127.0.0.1";
|
|
|
|
|
|
|
|
var requestUri = new Uri($"http://{hostName}:{connectionParams.ManagementPort}/api/{path}");
|
2019-01-24 21:52:21 +00:00
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
|
|
|
var retryDelayIndex = 0;
|
2019-02-13 11:00:34 +00:00
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
while (true)
|
|
|
|
{
|
|
|
|
try
|
2019-08-13 18:30:04 +00:00
|
|
|
{
|
2021-07-18 11:27:10 +00:00
|
|
|
var response = await managementClient.SendAsync(request);
|
|
|
|
return await handleResponse(response);
|
|
|
|
}
|
|
|
|
catch (TimeoutException)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
catch (WebException e)
|
|
|
|
{
|
2022-11-22 12:20:47 +00:00
|
|
|
if (e.Response is not HttpWebResponse response)
|
2021-07-18 11:27:10 +00:00
|
|
|
throw;
|
2019-01-24 21:52:21 +00:00
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
if (!TransientStatusCodes.Contains(response.StatusCode))
|
|
|
|
throw;
|
|
|
|
}
|
2019-01-24 21:52:21 +00:00
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
await Task.Delay(ExponentialBackoff[retryDelayIndex]);
|
2019-01-25 13:52:09 +00:00
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
if (retryDelayIndex < ExponentialBackoff.Length - 1)
|
|
|
|
retryDelayIndex++;
|
2019-08-13 18:30:04 +00:00
|
|
|
}
|
2017-01-31 11:01:08 +00:00
|
|
|
}
|
|
|
|
|
2019-02-13 11:00:34 +00:00
|
|
|
|
2022-11-17 15:47:07 +00:00
|
|
|
private readonly HashSet<string> declaredExchanges = new();
|
2019-08-15 13:55:45 +00:00
|
|
|
|
|
|
|
private void DeclareExchange(IModel channel, string exchange)
|
|
|
|
{
|
|
|
|
if (declaredExchanges.Contains(exchange))
|
|
|
|
return;
|
|
|
|
|
2020-03-05 09:27:46 +00:00
|
|
|
(logger as IBindingLogger)?.ExchangeDeclare(exchange);
|
2019-08-15 13:55:45 +00:00
|
|
|
channel.ExchangeDeclare(exchange, "topic", true);
|
|
|
|
declaredExchanges.Add(exchange);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
private TapetiChannel GetTapetiChannel(TapetiChannelType channelType)
|
2019-08-20 09:47:53 +00:00
|
|
|
{
|
2021-07-18 11:27:10 +00:00
|
|
|
return channelType == TapetiChannelType.Publish
|
|
|
|
? publishChannel
|
|
|
|
: consumeChannel;
|
2019-08-20 09:47:53 +00:00
|
|
|
}
|
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
|
2016-11-20 13:34:50 +00:00
|
|
|
/// <remarks>
|
|
|
|
/// Only call this from a task in the taskQueue to ensure IModel is only used
|
|
|
|
/// by a single thread, as is recommended in the RabbitMQ .NET Client documentation.
|
|
|
|
/// </remarks>
|
2021-07-18 11:27:10 +00:00
|
|
|
private IModel GetModel(TapetiChannelType channelType)
|
2016-11-16 22:11:05 +00:00
|
|
|
{
|
2021-07-18 11:27:10 +00:00
|
|
|
lock (connectionLock)
|
2019-02-13 11:00:34 +00:00
|
|
|
{
|
2021-07-18 11:27:10 +00:00
|
|
|
var channel = channelType == TapetiChannelType.Publish
|
|
|
|
? publishChannelModel
|
|
|
|
: consumeChannelModel;
|
2019-02-13 11:00:34 +00:00
|
|
|
|
2022-02-09 08:19:56 +00:00
|
|
|
if (channel is { IsOpen: true })
|
2021-07-18 11:27:10 +00:00
|
|
|
return channel;
|
2021-09-21 14:17:09 +00:00
|
|
|
}
|
2019-02-13 11:00:34 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
// If the Disconnect quickly follows the Connect (when an error occurs that is reported back by RabbitMQ
|
|
|
|
// not related to the connection), wait for a bit to avoid spamming the connection
|
|
|
|
if ((DateTime.UtcNow - connectedDateTime).TotalMilliseconds <= MinimumConnectedReconnectDelay)
|
|
|
|
Thread.Sleep(ReconnectDelay);
|
2019-02-13 11:00:34 +00:00
|
|
|
|
2016-11-16 22:11:05 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
var connectionFactory = new ConnectionFactory
|
|
|
|
{
|
|
|
|
HostName = connectionParams.HostName,
|
|
|
|
Port = connectionParams.Port,
|
|
|
|
VirtualHost = connectionParams.VirtualHost,
|
|
|
|
UserName = connectionParams.Username,
|
|
|
|
Password = connectionParams.Password,
|
|
|
|
AutomaticRecoveryEnabled = false,
|
|
|
|
TopologyRecoveryEnabled = false,
|
|
|
|
RequestedHeartbeat = TimeSpan.FromSeconds(30)
|
|
|
|
};
|
|
|
|
|
|
|
|
if (connectionParams.ClientProperties != null)
|
|
|
|
foreach (var pair in connectionParams.ClientProperties)
|
2019-08-18 09:06:33 +00:00
|
|
|
{
|
2021-09-21 14:17:09 +00:00
|
|
|
if (connectionFactory.ClientProperties.ContainsKey(pair.Key))
|
|
|
|
connectionFactory.ClientProperties[pair.Key] = Encoding.UTF8.GetBytes(pair.Value);
|
|
|
|
else
|
|
|
|
connectionFactory.ClientProperties.Add(pair.Key, Encoding.UTF8.GetBytes(pair.Value));
|
|
|
|
}
|
2021-07-18 11:27:10 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
|
|
|
|
while (true)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2022-11-23 08:13:38 +00:00
|
|
|
RabbitMQ.Client.IConnection? capturedConnection;
|
|
|
|
IModel? capturedConsumeChannelModel;
|
|
|
|
IModel? capturedPublishChannelModel;
|
2021-09-21 14:17:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
lock (connectionLock)
|
2021-07-18 11:27:10 +00:00
|
|
|
{
|
2021-09-21 14:17:09 +00:00
|
|
|
capturedConnection = connection;
|
2021-07-18 11:27:10 +00:00
|
|
|
}
|
2019-08-18 09:06:33 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
if (capturedConnection != null)
|
2021-07-18 11:27:10 +00:00
|
|
|
{
|
2021-09-21 14:17:09 +00:00
|
|
|
try
|
2021-07-18 11:27:10 +00:00
|
|
|
{
|
2022-11-23 08:13:38 +00:00
|
|
|
if (connection is { IsOpen: true })
|
2021-07-18 11:27:10 +00:00
|
|
|
connection.Close();
|
|
|
|
}
|
2021-09-21 14:17:09 +00:00
|
|
|
catch (AlreadyClosedException)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
finally
|
|
|
|
{
|
2022-11-23 08:13:38 +00:00
|
|
|
connection?.Dispose();
|
2021-09-21 14:17:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
connection = null;
|
|
|
|
}
|
2019-01-08 15:36:52 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
logger.Connect(new ConnectContext(connectionParams, isReconnect));
|
|
|
|
Interlocked.Increment(ref connectionReference);
|
2019-02-13 11:00:34 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
lock (connectionLock)
|
|
|
|
{
|
2021-07-18 11:27:10 +00:00
|
|
|
connection = connectionFactory.CreateConnection();
|
2021-09-21 14:17:09 +00:00
|
|
|
capturedConnection = connection;
|
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
consumeChannelModel = connection.CreateModel();
|
|
|
|
if (consumeChannel == null)
|
|
|
|
throw new BrokerUnreachableException(null);
|
2019-02-13 11:00:34 +00:00
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
publishChannelModel = connection.CreateModel();
|
|
|
|
if (publishChannel == null)
|
|
|
|
throw new BrokerUnreachableException(null);
|
2019-05-20 13:22:40 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
capturedConsumeChannelModel = consumeChannelModel;
|
|
|
|
capturedPublishChannelModel = publishChannelModel;
|
|
|
|
}
|
2019-08-13 18:30:04 +00:00
|
|
|
|
2021-07-18 11:27:10 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
if (config.Features.PublisherConfirms)
|
|
|
|
{
|
|
|
|
lastDeliveryTag = 0;
|
2021-07-18 11:27:10 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
Monitor.Enter(confirmLock);
|
|
|
|
try
|
|
|
|
{
|
|
|
|
foreach (var pair in confirmMessages)
|
|
|
|
pair.Value.CompletionSource.SetCanceled();
|
2021-07-18 11:27:10 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
confirmMessages.Clear();
|
2019-05-20 13:22:40 +00:00
|
|
|
}
|
2021-09-21 14:17:09 +00:00
|
|
|
finally
|
|
|
|
{
|
|
|
|
Monitor.Exit(confirmLock);
|
|
|
|
}
|
|
|
|
|
|
|
|
capturedPublishChannelModel.ConfirmSelect();
|
|
|
|
}
|
2019-05-20 13:22:40 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
if (connectionParams.PrefetchCount > 0)
|
2021-10-26 11:29:48 +00:00
|
|
|
capturedConsumeChannelModel.BasicQos(0, connectionParams.PrefetchCount, false);
|
2016-11-20 13:34:50 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
capturedPublishChannelModel.ModelShutdown += (_, e) =>
|
|
|
|
{
|
|
|
|
lock (connectionLock)
|
2021-07-18 11:27:10 +00:00
|
|
|
{
|
2021-09-21 14:17:09 +00:00
|
|
|
if (consumeChannelModel == null || consumeChannelModel != capturedConsumeChannelModel)
|
|
|
|
return;
|
2017-02-05 22:22:34 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
consumeChannelModel = null;
|
|
|
|
}
|
2021-07-18 11:27:10 +00:00
|
|
|
|
2022-11-23 08:13:38 +00:00
|
|
|
ConnectionEventListener?.Disconnected(new DisconnectedEventArgs(e.ReplyCode, e.ReplyText));
|
2021-09-21 14:17:09 +00:00
|
|
|
logger.Disconnect(new DisconnectContext(connectionParams, e.ReplyCode, e.ReplyText));
|
2021-07-18 11:27:10 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
// Reconnect if the disconnect was unexpected
|
|
|
|
if (!isClosing)
|
|
|
|
GetTapetiChannel(TapetiChannelType.Consume).QueueRetryable(_ => { });
|
|
|
|
};
|
2021-07-18 11:27:10 +00:00
|
|
|
|
2022-11-17 15:47:07 +00:00
|
|
|
capturedPublishChannelModel.ModelShutdown += (_, _) =>
|
2021-09-21 14:17:09 +00:00
|
|
|
{
|
|
|
|
lock (connectionLock)
|
2019-02-13 11:00:34 +00:00
|
|
|
{
|
2021-09-21 14:17:09 +00:00
|
|
|
if (publishChannelModel == null || publishChannelModel != capturedPublishChannelModel)
|
|
|
|
return;
|
2019-01-24 21:52:21 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
publishChannelModel = null;
|
|
|
|
}
|
2019-10-10 14:03:12 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
// No need to reconnect, the next Publish will
|
|
|
|
};
|
2019-08-08 09:56:10 +00:00
|
|
|
|
2017-07-14 10:33:09 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
capturedPublishChannelModel.BasicReturn += HandleBasicReturn;
|
|
|
|
capturedPublishChannelModel.BasicAcks += HandleBasicAck;
|
|
|
|
capturedPublishChannelModel.BasicNacks += HandleBasicNack;
|
2019-02-13 11:00:34 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
connectedDateTime = DateTime.UtcNow;
|
2019-02-13 11:00:34 +00:00
|
|
|
|
2022-11-23 08:13:38 +00:00
|
|
|
var connectedEventArgs = new ConnectedEventArgs(connectionParams, capturedConnection.LocalPort);
|
2019-10-10 14:03:12 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
if (isReconnect)
|
|
|
|
ConnectionEventListener?.Reconnected(connectedEventArgs);
|
|
|
|
else
|
|
|
|
ConnectionEventListener?.Connected(connectedEventArgs);
|
2019-02-13 11:00:34 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
logger.ConnectSuccess(new ConnectContext(connectionParams, isReconnect, capturedConnection.LocalPort));
|
|
|
|
isReconnect = true;
|
2019-01-08 15:36:52 +00:00
|
|
|
|
2021-09-21 14:17:09 +00:00
|
|
|
break;
|
2016-11-20 13:34:50 +00:00
|
|
|
}
|
2021-09-21 14:17:09 +00:00
|
|
|
catch (BrokerUnreachableException e)
|
|
|
|
{
|
|
|
|
logger.ConnectFailed(new ConnectContext(connectionParams, isReconnect, exception: e));
|
|
|
|
Thread.Sleep(ReconnectDelay);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
lock (connectionLock)
|
|
|
|
{
|
2021-07-18 11:27:10 +00:00
|
|
|
return channelType == TapetiChannelType.Publish
|
|
|
|
? publishChannelModel
|
|
|
|
: consumeChannelModel;
|
2016-11-20 13:34:50 +00:00
|
|
|
}
|
2016-11-16 22:11:05 +00:00
|
|
|
}
|
2017-02-12 20:43:30 +00:00
|
|
|
|
|
|
|
|
2022-11-23 08:13:38 +00:00
|
|
|
private void HandleBasicReturn(object? sender, BasicReturnEventArgs e)
|
2019-02-13 11:00:34 +00:00
|
|
|
{
|
|
|
|
/*
|
|
|
|
* "If the message is also published as mandatory, the basic.return is sent to the client before basic.ack."
|
|
|
|
* - https://www.rabbitmq.com/confirms.html
|
|
|
|
*
|
|
|
|
* Because there is no delivery tag included in the basic.return message. This solution is modeled after
|
|
|
|
* user OhJeez' answer on StackOverflow:
|
|
|
|
*
|
|
|
|
* "Since all messages with the same routing key are routed the same way. I assumed that once I get a
|
|
|
|
* basic.return about a specific routing key, all messages with this routing key can be considered undelivered"
|
|
|
|
* https://stackoverflow.com/questions/21336659/how-to-tell-which-amqp-message-was-not-routed-from-basic-return-response
|
|
|
|
*/
|
|
|
|
var key = GetReturnKey(e.Exchange, e.RoutingKey);
|
|
|
|
|
|
|
|
if (!returnRoutingKeys.TryGetValue(key, out var returnInfo))
|
|
|
|
{
|
|
|
|
returnInfo = new ReturnInfo
|
|
|
|
{
|
|
|
|
RefCount = 0,
|
|
|
|
FirstReplyCode = e.ReplyCode
|
|
|
|
};
|
|
|
|
|
|
|
|
returnRoutingKeys.Add(key, returnInfo);
|
|
|
|
}
|
|
|
|
|
|
|
|
returnInfo.RefCount++;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-11-23 08:13:38 +00:00
|
|
|
private void HandleBasicAck(object? sender, BasicAckEventArgs e)
|
2019-02-13 11:00:34 +00:00
|
|
|
{
|
2019-05-20 13:22:40 +00:00
|
|
|
Monitor.Enter(confirmLock);
|
|
|
|
try
|
2019-02-13 11:00:34 +00:00
|
|
|
{
|
2019-05-20 13:22:40 +00:00
|
|
|
foreach (var deliveryTag in GetDeliveryTags(e))
|
2019-02-13 11:00:34 +00:00
|
|
|
{
|
2019-05-20 13:22:40 +00:00
|
|
|
if (!confirmMessages.TryGetValue(deliveryTag, out var messageInfo))
|
|
|
|
continue;
|
2019-02-13 11:00:34 +00:00
|
|
|
|
2019-05-20 13:22:40 +00:00
|
|
|
if (returnRoutingKeys.TryGetValue(messageInfo.ReturnKey, out var returnInfo))
|
|
|
|
{
|
|
|
|
messageInfo.CompletionSource.SetResult(returnInfo.FirstReplyCode);
|
2019-02-13 11:00:34 +00:00
|
|
|
|
2019-05-20 13:22:40 +00:00
|
|
|
returnInfo.RefCount--;
|
|
|
|
if (returnInfo.RefCount == 0)
|
|
|
|
returnRoutingKeys.Remove(messageInfo.ReturnKey);
|
|
|
|
}
|
2021-12-10 10:45:09 +00:00
|
|
|
else
|
|
|
|
messageInfo.CompletionSource.SetResult(0);
|
2019-05-20 13:22:40 +00:00
|
|
|
|
|
|
|
confirmMessages.Remove(deliveryTag);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
finally
|
|
|
|
{
|
|
|
|
Monitor.Exit(confirmLock);
|
2019-02-13 11:00:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-11-23 08:13:38 +00:00
|
|
|
private void HandleBasicNack(object? sender, BasicNackEventArgs e)
|
2019-02-13 11:00:34 +00:00
|
|
|
{
|
2019-05-20 13:22:40 +00:00
|
|
|
Monitor.Enter(confirmLock);
|
|
|
|
try
|
2019-02-13 11:00:34 +00:00
|
|
|
{
|
2019-05-20 13:22:40 +00:00
|
|
|
foreach (var deliveryTag in GetDeliveryTags(e))
|
|
|
|
{
|
|
|
|
if (!confirmMessages.TryGetValue(deliveryTag, out var messageInfo))
|
|
|
|
continue;
|
2019-02-13 11:00:34 +00:00
|
|
|
|
2019-05-20 13:22:40 +00:00
|
|
|
messageInfo.CompletionSource.SetCanceled();
|
|
|
|
confirmMessages.Remove(e.DeliveryTag);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
finally
|
|
|
|
{
|
|
|
|
Monitor.Exit(confirmLock);
|
2019-02-13 11:00:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private IEnumerable<ulong> GetDeliveryTags(BasicAckEventArgs e)
|
|
|
|
{
|
|
|
|
return e.Multiple
|
|
|
|
? confirmMessages.Keys.Where(tag => tag <= e.DeliveryTag).ToArray()
|
|
|
|
: new[] { e.DeliveryTag };
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private IEnumerable<ulong> GetDeliveryTags(BasicNackEventArgs e)
|
|
|
|
{
|
|
|
|
return e.Multiple
|
|
|
|
? confirmMessages.Keys.Where(tag => tag <= e.DeliveryTag).ToArray()
|
|
|
|
: new[] { e.DeliveryTag };
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string GetReturnKey(string exchange, string routingKey)
|
|
|
|
{
|
|
|
|
return exchange + ':' + routingKey;
|
|
|
|
}
|
2019-10-10 14:03:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private class ConnectContext : IConnectSuccessContext, IConnectFailedContext
|
|
|
|
{
|
|
|
|
public TapetiConnectionParams ConnectionParams { get; }
|
|
|
|
public bool IsReconnect { get; }
|
|
|
|
public int LocalPort { get; }
|
2022-11-23 08:13:38 +00:00
|
|
|
public Exception? Exception { get; }
|
2019-10-10 14:03:12 +00:00
|
|
|
|
|
|
|
|
2022-11-23 08:13:38 +00:00
|
|
|
public ConnectContext(TapetiConnectionParams connectionParams, bool isReconnect, int localPort = 0, Exception? exception = null)
|
2019-10-10 14:03:12 +00:00
|
|
|
{
|
|
|
|
ConnectionParams = connectionParams;
|
|
|
|
IsReconnect = isReconnect;
|
|
|
|
LocalPort = localPort;
|
|
|
|
Exception = exception;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private class DisconnectContext : IDisconnectContext
|
|
|
|
{
|
|
|
|
public TapetiConnectionParams ConnectionParams { get; }
|
|
|
|
public ushort ReplyCode { get; }
|
|
|
|
public string ReplyText { get; }
|
|
|
|
|
|
|
|
|
|
|
|
public DisconnectContext(TapetiConnectionParams connectionParams, ushort replyCode, string replyText)
|
|
|
|
{
|
|
|
|
ConnectionParams = connectionParams;
|
|
|
|
ReplyCode = replyCode;
|
|
|
|
ReplyText = replyText;
|
|
|
|
}
|
|
|
|
}
|
2016-11-16 22:11:05 +00:00
|
|
|
}
|
|
|
|
}
|