|
1 | 1 | using RabbitMQ.Client; |
2 | 2 | using System; |
3 | 3 | using System.Collections.Generic; |
| 4 | +using Microsoft.Extensions.Logging; |
4 | 5 | using Unity.Modules.Shared.MessageBrokers.RabbitMQ.Constants; |
5 | 6 | using Unity.Modules.Shared.MessageBrokers.RabbitMQ.Interfaces; |
| 7 | +using System.Threading; |
6 | 8 |
|
7 | 9 | namespace Unity.Modules.Shared.MessageBrokers.RabbitMQ |
8 | 10 | { |
9 | | - public class QueueChannelProvider<TQueueMessage> : IQueueChannelProvider<TQueueMessage> where TQueueMessage : IQueueMessage |
| 11 | + public class QueueChannelProvider<TQueueMessage>(IChannelProvider channelProvider, ILogger<QueueChannelProvider<TQueueMessage>> logger) : IQueueChannelProvider<TQueueMessage> |
| 12 | + where TQueueMessage : IQueueMessage |
10 | 13 | { |
11 | | - private readonly IChannelProvider _channelProvider; |
| 14 | + private readonly IChannelProvider _channelProvider = channelProvider ?? throw new ArgumentNullException(nameof(channelProvider)); |
| 15 | + private readonly ILogger<QueueChannelProvider<TQueueMessage>> _logger = logger ?? throw new ArgumentNullException(nameof(logger)); |
| 16 | + private readonly Lock _lock = new(); |
12 | 17 | private IModel? _channel; |
13 | | - private bool disposedValue; |
14 | | - private readonly string _queueName; |
| 18 | + private bool _disposed; |
| 19 | + private bool _queuesDeclared; |
| 20 | + private readonly string _queueName = typeof(TQueueMessage).Name; |
15 | 21 |
|
16 | | - public QueueChannelProvider( |
17 | | - IChannelProvider channelProvider) |
| 22 | + public IModel GetChannel() |
18 | 23 | { |
19 | | - _channelProvider = channelProvider; |
20 | | - _queueName = typeof(TQueueMessage).Name; |
| 24 | + ObjectDisposedException.ThrowIf(_disposed, typeof(QueueChannelProvider<TQueueMessage>)); |
| 25 | + |
| 26 | + lock (_lock) |
| 27 | + { |
| 28 | + if (_channel == null || !_channel.IsOpen) |
| 29 | + { |
| 30 | + _channel?.Dispose(); |
| 31 | + _channel = _channelProvider.GetChannel(); |
| 32 | + _queuesDeclared = false; |
| 33 | + } |
| 34 | + |
| 35 | + if (_channel == null || !_channel.IsOpen) |
| 36 | + throw new InvalidOperationException("Failed to get a valid RabbitMQ channel"); |
| 37 | + |
| 38 | + if (!_queuesDeclared) |
| 39 | + { |
| 40 | + DeclareQueueAndDeadLetter(_channel); |
| 41 | + _queuesDeclared = true; |
| 42 | + } |
| 43 | + |
| 44 | + return _channel; |
| 45 | + } |
21 | 46 | } |
22 | 47 |
|
23 | | - public IModel? GetChannel() |
| 48 | + private void DeclareQueueAndDeadLetter(IModel channel) |
24 | 49 | { |
25 | | - _channel = _channelProvider?.GetChannel(); |
26 | | - DeclareQueueAndDeadLetter(); |
27 | | - return _channel; |
| 50 | + try |
| 51 | + { |
| 52 | + // First, try to declare the queue as passive to check if it exists |
| 53 | + try |
| 54 | + { |
| 55 | + channel.QueueDeclarePassive(_queueName); |
| 56 | + // Queue exists and is compatible, just declare exchange and binding |
| 57 | + DeclareCompatibleQueue(channel); |
| 58 | + return; |
| 59 | + } |
| 60 | + catch (global::RabbitMQ.Client.Exceptions.OperationInterruptedException ex) |
| 61 | + { |
| 62 | + // The channel is now closed. Get a new one immediately. |
| 63 | + _channel?.Dispose(); |
| 64 | + _channel = _channelProvider.GetChannel(); |
| 65 | + channel = _channel ?? throw new InvalidOperationException("Failed to get a new RabbitMQ channel after an error."); |
| 66 | + |
| 67 | + // Check the reason for the exception |
| 68 | + if (ex.ShutdownReason.ReplyCode == 404) |
| 69 | + { |
| 70 | + // Queue not found, declare it with the full dead-letter configuration |
| 71 | + DeclareQueueWithDeadLetter(channel); |
| 72 | + return; |
| 73 | + } |
| 74 | + if (ex.ShutdownReason.ReplyText.Contains("inequivalent arg")) |
| 75 | + { |
| 76 | + _logger.LogDebug("Queue {QueueName} exists with incompatible configuration, falling back to compatibility mode.", _queueName); |
| 77 | + DeclareCompatibleQueue(channel); |
| 78 | + return; |
| 79 | + } |
| 80 | + |
| 81 | + // Re-throw any other exceptions |
| 82 | + throw; |
| 83 | + } |
| 84 | + } |
| 85 | + catch (Exception ex) |
| 86 | + { |
| 87 | + throw new InvalidOperationException($"Failed to declare queues for {_queueName}", ex); |
| 88 | + } |
28 | 89 | } |
29 | 90 |
|
30 | | - private void DeclareQueueAndDeadLetter() |
| 91 | + private void DeclareQueueWithDeadLetter(IModel channel) |
31 | 92 | { |
32 | | - var deadLetterQueueName = $"{_queueName}{QueueingConstants.DeadletterAddition}"; |
| 93 | + var dlxName = $"{_queueName}.dlx"; |
| 94 | + var dlqName = $"{_queueName}{QueueingConstants.DeadletterAddition}"; |
| 95 | + var mainExchange = $"{_queueName}.exchange"; |
33 | 96 |
|
34 | | - // Declare the DeadLetter Queue |
35 | | - var deadLetterQueueArgs = new Dictionary<string, object> |
| 97 | + channel.ExchangeDeclare(dlxName, ExchangeType.Direct, durable: true); |
| 98 | + |
| 99 | + var dlqArgs = new Dictionary<string, object> |
36 | 100 | { |
37 | | - { "x-queue-type", "quorum" }, |
38 | | - { "overflow", "reject-publish" } // If the queue is full, reject the publish |
| 101 | + { "x-queue-type", "quorum" }, |
| 102 | + { "x-overflow", "reject-publish" } |
39 | 103 | }; |
40 | | - if(_channel == null) return; |
41 | 104 |
|
42 | | - _channel.ExchangeDeclare(deadLetterQueueName, ExchangeType.Direct); |
43 | | - _channel.QueueDeclare(deadLetterQueueName, true, false, false, deadLetterQueueArgs); |
44 | | - _channel.QueueBind(deadLetterQueueName, deadLetterQueueName, deadLetterQueueName, null); |
| 105 | + channel.QueueDeclare(dlqName, durable: true, exclusive: false, autoDelete: false, arguments: dlqArgs); |
| 106 | + channel.QueueBind(dlqName, dlxName, dlqName); |
| 107 | + |
| 108 | + channel.ExchangeDeclare(mainExchange, ExchangeType.Direct, durable: true); |
45 | 109 |
|
46 | | - // Declare the Queue |
47 | | - var queueArgs = new Dictionary<string, object> |
| 110 | + var mainQArgs = new Dictionary<string, object> |
48 | 111 | { |
49 | | - { "x-dead-letter-exchange", deadLetterQueueName }, |
50 | | - { "x-dead-letter-routing-key", deadLetterQueueName }, |
51 | 112 | { "x-queue-type", "quorum" }, |
52 | | - { "x-dead-letter-strategy", "at-least-once" }, // Ensure that deadletter messages are delivered in any case see: https://www.rabbitmq.com/quorum-queues.html#dead-lettering |
53 | | - { "overflow", "reject-publish" } // If the queue is full, reject the publish |
| 113 | + { "x-overflow", "reject-publish" }, |
| 114 | + { "x-dead-letter-exchange", dlxName }, |
| 115 | + { "x-dead-letter-routing-key", dlqName }, |
| 116 | + { "x-dead-letter-strategy", "at-least-once" }, |
| 117 | + { "x-delivery-limit", 10 } |
54 | 118 | }; |
55 | 119 |
|
56 | | - _channel.ExchangeDeclare(_queueName, ExchangeType.Direct); |
57 | | - _channel.QueueDeclare(_queueName, true, false, false, queueArgs); |
58 | | - _channel.QueueBind(_queueName, _queueName, _queueName, null); |
| 120 | + channel.QueueDeclare(_queueName, durable: true, exclusive: false, autoDelete: false, arguments: mainQArgs); |
| 121 | + channel.QueueBind(_queueName, mainExchange, _queueName); |
59 | 122 | } |
60 | 123 |
|
61 | | - protected virtual void Dispose(bool disposing) |
| 124 | + private void DeclareCompatibleQueue(IModel channel) |
62 | 125 | { |
63 | | - if (!disposedValue) |
| 126 | + var mainExchange = $"{_queueName}.exchange"; |
| 127 | + |
| 128 | + try |
64 | 129 | { |
65 | | - if (disposing) |
66 | | - { |
67 | | - // dispose managed state (managed objects) |
68 | | - } |
| 130 | + channel.ExchangeDeclare(mainExchange, ExchangeType.Direct, durable: true); |
| 131 | + channel.QueueBind(_queueName, mainExchange, _queueName); |
69 | 132 |
|
70 | | - disposedValue = true; |
| 133 | + _logger.LogWarning("Queue {QueueName} exists with incompatible configuration. Running in compatibility mode without dead letter support.", _queueName); |
| 134 | + } |
| 135 | + catch (Exception ex) |
| 136 | + { |
| 137 | + throw new InvalidOperationException( |
| 138 | + $"Failed to declare queue {_queueName} in compatibility mode. " + |
| 139 | + "The existing queue has incompatible configuration and cannot be used.", ex); |
71 | 140 | } |
72 | 141 | } |
73 | 142 |
|
74 | 143 | public void Dispose() |
75 | 144 | { |
76 | | - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method |
77 | | - Dispose(disposing: true); |
| 145 | + Dispose(true); |
78 | 146 | GC.SuppressFinalize(this); |
79 | 147 | } |
| 148 | + |
| 149 | + protected virtual void Dispose(bool disposing) |
| 150 | + { |
| 151 | + if (_disposed) return; |
| 152 | + |
| 153 | + if (disposing) |
| 154 | + { |
| 155 | + if (_channel != null && _channelProvider != null) |
| 156 | + { |
| 157 | + try |
| 158 | + { |
| 159 | + _channelProvider.ReturnChannel(_channel); |
| 160 | + } |
| 161 | + catch |
| 162 | + { |
| 163 | + _channel?.Dispose(); |
| 164 | + } |
| 165 | + } |
| 166 | + } |
| 167 | + |
| 168 | + _disposed = true; |
| 169 | + _channel = null; |
| 170 | + } |
| 171 | + |
| 172 | + public string QueueName => _queueName; |
80 | 173 | } |
81 | 174 | } |
0 commit comments