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 ;
2016-11-16 22:11:05 +00:00
using RabbitMQ.Client ;
2016-11-20 13:34:50 +00:00
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 ;
2016-11-20 13:34:50 +00:00
using Tapeti.Tasks ;
2016-11-16 22:11:05 +00:00
namespace Tapeti.Connection
{
public class TapetiWorker
{
2017-02-21 21:08:05 +00:00
private const int ReconnectDelay = 5000 ;
2019-01-24 21:52:21 +00:00
private const int MandatoryReturnTimeout = 30000 ;
2017-02-21 21:08:05 +00:00
private const int PublishMaxConnectAttempts = 3 ;
2017-02-12 20:43:30 +00:00
private readonly IConfig config ;
2019-01-08 15:36:52 +00:00
private readonly ILogger logger ;
2016-11-21 19:54:29 +00:00
public TapetiConnectionParams ConnectionParams { get ; set ; }
2017-07-14 10:33:09 +00:00
public IConnectionEventListener ConnectionEventListener { get ; set ; }
2016-11-16 22:11:05 +00:00
2016-11-20 13:34:50 +00:00
private readonly IMessageSerializer messageSerializer ;
private readonly IRoutingKeyStrategy routingKeyStrategy ;
2017-02-07 15:13:33 +00:00
private readonly IExchangeStrategy exchangeStrategy ;
2016-11-20 13:34:50 +00:00
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 ;
2016-11-20 13:34:50 +00:00
2017-02-12 20:43:30 +00:00
public TapetiWorker ( IConfig config )
2016-11-20 13:34:50 +00:00
{
2017-02-12 20:43:30 +00:00
this . config = config ;
2017-02-07 15:13:33 +00:00
2019-01-08 15:36:52 +00:00
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-20 13:34:50 +00:00
}
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 ) ;
2017-01-31 11:01:08 +00:00
}
2016-11-20 13:34:50 +00:00
2017-01-31 11:01:08 +00:00
2019-01-24 21:52:21 +00:00
public Task PublishDirect ( object message , string queueName , IBasicProperties properties , bool mandatory )
2017-01-31 11:01:08 +00:00
{
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
{
2017-02-12 14:18:12 +00:00
if ( string . IsNullOrEmpty ( queueName ) )
throw new ArgumentNullException ( nameof ( queueName ) ) ;
2019-01-25 13:52:09 +00:00
return taskQueue . Value . Add ( ( ) = >
2016-11-20 13:34:50 +00:00
{
2019-01-25 13:52:09 +00:00
GetChannel ( ) . BasicConsume ( queueName , false , new TapetiConsumer ( this , queueName , config . DependencyResolver , bindings , config . MessageMiddleware , config . CleanupMiddleware ) ) ;
} ) ;
2016-11-17 16:33:27 +00:00
}
2016-11-16 22:11:05 +00:00
2017-02-12 14:18:12 +00:00
public Task Subscribe ( IQueue queue )
2016-11-16 22:11:05 +00:00
{
2019-01-25 13:52:09 +00:00
return taskQueue . Value . Add ( ( ) = >
2016-12-11 14:08:58 +00:00
{
2019-01-25 13:52:09 +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 ) ;
2017-02-12 14:18:12 +00:00
( queue as IDynamicQueue ) ? . SetName ( dynamicQueue . QueueName ) ;
2016-12-11 14:08:58 +00:00
foreach ( var binding in queue . Bindings )
{
2017-02-15 21:05:01 +00:00
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
2017-07-21 12:14:19 +00:00
( binding as IBuildBinding ) ? . SetQueueName ( dynamicQueue . QueueName ) ;
2017-01-31 11:01:08 +00:00
}
2016-12-11 14:08:58 +00:00
}
2017-02-12 14:18:12 +00:00
else
2017-07-21 12:14:19 +00:00
{
2017-02-12 14:18:12 +00:00
channel . QueueDeclarePassive ( queue . Name ) ;
2017-07-21 12:14:19 +00:00
foreach ( var binding in queue . Bindings )
{
( binding as IBuildBinding ) ? . SetQueueName ( queue . Name ) ;
}
}
2019-01-25 13:52:09 +00:00
} ) ;
2016-11-20 13:34:50 +00:00
}
public Task Respond ( ulong deliveryTag , ConsumeResponse response )
{
2019-01-25 13:52:09 +00:00
return taskQueue . Value . Add ( ( ) = >
2016-11-16 22:11:05 +00:00
{
2016-11-20 13:34:50 +00:00
switch ( response )
{
case ConsumeResponse . Ack :
2019-01-25 13:52:09 +00:00
GetChannel ( ) . BasicAck ( deliveryTag , false ) ;
2016-11-20 13:34:50 +00:00
break ;
case ConsumeResponse . Nack :
2019-01-25 13:52:09 +00:00
GetChannel ( ) . BasicNack ( deliveryTag , false , false ) ;
2016-11-20 13:34:50 +00:00
break ;
case ConsumeResponse . Requeue :
2019-01-25 13:52:09 +00:00
GetChannel ( ) . BasicNack ( deliveryTag , false , true ) ;
2016-11-20 13:34:50 +00:00
break ;
}
2019-01-25 13:52:09 +00:00
} ) ;
2016-11-20 13:34:50 +00:00
}
public Task Close ( )
{
if ( ! taskQueue . IsValueCreated )
return Task . CompletedTask ;
2016-11-16 22:11:05 +00:00
2016-11-20 13:34:50 +00:00
return taskQueue . Value . Add ( ( ) = >
{
2016-12-11 14:08:58 +00:00
if ( channelInstance ! = null )
2016-11-20 13:34:50 +00:00
{
2016-12-11 14:08:58 +00:00
channelInstance . Dispose ( ) ;
channelInstance = null ;
2016-11-20 13:34:50 +00:00
}
// ReSharper disable once InvertIf
if ( connection ! = null )
{
connection . Dispose ( ) ;
connection = null ;
}
taskQueue . Value . Dispose ( ) ;
} ) ;
2016-11-16 22:11:05 +00:00
}
2019-01-24 21:52:21 +00:00
private Task Publish ( object message , IBasicProperties properties , string exchange , string routingKey , bool mandatory )
2017-01-31 11:01:08 +00:00
{
2017-02-12 20:43:30 +00:00
var context = new PublishContext
2017-01-31 11:01:08 +00:00
{
2017-02-12 20:43:30 +00:00
DependencyResolver = config . DependencyResolver ,
Exchange = exchange ,
RoutingKey = routingKey ,
Message = message ,
Properties = properties ? ? new BasicProperties ( )
} ;
2017-01-31 11:01:08 +00:00
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 18:15:56 +00:00
2017-02-12 20:43:30 +00:00
if ( ! context . Properties . IsDeliveryModePresent ( ) )
context . Properties . DeliveryMode = 2 ; // Persistent
2017-01-31 11:01:08 +00:00
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 ) ,
2019-01-25 13:52:09 +00:00
( ) = > 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 ;
2019-01-25 13:52:09 +00:00
GetChannel ( PublishMaxConnectAttempts ) . BasicPublish ( context . Exchange , context . RoutingKey , mandatory , context . Properties , body ) ;
2019-01-24 21:52:21 +00:00
2019-01-25 13:52:09 +00:00
if ( publishResultTask = = null )
return ;
2019-01-24 21:52:21 +00:00
2019-01-25 13:52:09 +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
2019-01-25 13:52:09 +00:00
var replyCode = publishResultTask . Result ;
2019-01-24 21:52:21 +00:00
2019-01-25 13:52:09 +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
2017-01-31 11:01:08 +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>
2019-01-25 13:52:09 +00:00
private IModel GetChannel ( int? maxAttempts = null )
2016-11-16 22:11:05 +00:00
{
2016-12-11 14:08:58 +00:00
if ( channelInstance ! = null )
return channelInstance ;
2016-11-16 22:11:05 +00:00
2017-02-21 21:08:05 +00:00
var attempts = 0 ;
2016-11-16 22:11:05 +00:00
var connectionFactory = new ConnectionFactory
{
2016-11-21 19:54:29 +00:00
HostName = ConnectionParams . HostName ,
Port = ConnectionParams . Port ,
VirtualHost = ConnectionParams . VirtualHost ,
UserName = ConnectionParams . Username ,
Password = ConnectionParams . Password ,
2017-07-14 10:33:09 +00:00
AutomaticRecoveryEnabled = true , // The created connection is an IRecoverable
2019-01-25 13:52:09 +00:00
TopologyRecoveryEnabled = false , // We'll manually redeclare all queues in the Reconnect event to update the internal state for dynamic queues
2016-11-20 13:34:50 +00:00
RequestedHeartbeat = 30
2016-11-16 22:11:05 +00:00
} ;
2016-11-20 13:34:50 +00:00
while ( true )
{
try
{
2019-01-08 15:36:52 +00:00
logger . Connect ( ConnectionParams ) ;
2016-11-20 13:34:50 +00:00
connection = connectionFactory . CreateConnection ( ) ;
2016-12-11 14:08:58 +00:00
channelInstance = connection . CreateModel ( ) ;
2019-01-24 21:52:21 +00:00
channelInstance . ConfirmSelect ( ) ;
2016-11-20 13:34:50 +00:00
2017-02-05 22:22:34 +00:00
if ( ConnectionParams . PrefetchCount > 0 )
channelInstance . BasicQos ( 0 , ConnectionParams . PrefetchCount , false ) ;
2017-07-14 10:33:09 +00:00
( ( 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 ;
} ;
2017-07-14 10:33:09 +00:00
ConnectionEventListener ? . Connected ( ) ;
2019-01-08 15:36:52 +00:00
logger . ConnectSuccess ( ConnectionParams ) ;
2016-11-20 13:34:50 +00:00
break ;
}
2019-01-08 15:36:52 +00:00
catch ( BrokerUnreachableException e )
2016-11-20 13:34:50 +00:00
{
2019-01-08 15:36:52 +00:00
logger . ConnectFailed ( ConnectionParams , e ) ;
2017-02-21 21:08:05 +00:00
attempts + + ;
if ( maxAttempts . HasValue & & attempts > maxAttempts . Value )
throw ;
2019-01-25 13:52:09 +00:00
Thread . Sleep ( ReconnectDelay ) ;
2016-11-20 13:34:50 +00:00
}
}
2016-11-16 22:11:05 +00:00
2016-12-11 14:08:58 +00:00
return channelInstance ;
2016-11-16 22:11:05 +00:00
}
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 ; }
}
2016-11-16 22:11:05 +00:00
}
}