private bool TryGetRequest(ConcurrentQueueingConsumer consumer, out HttpRequestPacket request, out MessageDispatch dispatch) { request = null; dispatch = null; BasicDeliverEventArgs item; if (!consumer.TryInstantDequeue(out item, throwIfClosed: false)) { return false; } //TODO: Pool MessageDispatch //Get message dispatch = new MessageDispatch { Consumer = consumer, Delivery = item }; //Deserialize message bool wasDeserialized = true; try { request = HttpRequestPacket.Deserialize(item.Body); } catch { wasDeserialized = false; } if (wasDeserialized) { //Add/Update Subscriber-Id header request.Headers[Common.Shared.SUBSCRIBER_ID_HEADER] = this.subscriberIdHeader; //Add redelivered header if item was redelivered. if (item.Redelivered) { request.Headers[Common.Shared.REDELIVERED_HEADER] = TRUE_STRING_ARRAY; } } //Reject message if deserialization failed. else if (!wasDeserialized && Settings.AckBehavior != SubscriberAckBehavior.Automatic ) { consumer.Model.BasicReject(item.DeliveryTag, false); return false; } return true; }
//Will block until a request is received from either queue public MessageContext Dequeue() { if (disposed) throw new ObjectDisposedException(GetType().FullName); if(workConsumer == null || subscriberConsumer == null) throw new InvalidOperationException("Start the subscriber prior to calling Dequeue"); //TODO: Test what happens if either of these consumers are cancelled by the server, should consumer.Cancelled be handled? //In that scenario, requestQueued.Wait below should throw an exception and try to reconnect. HttpRequestPacket request; MessageDispatch dispatch; ConcurrentQueueingConsumer queue1 = null, queue2 = null; while (true) { if (disposed) throw new ObjectDisposedException(GetType().FullName); if (lastProcessedConsumerQueue == subscriberConsumer) { queue1 = workConsumer; queue2 = subscriberConsumer; } else { queue1 = subscriberConsumer; queue2 = workConsumer; } try { if (TryGetRequest(queue1, out request, out dispatch)) { lastProcessedConsumerQueue = queue1; break; } if (TryGetRequest(queue2, out request, out dispatch)) { lastProcessedConsumerQueue = queue2; break; } } catch (Exception e) { //TODO: Log this -- no exception is expected from the calls in the try block. throw; } try { requestQueued.Wait(stopWaitingOnQueue.Token); } catch (OperationCanceledException) { if (!disposed && connectionBroken.IsCancellationRequested) { //Connection broken or consumer has been cancelled but client is not disposed //So reconnect Reconnect(); } else { throw; } } requestQueued.Reset(); } return new MessageContext { Request = request, ReplyToQueue = dispatch.Delivery.BasicProperties == null ? null : dispatch.Delivery.BasicProperties.ReplyTo, CorrelationId = dispatch.Delivery.BasicProperties.CorrelationId, Dispatch = dispatch }; }
//TODO: REMOVE LATER: This method is never called because the DirectReplyToStrategy is used when the client decides to use //the direct reply to feature. /// <summary> /// Discovers the Direct reply-to queue name ( https://www.rabbitmq.com/direct-reply-to.html ) by messaging itself. /// </summary> private static string DiscoverDirectReplyToQueueName(IModel channel, string indirectReplyToQueueName) { DeclareIndirectReplyToQueue(channel, indirectReplyToQueueName); var receiver = new ConcurrentQueueingConsumer(channel); var receiverTag = channel.BasicConsume(indirectReplyToQueueName, true, receiver); channel.BasicPublish(String.Empty, indirectReplyToQueueName, true, new BasicProperties { ReplyTo = RPCStrategyHelpers.DIRECT_REPLY_TO_QUEUENAME_ARG }, new byte[0]); BasicDeliverEventArgs delivery; using (ManualResetEventSlim messageReturned = new ManualResetEventSlim()) { EventHandler<BasicReturnEventArgs> returnHandler = null; Interlocked.Exchange(ref returnHandler, (a, e) => { messageReturned.Set(); try { receiver.Model.BasicReturn -= returnHandler; } catch { } }); receiver.Model.BasicReturn += returnHandler; System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch(); watch.Start(); while (!receiver.TryInstantDequeue(out delivery, throwIfClosed: false)) { Thread.Sleep(1); if (watch.Elapsed > TimeSpan.FromSeconds(10) || messageReturned.IsSet) { break; } } watch.Stop(); if (!messageReturned.IsSet) { try { receiver.Model.BasicReturn -= returnHandler; } catch { } } try { channel.BasicCancel(receiverTag); } catch { } } if (delivery == null) { throw new InvalidOperationException("Unable to determine direct reply-to queue name."); } var result = delivery.BasicProperties.ReplyTo; if (result == null || result == RPCStrategyHelpers.DIRECT_REPLY_TO_QUEUENAME_ARG || !result.StartsWith(RPCStrategyHelpers.DIRECT_REPLY_TO_QUEUENAME_ARG)) { throw new InvalidOperationException("Discovered direct reply-to queue name (" + (result ?? "null") + ") was not in expected format."); } return result; }
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(); }; }
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 } } }