private void CloseAmqpModel(AmqpModelContainer model) { if (model != null) { model.Close(); } }
private void RedeclareExchangesAndQueues(AmqpModelContainer model, string serviceName) { //Redeclare exchanges and queues every minute if exchanges and queues are transient, or the first time client is sending a message TimeSpan elapsedSinceLastDeclareExchange = TimeSpan.FromMilliseconds(Environment.TickCount - lastExchangeDeclareTickCount); //TODO: Partition elapsedSinceLastDeclareExchange by serviceName and connection so that redeclares take place on new servicenames and connections. //Discovering firstDeclare by comparing lastExchangeDeclareTickCount to zero is not perfect //because tickcount can wrap back to zero (through the negative number range), if client is running long enough. //However, redeclaring exchanges and queues are a safe operation, so this is okay if it occurs more than once in persistent queues. bool firstDeclare = lastExchangeDeclareTickCount == 0; if (firstDeclare || (!messagingConfig.PersistentWorkQueuesAndExchanges && (elapsedSinceLastDeclareExchange.TotalMilliseconds < 0 || elapsedSinceLastDeclareExchange.TotalSeconds > 60))) { if (!firstDeclare) { //All threads must attempt to declare exchanges and queues if it hasn't been previously declared //(for instance, all threads were started at once) //So do not swap out this value on first declare lastExchangeDeclareTickCount = Environment.TickCount; } AmqpUtils.DeclareExchangeAndQueues(model.Channel, messageMapper, messagingConfig, serviceName, exchangeDeclareSync, null); if (firstDeclare) { //Swap out this value after declaring on firstdeclare lastExchangeDeclareTickCount = Environment.TickCount; } } }
/// <summary>Send an HTTP request as an asynchronous operation.</summary> /// <returns>Returns <see cref="T:System.Threading.Tasks.Task`1" />.The task object representing the asynchronous operation.</returns> /// <param name="request">The HTTP request message to send.</param> /// <param name="cancellationToken">The cancellation token to cancel operation.</param> /// <exception cref="T:System.ArgumentNullException">The <paramref name="request" /> was null.</exception> public override Task <HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { if (request == null) { throw new ArgumentNullException("request"); } if (request.RequestUri == null && BaseAddress == null) { throw new InvalidOperationException("The request URI must either be set or BaseAddress must be set"); } if (disposed) { throw new ObjectDisposedException(GetType().FullName); } hasKickStarted = true; PrepareMessage(request); //Get Request Options RequestOptions requestOptions = GetRequestOptions(request); var messageProperties = GetMessagingProperties(requestOptions); //Determine if message expects a response TimeSpan requestTimeout = GetRequestTimeout(requestOptions); //TODO: expectingResponse has a slightly different meaning in publisher confirms //where timespan may be longer than zero but MessageExpectsReply is false //in which case the timeout only applies to how long to wait for the publisher confirmation. bool expectingResponse = requestTimeout != TimeSpan.Zero && GetExpectsReply(request); //Declare messaging resources ExpectedResponse arrival = null; AmqpModelContainer model = null; bool modelClosed = false; string correlationId = null; //Get channel pool and decide on RPC strategy var pool = connectionMgr.GetConnectedPool(); IRPCStrategy rpcStrategy = pool.IsDirectReplyToCapable && !Settings.DisableDirectReplies ? directStrategy : callbackStrategy; try { #region Ensure CallbackQueue is started (If Using CallbackQueue Strategy) rpcStrategy.StartStrategy(pool, expectingResponse); #endregion #region Populate BasicProperties //Fill BasicProperties BasicProperties basicProperties = new BasicProperties(); //Set message delivery mode -- Make message persistent if either: // 1. Properties.Persistent is true // 2. messagingConfig.PersistentMessages is true and Properties.Persistent is null // 3. messagingConfig.PersistentMessages is true and Properties.Persistent is true if (messageProperties.Persistent == true || (messagingConfig.PersistentMessages && messageProperties.Persistent != false)) { basicProperties.Persistent = true; } //Set Exchange Headers var exchangeHeaders = messageProperties.Headers ?? messageMapper.GetHeaders(request); if (exchangeHeaders != null) { basicProperties.Headers = exchangeHeaders; } if (expectingResponse) { //Set CorrelationId correlationId = correlationIdGen.GetNextId(); basicProperties.CorrelationId = correlationId; //Set Expiration if messageProperties doesn't override Client.Timeout, RequestOptions and MessageMapper. if (!messageProperties.Expiration.HasValue && requestTimeout != System.Threading.Timeout.InfiniteTimeSpan && (messagingConfig.MessageExpires == null || messagingConfig.MessageExpires(request))) { if (requestTimeout.TotalMilliseconds > Int32.MaxValue) { basicProperties.Expiration = Int32.MaxValue.ToString(); } else { basicProperties.Expiration = ((int)requestTimeout.TotalMilliseconds).ToString(); } } } else if (!messageProperties.Expiration.HasValue && (messagingConfig.MessageExpires == null || messagingConfig.MessageExpires(request))) { //Request has a zero timeout and the message mapper indicates it should expire and messageproperties expiration is not set: //Set the expiration to zero which means RabbitMQ will only transmit if there is a consumer ready to receive it. //If there is no ready consumer, RabbitMQ drops the message. See https://www.rabbitmq.com/ttl.html basicProperties.Expiration = "0"; } //Set expiration if set in message properties if (messageProperties.Expiration.HasValue) { if (messageProperties.Expiration != System.Threading.Timeout.InfiniteTimeSpan) { var expiration = messageProperties.Expiration.Value.Duration(); if (expiration.TotalMilliseconds > Int32.MaxValue) { basicProperties.Expiration = Int32.MaxValue.ToString(); } else { basicProperties.Expiration = ((int)expiration.TotalMilliseconds).ToString(); } } else { //Infinite Timespan indicates that message should never expire basicProperties.ClearExpiration(); } } #endregion #region Get Ready to Send Message model = rpcStrategy.GetModel(pool, false); var serviceName = (requestOptions == null || requestOptions.ServiceName == null) ? (messageMapper.GetServiceName(request) ?? String.Empty).Trim() : requestOptions.ServiceName.Trim(); RedeclareExchangesAndQueues(model, serviceName); //TODO: Check if cancellation token was set before operation even began var taskSource = new TaskCompletionSource <HttpResponseMessage>(); var exchangeKind = ExchangeKind.Direct; //TODO: Get ExchangeKind from CLient.Settings.ExchangeKind //TODO: Pull exchangeName from a concurrent dictionary that has a key of serviceName, exchangeKind //exchangeKind could be an index into arrays that have concurrentDictionaries. var exchangeName = AmqpUtils.GetExchangeName(messagingConfig, serviceName, exchangeKind); #endregion #region Start waiting for response //Start waiting for response if a request timeout is set. if (expectingResponse) { //TODO: Better to just check if cancellationHasbeen requested instead of checking if it's None if (!cancellationToken.Equals(System.Threading.CancellationToken.None)) { //TODO: Have cancellationtokens cancel event trigger callbackHandle //In fact turn this whole thing into an extension } arrival = rpcStrategy.PrepareForResponse(correlationId, basicProperties, model, request, requestTimeout, cancellationToken, taskSource); } #endregion #region Send Message //TODO: Implement routing to a different exchangeKind via substituting exchangeName //Send message model.Channel.BasicPublish(exchangeName, messageProperties.RoutingKey ?? messageMapper.GetRoutingKey(request, exchangeKind) ?? AmqpUtils.GetWorkQueueRoutingKey(), basicProperties, request.ToHttpRequestPacket().Serialize()); //Close channel if (!expectingResponse || rpcStrategy.ReturnModelAfterSending) { CloseAmqpModel(model); modelClosed = true; } #endregion #region Cleanup if not expecting response //Exit with OK result if no request timeout was set. if (!expectingResponse) { //TODO: Investigate adding a publisher confirm for zero timeout messages so we know that RabbitMQ did pick up the message before replying OK. //Might add extra complexity to this class. //Zero timespan means the client isn't interested in a response taskSource.SetResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = _emptyByteArrayContent }); rpcStrategy.CleanupMessagingResources(correlationId, arrival); } #endregion return(taskSource.Task); } catch (Exception ex) { //TODO: Log this if (model != null && !modelClosed) { if (expectingResponse && model.Flags == ChannelFlags.RPC || model.Flags == ChannelFlags.RPCWithPublisherConfirms) { //Model might still be in use in waiting thread and so unsafe to be recycled model.Discard = true; } CloseAmqpModel(model); } rpcStrategy.CleanupMessagingResources(correlationId, arrival); if (ex is HttpRequestException) { throw; } else { throw GetWrappedException("An error occurred while sending the request.", ex); } } }
private void StartCallbackQueueConsumer(AmqpChannelPooler pool) { //TODO: Double-checked locking -- make this better if (callbackConsumer == null || !isInConsumerLoop || !pool.Connection.IsOpen) { lock (restartConsumerSync) { if (!(callbackConsumer == null || !isInConsumerLoop || !pool.Connection.IsOpen)) { return; } //This method waits on this signal to make sure the callbackprocessor thread either started successfully or failed. ManualResetEventSlim consumerSignal = new ManualResetEventSlim(false); Exception consumerSignalException = null; Thread callBackProcessor = new Thread(p => { ConcurrentQueueingConsumer consumer = null; try { //Start consumer AmqpModelContainer channelContainer = null; try { channelContainer = pool.GetModel(ChannelFlags.Consumer); IModel channel = channelContainer.Channel; if (clientSettings.DisableDirectReplies || !channelContainer.IsDirectReplyToCapable) { DeclareIndirectReplyToQueue(channel, indirectReplyToQueueName); } consumer = new ConcurrentQueueingConsumer(channel, responseQueued); //Set consumerCancelled to true on consumer cancellation consumerCancelled = false; consumer.ConsumerCancelled += (s, e) => { consumerCancelled = true; }; channel.BasicQos(0, (ushort)clientSettings.PrefetchCount, false); //Start consumer: string replyToQueueName; if (clientSettings.DisableDirectReplies || !channelContainer.IsDirectReplyToCapable) { channel.BasicConsume(indirectReplyToQueueName, clientSettings.AckBehavior == ClientAckBehavior.Automatic, consumer); replyToQueueName = indirectReplyToQueueName; } else { //TODO: REMOVE LATER: This clause is never called because in this case the DirectReplyToRPCStrategy would be in use //instead of this strategy. //throw an InvalidOperationException instead channel.BasicConsume(RPCStrategyHelpers.DIRECT_REPLY_TO_QUEUENAME_ARG, true, consumer); //Discover direct reply to queue name replyToQueueName = DiscoverDirectReplyToQueueName(channel, indirectReplyToQueueName); } //Set callbackConsumer to consumer callbackQueueName = replyToQueueName; callbackConsumer = consumer; //Notify outer thread that channel has started consumption consumerSignal.Set(); BasicDeliverEventArgs evt; ExpectedResponse expected; isInConsumerLoop = true; while (true) { try { evt = DequeueCallbackQueue(); } catch { //TODO: Log this exception except it's ObjectDisposedException or OperationCancelledException throw; } expected = null; if (!String.IsNullOrEmpty(evt.BasicProperties.CorrelationId) && expectedResponses.TryRemove(evt.BasicProperties.CorrelationId, out expected)) { RPCStrategyHelpers.ReadAndSignalDelivery(expected, evt); } //Acknowledge receipt: //In ClientBehavior.Automatic mode //Client acks all received messages, even if it wasn't the expected one or even if it wasn't expecting anything. //This prevents a situation where crap messages are sent to the client but the good expected message is stuck behind the //crap ones and isn't delivered because the crap ones in front of the queue aren't acked and crap messages exceed prefetchCount. //In ClientAckBehavior.ValidResponses mode (and Direct Reply to is not in effect): //Client only acks expected messages if they could be deserialized //If not, they are rejected. if ((clientSettings.DisableDirectReplies || !channelContainer.IsDirectReplyToCapable) && clientSettings.AckBehavior == ClientAckBehavior.ValidResponses) { if (expected != null && expected.DeserializationException != null) { channel.BasicAck(evt.DeliveryTag, false); } else { channel.BasicReject(evt.DeliveryTag, false); } } //Exit loop if consumer is cancelled. if (consumerCancelled) { break; } } } finally { isInConsumerLoop = false; pool.SetRecycle(); if (channelContainer != null) { if (consumer != null && !consumerCancelled) { try { channelContainer.Channel.BasicCancel(consumer.ConsumerTag); } catch { } } channelContainer.Close(); } } } catch (Exception ex) { //TODO: Log error (Except it's object disposed exception) //Set Exception object which will be throw by signal waiter consumerSignalException = ex; //Notify outer thread to move on, in case it's still waiting try { consumerSignal.Set(); } catch { } } finally { if (pool != null) { pool.Dispose(); } } }); //Start Thread callBackProcessor.Name = "RestBus RabbitMQ Client Callback Queue Consumer"; callBackProcessor.IsBackground = true; callBackProcessor.Start(); //Wait for Thread to start consuming messages consumerSignal.Wait(); consumerSignal.Dispose(); //Examine exception if it were set and rethrow it Thread.MemoryBarrier(); //Ensure we have the non-cached version of consumerSignalException if (consumerSignalException != null) { throw consumerSignalException; } //No more code from this point in this method } } }
public ExpectedResponse PrepareForResponse(string correlationId, BasicProperties basicProperties, AmqpModelContainer model, HttpRequestMessage request, TimeSpan requestTimeout, CancellationToken cancellationToken, TaskCompletionSource <HttpResponseMessage> taskSource) { //Set Reply to queue basicProperties.ReplyTo = callbackQueueName; //Initialize response arrival object and add to expected responses dictionary var arrival = new ExpectedResponse(); expectedResponses[correlationId] = arrival; RPCStrategyHelpers.WaitForResponse(request, arrival, requestTimeout, model, false, cancellationToken, taskSource, () => CleanupMessagingResources(correlationId, arrival)); return(arrival); }
internal static void WaitForResponse(HttpRequestMessage request, ExpectedResponse arrival, TimeSpan requestTimeout, AmqpModelContainer model, bool closeModel, CancellationToken cancellationToken, TaskCompletionSource <HttpResponseMessage> taskSource, Action cleanup) { //Spawning a new task to wait on the MRESlim is slower than using ThreadPool.RegisterWaitForSingleObject // //TODO: Test task vs RegisterWaitForSingleObject modes in a super fast network environment with 40, 100, 200 all the way to 1000 threads to see what method has fastest throughput. #if WAIT_FOR_RESPONSE_IN_TASK_MODE //Wait for response arrival event on a new task Task.Factory.StartNew(() => { bool succeeded = arrival.ReceivedEvent.Wait( requestTimeout.TotalMilliseconds > Int32.MaxValue ? TimeSpan.FromMilliseconds(Int32.MaxValue) : requestTimeout /* Covers InfiniteTimeSpan */, cancellationToken); Thread.MemoryBarrier(); //Ensure non-cached versions of arrival are read //It failed either because it timed out or was cancelled //HttpClient treats both scenarios the same way. try { if (closeModel) { model.Close(); } SetResponseResult(request, !succeeded, arrival, taskSource); } catch { //TODO: Log this: // the code in the try block should be safe so this catch block should never be called, // hoewever, this delegate is called on a separate thread and should be protected. } finally { CleanupMessagingResources(correlationId, arrival); } }, cancellationToken); #else //Wait for response arrival event on the ThreadPool: var localVariableInitLock = new object(); lock (localVariableInitLock) { //TODO: Have cancellationToken signal WaitHandle so that threadpool stops waiting. RegisteredWaitHandle callbackHandle = null; callbackHandle = ThreadPool.RegisterWaitForSingleObject(arrival.ReceivedEvent.WaitHandle, (state, timedOut) => { //TODO: Investigate, this memorybarrier might be unnecessary since the thread is released from the threadpool //after deserializationException and responsePacket is set. Thread.MemoryBarrier(); //Ensure non-cached versions of arrival are read try { //TODO: Check Cancelation Token when it's implemented if (closeModel) { model.Close(); } SetResponseResult(request, timedOut, arrival, taskSource); lock (localVariableInitLock) { callbackHandle.Unregister(null); } } catch { //TODO: Log this: // the code in the try block should be safe so this catch block should never be called, // hoewever, this delegate is called on a separate thread and should be protected. } finally { cleanup(); } }, null, requestTimeout == System.Threading.Timeout.InfiniteTimeSpan ? System.Threading.Timeout.Infinite : (long)requestTimeout.TotalMilliseconds, true); } #endif }
public ExpectedResponse PrepareForResponse(string correlationId, BasicProperties basicProperties, AmqpModelContainer model, HttpRequestMessage request, TimeSpan requestTimeout, CancellationToken cancellationToken, TaskCompletionSource <HttpResponseMessage> taskSource) { //Set Reply to queue basicProperties.ReplyTo = RPCStrategyHelpers.DIRECT_REPLY_TO_QUEUENAME_ARG; var rpcModel = (RPCModelContainer)model; var arrival = new ExpectedResponse(rpcModel.ReceivedResponseEvent); rpcModel.ExpectResponse(correlationId, arrival); RPCStrategyHelpers.WaitForResponse(request, arrival, requestTimeout, model, true, cancellationToken, taskSource, () => CleanupMessagingResources(correlationId, arrival)); return(arrival); }
public void SendResponse(MessageContext context, HttpResponsePacket response) { if (disposed) { throw new ObjectDisposedException(GetType().FullName); } var dispatch = context.Dispatch as MessageDispatch; if (dispatch != null) { //Ack request if (Settings.AckBehavior != SubscriberAckBehavior.Automatic && dispatch.Consumer.Model.IsOpen) { dispatch.Consumer.Model.BasicAck(dispatch.Delivery.DeliveryTag, false); //NOTE: The call above takes place in different threads silmultaneously //In which case multiple threads will be using the same channel at the same time. //It's okay in this case, because transmissions within a channel are synchronized, as seen in: //https://github.com/rabbitmq/rabbitmq-dotnet-client/blob/f16c093f6409e11d9d77115038cb224eb39468ec/projects/client/RabbitMQ.Client/src/client/impl/ModelBase.cs#L459 //and //https://github.com/rabbitmq/rabbitmq-dotnet-client/blob/f16c093f6409e11d9d77115038cb224eb39468ec/projects/client/RabbitMQ.Client/src/client/impl/SessionBase.cs#L177 } } //Exit method if no replyToQueue was specified. if (String.IsNullOrEmpty(context.ReplyToQueue)) { return; } if (_subscriberPool.Connection == null) { //TODO: Log this -- it technically shouldn't happen. Also translate to a HTTP Unreachable because it means StartCallbackQueueConsumer didn't create a connection throw new InvalidOperationException("This is Bad"); } //Add/Update Subscriber-Id header response.Headers[Common.Shared.SUBSCRIBER_ID_HEADER] = subscriberIdHeader; //Send response var pooler = _subscriberPool; AmqpModelContainer model = null; try { model = pooler.GetModel(ChannelFlags.None); BasicProperties basicProperties = new BasicProperties { CorrelationId = context.CorrelationId }; model.Channel.BasicPublish(String.Empty, context.ReplyToQueue, basicProperties, response.Serialize()); } finally { if (model != null) { model.Close(); } } }
public void Restart() { hasStarted.Set(true); //CLose connections and channels if (subscriberChannel != null) { if (subscriberConsumer != null) { try { subscriberChannel.Channel.BasicCancel(subscriberConsumer.ConsumerTag); } catch { } } try { subscriberChannel.Close(); } catch { } } if (workChannel != null) { if (workConsumer != null) { try { workChannel.Channel.BasicCancel(workConsumer.ConsumerTag); } catch { } } try { workChannel.Close(); } catch { } } if (_subscriberPool != null) { _subscriberPool.Dispose(); } //NOTE: CreateConnection() can throw BrokerUnreachableException //That's okay because the exception needs to propagate to Reconnect() or Start() var conn = connectionFactory.CreateConnection(); if (connectionBroken != null) { connectionBroken.Dispose(); } connectionBroken = new CancellationTokenSource(); if (stopWaitingOnQueue != null) { stopWaitingOnQueue.Dispose(); } stopWaitingOnQueue = CancellationTokenSource.CreateLinkedTokenSource(disposedCancellationSource.Token, connectionBroken.Token); var pool = new AmqpChannelPooler(conn); _subscriberPool = pool; //Use pool reference henceforth. //Create work channel and declare exchanges and queues workChannel = pool.GetModel(ChannelFlags.Consumer); //Redeclare exchanges and queues AmqpUtils.DeclareExchangeAndQueues(workChannel.Channel, messageMapper, messagingConfig, serviceName, exchangeDeclareSync, Id); //Listen on work queue workConsumer = new ConcurrentQueueingConsumer(workChannel.Channel, requestQueued); string workQueueName = AmqpUtils.GetWorkQueueName(messagingConfig, serviceName); workChannel.Channel.BasicQos(0, (ushort)Settings.PrefetchCount, false); workChannel.Channel.BasicConsume(workQueueName, Settings.AckBehavior == SubscriberAckBehavior.Automatic, workConsumer); //Listen on subscriber queue subscriberChannel = pool.GetModel(ChannelFlags.Consumer); subscriberConsumer = new ConcurrentQueueingConsumer(subscriberChannel.Channel, requestQueued); string subscriberWorkQueueName = AmqpUtils.GetSubscriberQueueName(serviceName, Id); subscriberChannel.Channel.BasicQos(0, (ushort)Settings.PrefetchCount, false); subscriberChannel.Channel.BasicConsume(subscriberWorkQueueName, Settings.AckBehavior == SubscriberAckBehavior.Automatic, subscriberConsumer); //Cancel connectionBroken on connection/consumer problems pool.Connection.ConnectionShutdown += (s, e) => { connectionBroken.Cancel(); }; workConsumer.ConsumerCancelled += (s, e) => { connectionBroken.Cancel(); }; subscriberConsumer.ConsumerCancelled += (s, e) => { connectionBroken.Cancel(); }; }
internal static void WaitForResponse(HttpRequestMessage request, ExpectedResponse arrival, TimeSpan requestTimeout, AmqpModelContainer model, bool closeModel, CancellationToken cancellationToken, TaskCompletionSource <HttpResponseMessage> taskSource, Action cleanup) { var localVariableInitLock = new object(); lock (localVariableInitLock) { //TODO: Have cancellationToken signal WaitHandle so that threadpool stops waiting. RegisteredWaitHandle callbackHandle = null; callbackHandle = ThreadPool.RegisterWaitForSingleObject(arrival.ReceivedEvent.WaitHandle, (state, timedOut) => { //TODO: Investigate, this memorybarrier might be unnecessary since the thread is released from the threadpool //after deserializationException and responsePacket is set. Interlocked.MemoryBarrier(); //Ensure non-cached versions of arrival are read try { //TODO: Check Cancelation Token when it's implemented if (closeModel) { model.Close(); } SetResponseResult(request, timedOut, arrival, taskSource); lock (localVariableInitLock) { callbackHandle.Unregister(null); } } catch { //TODO: Log this: // the code in the try block should be safe so this catch block should never be called, // hoewever, this delegate is called on a separate thread and should be protected. } finally { cleanup(); } }, null, requestTimeout == System.Threading.Timeout.InfiniteTimeSpan ? System.Threading.Timeout.Infinite : (long)requestTimeout.TotalMilliseconds, true); } }