1
0
mirror of synced 2024-06-25 22:07:39 +00:00

Added support for the Obsolete attribute to remove bindings and queues with backwards compatibility

Updated license in nuspec to comply with the new specifications
This commit is contained in:
Mark van Renswoude 2019-08-20 11:47:53 +02:00
parent 295b584969
commit bef3961f7f
25 changed files with 336 additions and 88 deletions

View File

@ -61,6 +61,11 @@ namespace ExampleLib
public async Task WaitAsync()
{
await doneSignal.Task;
// This is a hack, because the signal is often given in a message handler before the message can be
// acknowledged, causing it to be put back on the queue because the connection is closed.
// This short delay allows consumers to finish. This is not an issue in a proper service application.
await Task.Delay(500);
}

View File

@ -6,7 +6,7 @@
<title>Tapeti Annotations</title>
<authors>Mark van Renswoude</authors>
<owners>Mark van Renswoude</owners>
<licenseUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/UNLICENSE</licenseUrl>
<license type="expression">Unlicense</license>
<projectUrl>https://github.com/MvRens/Tapeti</projectUrl>
<iconUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/resources/icons/Tapeti.Annotations.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@ -6,7 +6,7 @@
<title>Tapeti Autofac</title>
<authors>Mark van Renswoude</authors>
<owners>Mark van Renswoude</owners>
<licenseUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/UNLICENSE</licenseUrl>
<license type="expression">Unlicense</license>
<projectUrl>https://github.com/MvRens/Tapeti</projectUrl>
<iconUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/resources/icons/Tapeti.SimpleInjector.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@ -6,7 +6,7 @@
<title>Tapeti Castle Windsor</title>
<authors>Mark van Renswoude</authors>
<owners>Mark van Renswoude</owners>
<licenseUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/UNLICENSE</licenseUrl>
<license type="expression">Unlicense</license>
<projectUrl>https://github.com/MvRens/Tapeti</projectUrl>
<iconUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/resources/icons/Tapeti.SimpleInjector.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@ -6,7 +6,7 @@
<title>Tapeti DataAnnotations Extensions</title>
<authors>Mark van Renswoude</authors>
<owners>Mark van Renswoude</owners>
<licenseUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/UNLICENSE</licenseUrl>
<license type="expression">Unlicense</license>
<projectUrl>https://github.com/MvRens/Tapeti</projectUrl>
<iconUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/resources/icons/Tapeti.DataAnnotations.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@ -6,7 +6,7 @@
<title>Tapeti DataAnnotations</title>
<authors>Mark van Renswoude</authors>
<owners>Mark van Renswoude</owners>
<licenseUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/UNLICENSE</licenseUrl>
<license type="expression">Unlicense</license>
<projectUrl>https://github.com/MvRens/Tapeti</projectUrl>
<iconUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/resources/icons/Tapeti.DataAnnotations.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@ -6,7 +6,7 @@
<title>Tapeti Flow SQL</title>
<authors>Mark van Renswoude</authors>
<owners>Mark van Renswoude</owners>
<licenseUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/UNLICENSE</licenseUrl>
<license type="expression">Unlicense</license>
<projectUrl>https://github.com/MvRens/Tapeti</projectUrl>
<iconUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/resources/icons/Tapeti.Flow.SQL.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@ -6,7 +6,7 @@
<title>Tapeti Flow</title>
<authors>Menno van Lavieren, Mark van Renswoude</authors>
<owners>Mark van Renswoude</owners>
<licenseUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/UNLICENSE</licenseUrl>
<license type="expression">Unlicense</license>
<projectUrl>https://github.com/MvRens/Tapeti</projectUrl>
<iconUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/resources/icons/Tapeti.Flow.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@ -6,7 +6,7 @@
<title>Tapeti Ninject</title>
<authors>Mark van Renswoude</authors>
<owners>Mark van Renswoude</owners>
<licenseUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/UNLICENSE</licenseUrl>
<license type="expression">Unlicense</license>
<projectUrl>https://github.com/MvRens/Tapeti</projectUrl>
<iconUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/resources/icons/Tapeti.SimpleInjector.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@ -6,7 +6,7 @@
<title>Tapeti Serilog</title>
<authors>Hans Mulder</authors>
<owners>Hans Mulder</owners>
<licenseUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/UNLICENSE</licenseUrl>
<license type="expression">Unlicense</license>
<projectUrl>https://github.com/MvRens/Tapeti</projectUrl>
<iconUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/resources/icons/Tapeti.Serilog.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@ -71,5 +71,14 @@ namespace Tapeti.Serilog
contextLogger.Error(exception, "Tapeti: exception in message handler");
}
/// <inheritdoc />
public void QueueObsolete(string queueName, bool deleted, uint messageCount)
{
if (deleted)
seriLogger.Information("Tapeti: obsolete queue {queue} has been deleted", queueName);
else
seriLogger.Information("Tapeti: obsolete queue {queue} has been unbound but not yet deleted, {messageCount} messages remaining", queueName, messageCount);
}
}
}

View File

@ -6,7 +6,7 @@
<title>Tapeti SimpleInjector</title>
<authors>Mark van Renswoude</authors>
<owners>Mark van Renswoude</owners>
<licenseUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/UNLICENSE</licenseUrl>
<license type="expression">Unlicense</license>
<projectUrl>https://github.com/MvRens/Tapeti</projectUrl>
<iconUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/resources/icons/Tapeti.SimpleInjector.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@ -6,7 +6,7 @@
<title>Tapeti Transient</title>
<authors>Menno van Lavieren, Mark van Renswoude</authors>
<owners>Mark van Renswoude</owners>
<licenseUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/UNLICENSE</licenseUrl>
<license type="expression">Unlicense</license>
<projectUrl>https://github.com/MvRens/Tapeti</projectUrl>
<iconUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/resources/icons/Tapeti.Flow.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@ -6,7 +6,7 @@
<title>Tapeti UnityContainer</title>
<authors>Mark van Renswoude</authors>
<owners>Mark van Renswoude</owners>
<licenseUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/UNLICENSE</licenseUrl>
<license type="expression">Unlicense</license>
<projectUrl>https://github.com/MvRens/Tapeti</projectUrl>
<iconUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/resources/icons/Tapeti.SimpleInjector.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@ -117,5 +117,12 @@ namespace Tapeti.Config
/// <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>
/// <returns>The generated name of the dynamic queue</returns>
Task<string> BindDynamicDirect(string queuePrefix = null);
/// <summary>
/// Marks the specified durable queue as having an obsolete binding. If after all bindings have subscribed, the queue only contains obsolete
/// bindings and is empty, it will be removed.
/// </summary>
/// <param name="queueName">The name of the durable queue</param>
Task BindDurableObsolete(string queueName);
}
}

View File

@ -91,6 +91,13 @@ namespace Tapeti.Connection
/// <param name="queueName">The name of the queue to verify</param>
Task DurableQueueVerify(string queueName);
/// <summary>
/// Deletes a durable queue.
/// </summary>
/// <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(string queueName, bool onlyIfEmpty = true);
/// <summary>
/// Creates a dynamic queue.
/// </summary>

View File

@ -50,6 +50,7 @@ namespace Tapeti.Connection
private ulong lastDeliveryTag;
private DateTime connectedDateTime;
private readonly HttpClient managementClient;
private readonly HashSet<string> deletedQueues = new HashSet<string>();
// These fields must be locked, since the callbacks for BasicAck/BasicReturn can run in a different thread
private readonly object confirmLock = new object();
@ -185,16 +186,16 @@ namespace Tapeti.Connection
/// <inheritdoc />
public async Task Consume(string queueName, IConsumer consumer)
{
if (deletedQueues.Contains(queueName))
return;
if (string.IsNullOrEmpty(queueName))
throw new ArgumentNullException(nameof(queueName));
await taskQueue.Value.Add(() =>
{
WithRetryableChannel(channel =>
{
var basicConsumer = new TapetiBasicConsumer(consumer, Respond);
channel.BasicConsume(queueName, false, basicConsumer);
});
await QueueWithRetryableChannel(channel =>
{
var basicConsumer = new TapetiBasicConsumer(consumer, Respond);
channel.BasicConsume(queueName, false, basicConsumer);
});
}
@ -223,7 +224,6 @@ namespace Tapeti.Connection
default:
throw new ArgumentOutOfRangeException(nameof(result), result, null);
}
});
}
@ -255,32 +255,106 @@ namespace Tapeti.Connection
/// <inheritdoc />
public async Task DurableQueueVerify(string queueName)
{
await taskQueue.Value.Add(() =>
{
WithRetryableChannel(channel =>
{
channel.QueueDeclarePassive(queueName);
});
await QueueWithRetryableChannel(channel =>
{
channel.QueueDeclarePassive(queueName);
});
}
/// <inheritdoc />
public async Task DurableQueueDelete(string queueName, bool onlyIfEmpty = true)
{
if (!onlyIfEmpty)
{
uint deletedMessages = 0;
await QueueWithRetryableChannel(channel =>
{
deletedMessages = channel.QueueDelete(queueName);
});
deletedQueues.Add(queueName);
logger.QueueObsolete(queueName, true, deletedMessages);
return;
}
await taskQueue.Value.Add(async () =>
{
bool retry;
do
{
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
{
WithRetryableChannel(channel =>
{
channel.QueueDelete(queueName, false, true);
});
deletedQueues.Add(queueName);
logger.QueueObsolete(queueName, true, 0);
}
catch (OperationInterruptedException e)
{
if (e.ShutdownReason.ReplyCode == RabbitMQ.Client.Framing.Constants.PreconditionFailed)
retry = true;
else
throw;
}
}
else
{
// Remove all bindings instead
var existingBindings = (await GetQueueBindings(queueName)).ToList();
if (existingBindings.Count > 0)
{
WithRetryableChannel(channel =>
{
foreach (var binding in existingBindings)
channel.QueueUnbind(queueName, binding.Exchange, binding.RoutingKey);
});
}
logger.QueueObsolete(queueName, false, queueInfo.Messages);
}
} while (retry);
});
}
/// <inheritdoc />
public async Task<string> DynamicQueueDeclare(string queuePrefix = null)
{
string queueName = null;
await taskQueue.Value.Add(() =>
await QueueWithRetryableChannel(channel =>
{
WithRetryableChannel(channel =>
if (!string.IsNullOrEmpty(queuePrefix))
{
if (!string.IsNullOrEmpty(queuePrefix))
{
queueName = queuePrefix + "." + Guid.NewGuid().ToString("N");
channel.QueueDeclare(queueName);
}
else
queueName = channel.QueueDeclare().QueueName;
});
queueName = queuePrefix + "." + Guid.NewGuid().ToString("N");
channel.QueueDeclare(queueName);
}
else
queueName = channel.QueueDeclare().QueueName;
});
return queueName;
@ -289,13 +363,10 @@ namespace Tapeti.Connection
/// <inheritdoc />
public async Task DynamicQueueBind(string queueName, QueueBinding binding)
{
await taskQueue.Value.Add(() =>
await QueueWithRetryableChannel(channel =>
{
WithRetryableChannel(channel =>
{
DeclareExchange(channel, binding.Exchange);
channel.QueueBind(queueName, binding.Exchange, binding.RoutingKey);
});
DeclareExchange(channel, binding.Exchange);
channel.QueueBind(queueName, binding.Exchange, binding.RoutingKey);
});
}
@ -335,18 +406,31 @@ namespace Tapeti.Connection
HttpStatusCode.ServiceUnavailable
};
private static readonly TimeSpan[] ExponentialBackoff =
private class ManagementQueueInfo
{
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)
};
[JsonProperty("messages")]
public uint Messages { get; set; }
}
private async Task<ManagementQueueInfo> GetQueueInfo(string queueName)
{
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);
});
}
private class ManagementBinding
@ -378,10 +462,42 @@ namespace Tapeti.Connection
{
var virtualHostPath = Uri.EscapeDataString(connectionParams.VirtualHost);
var queuePath = Uri.EscapeDataString(queueName);
var requestUri = new Uri($"http://{connectionParams.HostName}:{connectionParams.ManagementPort}/api/queues/{virtualHostPath}/{queuePath}/bindings");
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
return bindings
.Where(binding => !string.IsNullOrEmpty(binding.Source))
.Select(binding => new QueueBinding(binding.Source, binding.RoutingKey));
});
}
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)
{
var requestUri = new Uri($"http://{connectionParams.HostName}:{connectionParams.ManagementPort}/api/{path}");
using (var request = new HttpRequestMessage(HttpMethod.Get, requestUri))
{
{
var retryDelayIndex = 0;
while (true)
@ -389,15 +505,7 @@ namespace Tapeti.Connection
try
{
var response = await managementClient.SendAsync(request);
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
return bindings
.Where(binding => !string.IsNullOrEmpty(binding.Source))
.Select(binding => new QueueBinding(binding.Source, binding.RoutingKey));
return await handleResponse(response);
}
catch (TimeoutException)
{
@ -435,6 +543,15 @@ namespace Tapeti.Connection
}
private async Task QueueWithRetryableChannel(Action<IModel> operation)
{
await taskQueue.Value.Add(() =>
{
WithRetryableChannel(operation);
});
}
/// <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.

View File

@ -91,6 +91,7 @@ namespace Tapeti.Connection
public abstract Task BindDurable(Type messageClass, string queueName);
public abstract Task BindDurableDirect(string queueName);
public abstract Task BindDurableObsolete(string queueName);
public async Task<string> BindDynamic(Type messageClass, string queuePrefix = null)
@ -182,6 +183,7 @@ 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>();
public DeclareDurableQueuesBindingTarget(Func<ITapetiClient> clientFactory, IRoutingKeyStrategy routingKeyStrategy, IExchangeStrategy exchangeStrategy) : base(clientFactory, routingKeyStrategy, exchangeStrategy)
@ -217,10 +219,23 @@ namespace Tapeti.Connection
}
public override Task BindDurableObsolete(string queueName)
{
obsoleteDurableQueues.Add(queueName);
return Task.CompletedTask;
}
public override async Task Apply()
{
var worker = ClientFactory();
await DeclareQueues(worker);
await DeleteObsoleteQueues(worker);
}
private async Task DeclareQueues(ITapetiClient worker)
{
await Task.WhenAll(durableQueues.Select(async queue =>
{
var bindings = queue.Value.Select(messageClass =>
@ -234,6 +249,15 @@ namespace Tapeti.Connection
await worker.DurableQueueDeclare(queue.Key, bindings);
}));
}
private async Task DeleteObsoleteQueues(ITapetiClient worker)
{
await Task.WhenAll(obsoleteDurableQueues.Except(durableQueues.Keys).Select(async queue =>
{
await worker.DurableQueueDelete(queue);
}));
}
}
@ -257,6 +281,11 @@ namespace Tapeti.Connection
await VerifyDurableQueue(queueName);
}
public override Task BindDurableObsolete(string queueName)
{
return Task.CompletedTask;
}
private async Task VerifyDurableQueue(string queueName)
{

View File

@ -45,5 +45,13 @@ namespace Tapeti.Default
Console.WriteLine();
Console.WriteLine(exception);
}
/// <inheritdoc />
public void QueueObsolete(string queueName, bool deleted, uint messageCount)
{
Console.WriteLine(deleted
? $"[Tapeti] Obsolete queue was deleted: {queueName}"
: $"[Tapeti] Obsolete queue bindings removed: {queueName}, {messageCount} messages remaining");
}
}
}

View File

@ -46,6 +46,12 @@ namespace Tapeti.Default
/// </summary>
public BindingTargetMode BindingTargetMode;
/// <summary>
/// Indicates if the method or controller is marked with the Obsolete attribute, indicating it should
/// only handle messages already in the queue and not bind to the routing key for new messages.
/// </summary>
public bool IsObsolete;
/// <summary>
/// Value factories for the method parameters.
/// </summary>
@ -106,32 +112,40 @@ namespace Tapeti.Default
/// <inheritdoc />
public async Task Apply(IBindingTarget target)
{
switch (bindingInfo.BindingTargetMode)
if (!bindingInfo.IsObsolete)
{
case BindingTargetMode.Default:
if (bindingInfo.QueueInfo.QueueType == QueueType.Dynamic)
QueueName = await target.BindDynamic(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name);
else
{
await target.BindDurable(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name);
QueueName = bindingInfo.QueueInfo.Name;
}
switch (bindingInfo.BindingTargetMode)
{
case BindingTargetMode.Default:
if (bindingInfo.QueueInfo.QueueType == QueueType.Dynamic)
QueueName = await target.BindDynamic(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name);
else
{
await target.BindDurable(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name);
QueueName = bindingInfo.QueueInfo.Name;
}
break;
break;
case BindingTargetMode.Direct:
if (bindingInfo.QueueInfo.QueueType == QueueType.Dynamic)
QueueName = await target.BindDynamicDirect(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name);
else
{
await target.BindDurableDirect(bindingInfo.QueueInfo.Name);
QueueName = bindingInfo.QueueInfo.Name;
}
case BindingTargetMode.Direct:
if (bindingInfo.QueueInfo.QueueType == QueueType.Dynamic)
QueueName = await target.BindDynamicDirect(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name);
else
{
await target.BindDurableDirect(bindingInfo.QueueInfo.Name);
QueueName = bindingInfo.QueueInfo.Name;
}
break;
break;
default:
throw new ArgumentOutOfRangeException(nameof(bindingInfo.BindingTargetMode), bindingInfo.BindingTargetMode, "Invalid BindingTargetMode");
default:
throw new ArgumentOutOfRangeException(nameof(bindingInfo.BindingTargetMode), bindingInfo.BindingTargetMode, "Invalid BindingTargetMode");
}
}
else if (bindingInfo.QueueInfo.QueueType == QueueType.Durable)
{
await target.BindDurableObsolete(bindingInfo.QueueInfo.Name);
QueueName = bindingInfo.QueueInfo.Name;
}
}

View File

@ -28,5 +28,10 @@ namespace Tapeti.Default
public void ConsumeException(Exception exception, IMessageContext messageContext, ConsumeResult consumeResult)
{
}
/// <inheritdoc />
public void QueueObsolete(string queueName, bool deleted, uint messageCount)
{
}
}
}

View File

@ -42,5 +42,13 @@ namespace Tapeti
/// <param name="messageContext"></param>
/// <param name="consumeResult">Indicates the action taken by the exception handler</param>
void ConsumeException(Exception exception, IMessageContext messageContext, ConsumeResult consumeResult);
/// <summary>
/// Called when a queue is determined to be obsolete.
/// </summary>
/// <param name="queueName"></param>
/// <param name="deleted">True if the queue was empty and has been deleted, false if there are still messages to process</param>
/// <param name="messageCount">If deleted, the number of messages purged, otherwise the number of messages still in the queue</param>
void QueueObsolete(string queueName, bool deleted, uint messageCount);
}
}

View File

@ -6,7 +6,7 @@
<title>Tapeti</title>
<authors>Mark van Renswoude</authors>
<owners>Mark van Renswoude</owners>
<licenseUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/UNLICENSE</licenseUrl>
<license type="expression">Unlicense</license>
<projectUrl>https://github.com/MvRens/Tapeti</projectUrl>
<iconUrl>https://raw.githubusercontent.com/MvRens/Tapeti/master/resources/icons/Tapeti.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@ -40,6 +40,9 @@ namespace Tapeti
var controllerQueueInfo = GetQueueInfo(controller);
(builderAccess.DependencyResolver as IDependencyContainer)?.RegisterController(controller);
var controllerIsObsolete = controller.GetCustomAttribute<ObsoleteAttribute>() != null;
foreach (var method in controller.GetMembers(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.MemberType == MemberTypes.Method && m.DeclaringType != typeof(object) && (m as MethodInfo)?.IsSpecialName == false)
.Select(m => (MethodInfo)m))
@ -50,6 +53,9 @@ namespace Tapeti
$"Method {method.Name} or controller {controller.Name} requires a queue attribute");
var methodIsObsolete = controllerIsObsolete || method.GetCustomAttribute<ObsoleteAttribute>() != null;
var context = new ControllerBindingContext(method.GetParameters(), method.ReturnParameter)
{
Controller = controller,
@ -83,6 +89,7 @@ namespace Tapeti
QueueInfo = methodQueueInfo,
MessageClass = context.MessageClass,
BindingTargetMode = context.BindingTargetMode,
IsObsolete = methodIsObsolete,
ParameterFactories = context.GetParameterHandlers(),
ResultHandler = context.GetResultHandler(),

View File

@ -45,9 +45,41 @@ To enable the automatic creation of durable queues, call EnableDeclareDurableQue
.Build();
The queue will be bound to all message classes for which you have defined a message handler. If the queue already existed and contains bindings which are no longer valid, those bindings will be removed. Note however that if there are still messages of that type in the queue they will be consumed and cause an exception.
The queue will be bound to all message classes for which you have defined a message handler. If the queue already existed and contains bindings which are no longer valid, those bindings will be removed. Note however that if there are still messages of that type in the queue they will be consumed and cause an exception. To keep the queue backwards compatible, see the next section on migrating durable queues.
At the time of writing there is no special support for obsolete queues. Once a durable queue is no longer referenced in the service it will remain in RabbitMQ along with any messages in it, without a consumer. This allows you to inspect the contents, perform any migrating steps necessary and delete the queue manually.
Migrating durable queues
------------------------
.. note:: This section assumes you are using EnableDeclareDurableQueues.
As your service evolves so can your message handlers. Perhaps a message no longer needs to handled, or you want to split them into another queue.
If you remove a message handler the binding will also be removed from the queue, but there may still be messages of that type in the queue. Since these have nowhere to go, they will cause an error and be lost.
Instead of removing the message handler you can mark it with the standard .NET ``[Obsolete]`` attribute:
::
[MessageController]
[DurableQueue("monitoring")]
public class ObsoleteMonitoringController
{
[Obsolete]
public void HandleEscapeMessage(RabbitEscapedMessage message)
{
// Handle the message like before, perform the necessary migration,
// or simply ignore it if you no longer need it.
}
}
Messages will still be consumed from the queue as long as it exists, but the routing key binding will removed so no new messages of that type will be delivered.
The ``[Obsolete]`` attribute can also be applied to the entire controller to mark all message handlers it contains as obsolete.
If all message handlers bound to a durable queue are marked as obsolete, including other controllers bound to the same durable queue, the queue is a candidate for removal. During startup, if the queue is empty it will be deleted. This action is logged to the registered ILogger.
If there are still messages in the queue it's pending removal will be logged but the consumers will run as normal to empty the queue. The queue will then remain until it is checked again when the application is restarted.
Request - response