/// <summary> /// This is the method that actually receives Service Broker messages. /// </summary> /// <param name="timeout">Maximum time to wait for a message. This is passed to the RECIEVE command, not used as a SqlCommandTimeout</param> /// <param name="cancelEvent">An event to cancel the wait. Async ADO.NET is used to enable the thread to wait for either completion or cancel</param> /// <returns></returns> SqlDataReader GetMessageBatch(TimeSpan timeout, WaitHandle cancelEvent) { string SQL = string.Format(@" waitfor( RECEIVE conversation_handle,service_name,message_type_name,message_body,message_sequence_number FROM [{0}] WHERE conversation_group_id = @cgid ), timeout @timeout", this.serviceInfo.QueueName); SqlCommand cmd = new SqlCommand(SQL, this.con); SqlParameter pConversation = cmd.Parameters.Add("@cgid", SqlDbType.UniqueIdentifier); pConversation.Value = this.cgId; SqlParameter pTimeout = cmd.Parameters.Add("@timeout", SqlDbType.Int); pTimeout.Value = TimeoutHelper.ToMilliseconds(timeout); cmd.CommandTimeout = 0; //honor the RECIEVE timeout, whatever it is. //Run the command, but abort if the another thread wants to run Close or Abort IAsyncResult result = cmd.BeginExecuteReader(CommandBehavior.SequentialAccess); int rc = WaitHandle.WaitAny(new WaitHandle[] { result.AsyncWaitHandle, cancelEvent }); if (rc == 1) //cancel event { cmd.Cancel(); TraceHelper.TraceEvent(System.Diagnostics.TraceEventType.Verbose,"Canceling Service Broker wait on ConversationGroupReciever shutdown","GetMessageBatch"); return null; } if (rc != 0) { throw new InvalidOperationException("Unexpected state"); } return cmd.EndExecuteReader(result); }
public Message Receive(TimeSpan timeout) { TraceHelper.TraceMethod("SsbInputSessionChannel.Receive"); ThrowIfDisposedOrNotOpen(); byte[] buffer = null; SsbConversationContext conversation; int messageLength; //Don't linger waiting for messages. When the last message is received, go ahead and close the channel. //A new channel will be created if additional messages arive later on this conversation. To maintain //state between messages in a long-running conversation use a custom instance provider and/or save //the state to a database keyed by the ConversationGroup // TODO (dbrowne) consider allowing linger except after a conversation has ended as a performance enhancement. buffer = this.cg.Receive(TimeSpan.Zero, this.bufferManager, out messageLength, out conversation); TraceHelper.TraceEvent(System.Diagnostics.TraceEventType.Verbose, "SsbConversationGroupReceiver.Receiver returned", "SsbInputSessionChannel.Receive"); if (conversation != null && conversation.MessageTypeName == SsbConstants.SsbEndDialogMessage) { TraceHelper.TraceEvent(System.Diagnostics.TraceEventType.Verbose, "EndDialogMessage received, ending conversation", "SsbInputSessionChannel.Receive"); //End the conversation on our end. There's nothing else we can can do, so we might as well end the conversation //on our end, so when we commit, the conversation will be cleaned up. cg.EndConversation(conversation.ConversationHandle, this.DefaultSendTimeout); return(null); } // CONSIDER (yassers) raising events for first and last message in conversation else if (buffer == null) { TraceHelper.TraceEvent(System.Diagnostics.TraceEventType.Verbose, "SsbConversationGroupReceiver.Receiver returned null, no messages available", "SsbInputSessionChannel.Receive"); return(null); //there are no more messages available } Message message = this.encoder.ReadMessage(new ArraySegment <byte>(buffer, 0, messageLength), this.bufferManager); TraceHelper.TraceEvent(System.Diagnostics.TraceEventType.Verbose, "Decoded message", "SsbInputSessionChannel.Receive"); message.Properties.Add(SsbConstants.SsbConversationMessageProperty, conversation); //since we received the message it must be coming from the right queue //the To header isn't going to match the address that the Endpoint filter is looking for //so we fix that here message.Headers.To = this.listenAddress; TraceHelper.TraceEvent(System.Diagnostics.TraceEventType.Verbose, "Returning message", "SsbInputSessionChannel.Receive"); return(message); }
void cn_InfoMessage(object sender, SqlInfoMessageEventArgs e) { TraceHelper.TraceEvent(System.Diagnostics.TraceEventType.Verbose, "SqlConnection.InfoMessage " + e.Message, "cn_InfoMessage"); }
void tx_TransactionCompleted(object sender, TransactionEventArgs e) { TraceHelper.TraceEvent(System.Diagnostics.TraceEventType.Verbose, string.Format("SsbChannelListener Transaction completed - {0}", e.Transaction.TransactionInformation.Status), "tx_TransactionCompleted"); }
internal SsbConversationGroupReceiver AcceptConversationGroup(TimeSpan timeout) { TimeoutHelper helper = new TimeoutHelper(timeout); try { //create a transaction to own the conversation. Open the SqlConnection while //the transaction is current, so all the work on the connection will be enlisted //in the transaction. SqlTransaction doesn't work this way. It requires every command //to be manually enlisted, yuck. //As an alternative you could simply issue a new SqlCommand("begin transaction",cn).ExecuteNonQuery() //to start a transaction. // TODO (dbrowne) Consider whether to allow service-level code to commit this transaction, or, alternatively, //commit the transaction after the last conversation message in this session is processed. //If service-level code is allowed to commit the transaction then SsbConversationGroupReciver _must_ //retrieve only a single message in each batch. Else it could fetch 2 messages and risk the service-level //commiting after the first message. CommittableTransaction tx = new CommittableTransaction(TimeSpan.MaxValue); Transaction currentTrans = Transaction.Current; Transaction.Current = tx; SqlConnection cn = SsbHelper.GetConnection(this.connstring); cn.Open(); Transaction.Current = currentTrans; tx.TransactionCompleted += new TransactionCompletedEventHandler(tx_TransactionCompleted); //wait for a new message, but if that message is in a conversation group that another SsbChannelListener is waiting on //then roll back the conversation group lock and get back in line. The waiting SsbChannelListener should pick it up. string sql = String.Format(@" retry: save transaction swiper_no_swiping declare @cg uniqueidentifier declare @rc int waitfor (get conversation group @cg from [{0}]), TIMEOUT @timeout if @cg is null begin return --timeout end exec @rc = sp_getapplock @Resource=@cg, @LockMode='Exclusive', @LockTimeout = 0 if @rc = -1 begin print 'skipping message for locked conversation_group' rollback transaction swiper_no_swiping goto retry end set @conversation_group_id = @cg ", this.serviceInfo.QueueName); SqlCommand cmd = new SqlCommand(sql, cn); cmd.CommandTimeout = 0; cn.InfoMessage += new SqlInfoMessageEventHandler(cn_InfoMessage); SqlParameter pTimeout = cmd.Parameters.Add("@timeout", SqlDbType.Int); pTimeout.Value = TimeoutHelper.ToMilliseconds(timeout); SqlParameter pConversationGroupId = cmd.Parameters.Add("@conversation_group_id", SqlDbType.UniqueIdentifier); pConversationGroupId.Direction = ParameterDirection.Output; //Run the command, but abort if the another thread runs Close or Abort IAsyncResult result = cmd.BeginExecuteNonQuery(); int rc = WaitHandle.WaitAny(new WaitHandle[] { result.AsyncWaitHandle, cancelEvent }); if (rc == 1) //cancel event { cmd.Cancel(); TraceHelper.TraceEvent(System.Diagnostics.TraceEventType.Verbose, "Canceling Service Broker wait on SsbChannelListener shutdown", "AcceptConversationGroup"); cn.Close(); return(null); } if (rc != 0) { throw new InvalidOperationException("Unexpected state"); } cmd.EndExecuteNonQuery(result); if (pConversationGroupId.Value == null || pConversationGroupId.Value.GetType() != typeof(Guid)) { throw new TimeoutException(String.Format("Timed out while waiting for Conversation Group")); } Guid conversationGroupId = (Guid)pConversationGroupId.Value; TraceHelper.TraceEvent(System.Diagnostics.TraceEventType.Verbose, string.Format("Accepted conversation group {0}", conversationGroupId), "AcceptConversationGroup"); SsbConversationGroupReceiver cg = new SsbConversationGroupReceiver(cn, tx, conversationGroupId, this.serviceInfo, this); return(cg); } catch (SqlException ex) { if (!helper.IsTimeRemaining) { throw new TimeoutException(String.Format("Timed out while waiting for Conversation Group. Timeout value was {0} seconds", timeout.TotalSeconds), ex); } else { throw new ProtocolException("An error occurred while waiting for a Conversation Group", ex); } } }
internal SsbConversationGroupReceiver AcceptConversationGroup(Guid conversationGroupId, TimeSpan timeout) { TimeoutHelper helper = new TimeoutHelper(timeout); TransactionOptions to = new TransactionOptions(); to.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted; to.Timeout = TimeSpan.MaxValue; CommittableTransaction tx = new CommittableTransaction(to); Transaction currentTrans = Transaction.Current; Transaction.Current = tx; SqlConnection cn = SsbHelper.GetConnection(this.connstring); cn.Open(); Transaction.Current = currentTrans; //first lock the the Conversation Group. This will prevent other SsbConversationGroupReceivers //from poaching the messages. string sql = @" declare @cg uniqueidentifier declare @rc int set @cg = @conversation_group_id exec @rc = sp_getapplock @Resource=@cg, @LockMode='Exclusive' if @rc not in (0,1) begin raiserror('Failed to lock conversation group. sp_getapplock failure code %d.',16,1,@rc); end "; SqlCommand cmd = new SqlCommand(sql, cn); SqlParameter pConversationGroupId = cmd.Parameters.Add("@conversation_group_id", SqlDbType.UniqueIdentifier); pConversationGroupId.Value = conversationGroupId; cmd.CommandTimeout = helper.RemainingTimeInMillisecondsOrZero(); //Run the command, but abort if the another thread runs Close or Abort IAsyncResult result = cmd.BeginExecuteNonQuery(); int rc = WaitHandle.WaitAny(new WaitHandle[] { result.AsyncWaitHandle, cancelEvent }); if (rc == 1) //cancel event { cmd.Cancel(); TraceHelper.TraceEvent(System.Diagnostics.TraceEventType.Verbose, "Canceling Service Broker wait on SsbChannelListener shutdown", "AcceptConversationGroup"); cn.Close(); return(null); } if (rc != 0) { throw new InvalidOperationException("Unexpected state"); } cmd.EndExecuteNonQuery(result); SsbConversationGroupReceiver cg = new SsbConversationGroupReceiver(cn, tx, conversationGroupId, this.serviceInfo, this); cg.Open(); //now wait for a message on the conversation. Do it here because WCF expects the wait to be here instead of in a subsequent receive. //And also there is a different timeout to accepting a channel and waiting for additional messages. //But we need to pass in the cancelEvent to abort the listen, if this ChannelListener is closed or aborted if (!cg.WaitForFirstMessage(helper.RemainingTime(), cancelEvent)) { if (this.State != CommunicationState.Opened)//this ChannelListener was closed or aborted, so this is expected. { //for some reason the transaction tx is in an uncommitable state here. Not sure why. //however no other work has occured on this connection, so we can just shut it down. cn.Close(); return(null); } throw new TimeoutException(String.Format("Timed out while waiting for Conversation Group")); } return(cg); }