1
0
mirror of synced 2024-09-28 19:56:09 +00:00
Tapeti/Tapeti/Connection/TapetiWorker.cs

294 lines
11 KiB
C#
Raw Normal View History

2016-11-17 16:33:27 +00:00
using System;
2016-12-11 14:08:58 +00:00
using System.Collections.Generic;
2019-01-24 21:52:21 +00:00
using System.Threading;
2016-11-17 16:33:27 +00:00
using System.Threading.Tasks;
using RabbitMQ.Client;
using RabbitMQ.Client.Exceptions;
using RabbitMQ.Client.Framing;
2016-12-11 14:08:58 +00:00
using Tapeti.Config;
2019-01-24 21:52:21 +00:00
using Tapeti.Exceptions;
2017-02-12 20:43:30 +00:00
using Tapeti.Helpers;
using Tapeti.Tasks;
namespace Tapeti.Connection
{
public class TapetiWorker
{
private const int ReconnectDelay = 5000;
2019-01-24 21:52:21 +00:00
private const int MandatoryReturnTimeout = 30000;
private const int PublishMaxConnectAttempts = 3;
2017-02-12 20:43:30 +00:00
private readonly IConfig config;
private readonly ILogger logger;
public TapetiConnectionParams ConnectionParams { get; set; }
public IConnectionEventListener ConnectionEventListener { get; set; }
private readonly IMessageSerializer messageSerializer;
private readonly IRoutingKeyStrategy routingKeyStrategy;
private readonly IExchangeStrategy exchangeStrategy;
private readonly Lazy<SingleThreadTaskQueue> taskQueue = new Lazy<SingleThreadTaskQueue>();
2019-01-24 21:52:21 +00:00
// These fields are for use in the taskQueue only!
2016-12-05 22:41:17 +00:00
private RabbitMQ.Client.IConnection connection;
2016-12-11 14:08:58 +00:00
private IModel channelInstance;
2019-01-24 21:52:21 +00:00
private TaskCompletionSource<int> publishResultTaskSource;
2017-02-12 20:43:30 +00:00
public TapetiWorker(IConfig config)
{
2017-02-12 20:43:30 +00:00
this.config = config;
logger = config.DependencyResolver.Resolve<ILogger>();
2017-02-12 20:43:30 +00:00
messageSerializer = config.DependencyResolver.Resolve<IMessageSerializer>();
routingKeyStrategy = config.DependencyResolver.Resolve<IRoutingKeyStrategy>();
exchangeStrategy = config.DependencyResolver.Resolve<IExchangeStrategy>();
}
2016-11-17 16:33:27 +00:00
2019-01-24 21:52:21 +00:00
public Task Publish(object message, IBasicProperties properties, bool mandatory)
2016-11-17 16:33:27 +00:00
{
2019-01-24 21:52:21 +00:00
return Publish(message, properties, exchangeStrategy.GetExchange(message.GetType()), routingKeyStrategy.GetRoutingKey(message.GetType()), mandatory);
}
2019-01-24 21:52:21 +00:00
public Task PublishDirect(object message, string queueName, IBasicProperties properties, bool mandatory)
{
2019-01-24 21:52:21 +00:00
return Publish(message, properties, "", queueName, mandatory);
2016-11-17 16:33:27 +00:00
}
2016-12-11 14:08:58 +00:00
public Task Consume(string queueName, IEnumerable<IBinding> bindings)
2016-11-17 16:33:27 +00:00
{
if (string.IsNullOrEmpty(queueName))
throw new ArgumentNullException(nameof(queueName));
return taskQueue.Value.Add(() =>
{
GetChannel().BasicConsume(queueName, false, new TapetiConsumer(this, queueName, config.DependencyResolver, bindings, config.MessageMiddleware, config.CleanupMiddleware));
});
2016-11-17 16:33:27 +00:00
}
public Task Subscribe(IQueue queue)
{
return taskQueue.Value.Add(() =>
2016-12-11 14:08:58 +00:00
{
var channel = GetChannel();
2016-12-11 14:08:58 +00:00
if (queue.Dynamic)
{
2018-08-31 17:17:56 +00:00
var dynamicQueue = channel.QueueDeclare(queue.Name);
(queue as IDynamicQueue)?.SetName(dynamicQueue.QueueName);
2016-12-11 14:08:58 +00:00
foreach (var binding in queue.Bindings)
{
if (binding.QueueBindingMode == QueueBindingMode.RoutingKey)
{
var routingKey = routingKeyStrategy.GetRoutingKey(binding.MessageClass);
var exchange = exchangeStrategy.GetExchange(binding.MessageClass);
channel.QueueBind(dynamicQueue.QueueName, exchange, routingKey);
}
2016-12-11 14:08:58 +00:00
(binding as IBuildBinding)?.SetQueueName(dynamicQueue.QueueName);
}
2016-12-11 14:08:58 +00:00
}
else
{
channel.QueueDeclarePassive(queue.Name);
foreach (var binding in queue.Bindings)
{
(binding as IBuildBinding)?.SetQueueName(queue.Name);
}
}
});
}
public Task Respond(ulong deliveryTag, ConsumeResponse response)
{
return taskQueue.Value.Add(() =>
{
switch (response)
{
case ConsumeResponse.Ack:
GetChannel().BasicAck(deliveryTag, false);
break;
case ConsumeResponse.Nack:
GetChannel().BasicNack(deliveryTag, false, false);
break;
case ConsumeResponse.Requeue:
GetChannel().BasicNack(deliveryTag, false, true);
break;
}
});
}
public Task Close()
{
if (!taskQueue.IsValueCreated)
return Task.CompletedTask;
return taskQueue.Value.Add(() =>
{
2016-12-11 14:08:58 +00:00
if (channelInstance != null)
{
2016-12-11 14:08:58 +00:00
channelInstance.Dispose();
channelInstance = null;
}
// ReSharper disable once InvertIf
if (connection != null)
{
connection.Dispose();
connection = null;
}
taskQueue.Value.Dispose();
});
}
2019-01-24 21:52:21 +00:00
private Task Publish(object message, IBasicProperties properties, string exchange, string routingKey, bool mandatory)
{
2017-02-12 20:43:30 +00:00
var context = new PublishContext
{
2017-02-12 20:43:30 +00:00
DependencyResolver = config.DependencyResolver,
Exchange = exchange,
RoutingKey = routingKey,
Message = message,
Properties = properties ?? new BasicProperties()
};
2017-02-12 20:43:30 +00:00
if (!context.Properties.IsTimestampPresent())
context.Properties.Timestamp = new AmqpTimestamp(new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds());
2017-02-12 20:43:30 +00:00
if (!context.Properties.IsDeliveryModePresent())
context.Properties.DeliveryMode = 2; // Persistent
2017-02-12 20:43:30 +00:00
// ReSharper disable ImplicitlyCapturedClosure - MiddlewareHelper will not keep a reference to the lambdas
return MiddlewareHelper.GoAsync(
config.PublishMiddleware,
async (handler, next) => await handler.Handle(context, next),
() => taskQueue.Value.Add(() =>
2017-02-12 20:43:30 +00:00
{
var body = messageSerializer.Serialize(context.Message, context.Properties);
2019-01-24 21:52:21 +00:00
Task<int> publishResultTask = null;
if (config.UsePublisherConfirms)
{
publishResultTaskSource = new TaskCompletionSource<int>();
publishResultTask = publishResultTaskSource.Task;
}
else
mandatory = false;
GetChannel(PublishMaxConnectAttempts).BasicPublish(context.Exchange, context.RoutingKey, mandatory, context.Properties, body);
2019-01-24 21:52:21 +00:00
if (publishResultTask == null)
return;
2019-01-24 21:52:21 +00:00
if (!publishResultTask.Wait(MandatoryReturnTimeout))
throw new TimeoutException($"Timeout while waiting for basic.return for message with class {context.Message?.GetType().FullName ?? "null"} and Id {context.Properties.MessageId}");
2019-01-24 21:52:21 +00:00
var replyCode = publishResultTask.Result;
2019-01-24 21:52:21 +00:00
// There is no RabbitMQ.Client.Framing.Constants value for this "No route" reply code
// at the time of writing...
if (replyCode == 312)
throw new NoRouteException($"Mandatory message with class {context.Message?.GetType().FullName ?? "null"} does not have a route");
if (replyCode > 0)
throw new NoRouteException($"Mandatory message with class {context.Message?.GetType().FullName ?? "null"} could not be delivery, reply code {replyCode}");
}));
2017-02-12 20:43:30 +00:00
// ReSharper restore ImplicitlyCapturedClosure
}
/// <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>
private IModel GetChannel(int? maxAttempts = null)
{
2016-12-11 14:08:58 +00:00
if (channelInstance != null)
return channelInstance;
var attempts = 0;
var connectionFactory = new ConnectionFactory
{
HostName = ConnectionParams.HostName,
Port = ConnectionParams.Port,
VirtualHost = ConnectionParams.VirtualHost,
UserName = ConnectionParams.Username,
Password = ConnectionParams.Password,
AutomaticRecoveryEnabled = true, // The created connection is an IRecoverable
TopologyRecoveryEnabled = false, // We'll manually redeclare all queues in the Reconnect event to update the internal state for dynamic queues
RequestedHeartbeat = 30
};
while (true)
{
try
{
logger.Connect(ConnectionParams);
connection = connectionFactory.CreateConnection();
2016-12-11 14:08:58 +00:00
channelInstance = connection.CreateModel();
2019-01-24 21:52:21 +00:00
channelInstance.ConfirmSelect();
if (ConnectionParams.PrefetchCount > 0)
channelInstance.BasicQos(0, ConnectionParams.PrefetchCount, false);
((IRecoverable)connection).Recovery += (sender, e) => ConnectionEventListener?.Reconnected();
2019-01-24 21:52:21 +00:00
channelInstance.ModelShutdown += (sender, eventArgs) => ConnectionEventListener?.Disconnected();
channelInstance.BasicReturn += (sender, eventArgs) =>
{
publishResultTaskSource?.SetResult(eventArgs.ReplyCode);
publishResultTaskSource = null;
};
channelInstance.BasicAcks += (sender, eventArgs) =>
{
publishResultTaskSource?.SetResult(0);
publishResultTaskSource = null;
};
ConnectionEventListener?.Connected();
logger.ConnectSuccess(ConnectionParams);
break;
}
catch (BrokerUnreachableException e)
{
logger.ConnectFailed(ConnectionParams, e);
attempts++;
if (maxAttempts.HasValue && attempts > maxAttempts.Value)
throw;
Thread.Sleep(ReconnectDelay);
}
}
2016-12-11 14:08:58 +00:00
return channelInstance;
}
2017-02-12 20:43:30 +00:00
private class PublishContext : IPublishContext
{
public IDependencyResolver DependencyResolver { get; set; }
public string Exchange { get; set; }
public string RoutingKey { get; set; }
public object Message { get; set; }
public IBasicProperties Properties { get; set; }
}
}
}