PageRenderTime 51ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/ssbwcf/SsbTransportChannel/SsbConversationGroupReceiver.cs

#
C# | 469 lines | 338 code | 62 blank | 69 comment | 43 complexity | c78fe5098b7f70921fa7fd160f4ec909 MD5 | raw file
  1. //Copyright (c) Microsoft Corporation. All rights reserved.
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Text;
  5. using System.Data.SqlClient;
  6. using System.ServiceModel.Channels;
  7. using System.Data;
  8. using System.ServiceModel;
  9. using System.Threading;
  10. using System.Collections;
  11. using System.Transactions;
  12. namespace Microsoft.Samples.SsbTransportChannel
  13. {
  14. public class SsbConversationGroupReceiver : CommunicationObject
  15. {
  16. SqlConnection con;
  17. CommittableTransaction tx;
  18. SqlDataReader rdr;
  19. Guid cgId;
  20. ServiceInfo serviceInfo;
  21. object rdrlock;
  22. ManualResetEvent cancelEvent = new ManualResetEvent(false);
  23. SsbChannelListener channelListener;
  24. class ReceivedMessage
  25. {
  26. public int MessageLength;
  27. public SsbConversationContext Conversation;
  28. public byte[] MessageBody;
  29. }
  30. ReceivedMessage queuedResult;
  31. internal SsbConversationGroupReceiver(SqlConnection cn, CommittableTransaction tx, Guid cgId, ServiceInfo serviceInfo, SsbChannelListener channelListener)
  32. {
  33. this.con = cn;
  34. this.tx = tx;
  35. this.cgId = cgId;
  36. this.serviceInfo = serviceInfo;
  37. this.rdrlock = new object();
  38. this.channelListener = channelListener;
  39. }
  40. /// <summary>
  41. /// This method is only called if the SsbInputSessionChannel is configured to listen on a specific
  42. /// Conversation Group. In this case we will have to wait on a RECIEVE instead of on GET CONVERSATION GROUP
  43. /// </summary>
  44. /// <param name="timeout"></param>
  45. /// <returns></returns>
  46. internal bool WaitForFirstMessage(TimeSpan timeout, WaitHandle waitCancelEvent)
  47. {
  48. ThrowIfDisposedOrNotOpen();
  49. if (queuedResult != null)
  50. {
  51. throw new InvalidOperationException("There is already a queued result");
  52. }
  53. int messageLength;
  54. SsbConversationContext conversation;
  55. //respect the timeout here since we expect to have to wait on the first message.
  56. byte[] messageBody = Receive(timeout, null, out messageLength, out conversation, waitCancelEvent);
  57. if (messageBody == null)
  58. {
  59. return false;
  60. }
  61. queuedResult = new ReceivedMessage();
  62. queuedResult.Conversation = conversation;
  63. queuedResult.MessageBody = messageBody;
  64. queuedResult.MessageLength = messageLength;
  65. return true;
  66. }
  67. internal byte[] Receive(TimeSpan timeout, BufferManager bm, out int messageLength, out SsbConversationContext conversation)
  68. {
  69. return Receive(timeout, bm, out messageLength, out conversation, cancelEvent);
  70. }
  71. internal byte[] Receive(TimeSpan timeout, BufferManager bm, out int messageLength, out SsbConversationContext conversation,WaitHandle cancelEvent)
  72. {
  73. ThrowIfDisposedOrNotOpen();
  74. byte[] message = null;
  75. conversation = null;
  76. messageLength = 0;
  77. if (queuedResult != null)
  78. {
  79. ReceivedMessage r = queuedResult;
  80. queuedResult = null;
  81. conversation = r.Conversation;
  82. messageLength = r.MessageLength;
  83. byte[] buf = bm.TakeBuffer(messageLength);
  84. Buffer.BlockCopy(r.MessageBody, 0, buf, 0, messageLength);
  85. return buf;
  86. }
  87. try
  88. {
  89. lock (rdrlock)
  90. {
  91. //If this is the first time, open the reader, otherwise read the next row
  92. if ( rdr == null || !rdr.Read() )
  93. {
  94. //the last bactch has been processed. Close the reader.
  95. if (rdr != null)
  96. {
  97. rdr.Close();
  98. }
  99. rdr = GetMessageBatch(timeout,cancelEvent);
  100. //this is a timeout condition
  101. //caused by aborting or closing the reciever
  102. if ( rdr == null )
  103. {
  104. return null;
  105. }
  106. //this is a timeout condition caused by the WAITFOR expiring
  107. if( !rdr.Read() )
  108. {
  109. rdr.Close();
  110. //return the Receiver to it's initial state.
  111. rdr = null;
  112. return null;
  113. }
  114. }
  115. int i = 0;
  116. int conversation_handle = i++;
  117. int service_name = i++;
  118. int message_type_name = i++;
  119. int message_body = i++;
  120. int message_sequence_number = i++;
  121. Guid conversationHandle = rdr.GetGuid(conversation_handle);
  122. string ServiceName = rdr.GetString(service_name);
  123. string messageTypeName = rdr.GetString(message_type_name);
  124. if (messageTypeName != SsbConstants.SsbEndDialogMessage && messageTypeName != SsbConstants.SsbDialogTimerMessage)
  125. {
  126. //this eliminates a one copy because the message_body
  127. //isn't copied into the row buffer. Instead it's chunked directly
  128. //into the message byte[].
  129. // CONSIDER (dbrowne) wraping the reader in a custom stream implementation
  130. //and pass that back instead to eliminate another copy.
  131. messageLength = (int)rdr.GetBytes(message_body, 0, null, 0, 0);
  132. if (bm == null)
  133. {
  134. message = new byte[messageLength];
  135. }
  136. else
  137. {
  138. message = bm.TakeBuffer(messageLength);
  139. }
  140. int br = (int)rdr.GetBytes(message_body, 0, message, 0, messageLength);
  141. if (br != messageLength) //should never happen
  142. {
  143. throw new Exception("Failed to read all the message bytes");
  144. }
  145. }
  146. long sequence = rdr.GetInt64(message_sequence_number);
  147. conversation = new SsbConversationContext(conversationHandle, this.cgId, sequence, messageTypeName);
  148. if (messageTypeName == SsbConstants.SsbErrorMessage)
  149. {
  150. System.Xml.XmlDocument doc = new System.Xml.XmlDocument();
  151. doc.Load(new System.IO.MemoryStream(message, 0, messageLength));
  152. System.Xml.XmlNamespaceManager mgr = new System.Xml.XmlNamespaceManager(doc.NameTable);
  153. mgr.AddNamespace("er", SsbConstants.SsbErrorMessage);
  154. int code = int.Parse(doc.SelectSingleNode("/er:Error/er:Code", mgr).InnerText);
  155. string ermsg = doc.SelectSingleNode("/er:Error/er:Description", mgr).InnerText;
  156. throw new ProtocolException(string.Format("Service Broker Error Message {0}: {1}", code, ermsg));
  157. }
  158. SsbInstrumentation.MessageRecieved(messageLength);
  159. return message;
  160. //if (messageTypeName == SsbConstants.SsbDialogTimerMessage)
  161. //{
  162. // //well now we're running again after a lag, now what?
  163. //}
  164. }
  165. }
  166. catch (SqlException ex)
  167. {
  168. //the timeout will not result in a SqlExecption since the timeout value is passed to WAITFOR RECIEVE
  169. //if (!helper.IsTimeRemaining)
  170. //{
  171. // throw new TimeoutException(String.Format("Timed out while receiving. Timeout value was {0} seconds", timeout.TotalSeconds), ex);
  172. //}
  173. throw new CommunicationException(String.Format("An exception occurred while receiving from conversation group {0}.", this.cgId), ex);
  174. }
  175. }
  176. /// <summary>
  177. /// This is the method that actually receives Service Broker messages.
  178. /// </summary>
  179. /// <param name="timeout">Maximum time to wait for a message. This is passed to the RECIEVE command, not used as a SqlCommandTimeout</param>
  180. /// <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>
  181. /// <returns></returns>
  182. SqlDataReader GetMessageBatch(TimeSpan timeout, WaitHandle cancelEvent)
  183. {
  184. string SQL = string.Format(@"
  185. waitfor(
  186. RECEIVE conversation_handle,service_name,message_type_name,message_body,message_sequence_number
  187. FROM [{0}] WHERE conversation_group_id = @cgid
  188. ), timeout @timeout", this.serviceInfo.QueueName);
  189. SqlCommand cmd = new SqlCommand(SQL, this.con);
  190. SqlParameter pConversation = cmd.Parameters.Add("@cgid", SqlDbType.UniqueIdentifier);
  191. pConversation.Value = this.cgId;
  192. SqlParameter pTimeout = cmd.Parameters.Add("@timeout", SqlDbType.Int);
  193. pTimeout.Value = TimeoutHelper.ToMilliseconds(timeout);
  194. cmd.CommandTimeout = 0; //honor the RECIEVE timeout, whatever it is.
  195. //Run the command, but abort if the another thread wants to run Close or Abort
  196. IAsyncResult result = cmd.BeginExecuteReader(CommandBehavior.SequentialAccess);
  197. int rc = WaitHandle.WaitAny(new WaitHandle[] { result.AsyncWaitHandle, cancelEvent });
  198. if (rc == 1) //cancel event
  199. {
  200. cmd.Cancel();
  201. TraceHelper.TraceEvent(System.Diagnostics.TraceEventType.Verbose,"Canceling Service Broker wait on ConversationGroupReciever shutdown","GetMessageBatch");
  202. return null;
  203. }
  204. if (rc != 0)
  205. {
  206. throw new InvalidOperationException("Unexpected state");
  207. }
  208. return cmd.EndExecuteReader(result);
  209. }
  210. public Guid ConversationGroupId
  211. {
  212. get { return this.cgId; }
  213. }
  214. /// <summary>
  215. /// Convenience method for configuraing a callback channel. Since this is not a duplex channel it doesn't
  216. /// support the declarative callbacks in the normal WCF method.
  217. /// </summary>
  218. /// <typeparam name="TContract"></typeparam>
  219. /// <returns></returns>
  220. public TContract GetCallback<TContract>()
  221. {
  222. ThrowIfDisposedOrNotOpen();
  223. try
  224. {
  225. Binding binding = channelListener.CreateResponseBinding();
  226. TContract channel = ChannelFactory<TContract>.CreateChannel(binding,new EndpointAddress(SsbUri.Default));
  227. SsbConversationSender sender = ((IClientChannel)channel).GetProperty<SsbConversationSender>();
  228. sender.SetConnection(con);
  229. ((IClientChannel)channel).Open();
  230. SsbConversationContext conversation = OperationContext.Current.IncomingMessageProperties[SsbConstants.SsbConversationMessageProperty] as SsbConversationContext;
  231. sender.OpenConversation(conversation.ConversationHandle);
  232. return channel;
  233. }
  234. catch (Exception ex)
  235. {
  236. throw new CommunicationException("An error occurred while obtaining callback channel: " + ex.Message, ex);
  237. }
  238. }
  239. public void EndConversation()
  240. {
  241. SsbConversationContext conversation = OperationContext.Current.IncomingMessageProperties[SsbConstants.SsbConversationMessageProperty] as SsbConversationContext;
  242. this.EndConversation(conversation.ConversationHandle, this.DefaultCloseTimeout);
  243. }
  244. public void SetConversationTimer(Guid conversationHandle, TimeSpan timerTimeout)
  245. {
  246. SsbHelper.SetConversationTimer(conversationHandle, timerTimeout, this.con);
  247. }
  248. internal void EndConversation(Guid conversationHandle, TimeSpan timeout)
  249. {
  250. ThrowIfDisposedOrNotOpen();
  251. TimeoutHelper helper = new TimeoutHelper(timeout);
  252. try
  253. {
  254. string SQL = "END CONVERSATION @ConversationHandle";
  255. using (SqlCommand cmd = new SqlCommand(SQL, con))
  256. {
  257. cmd.CommandTimeout = helper.RemainingTimeInMillisecondsOrZero();
  258. SqlParameter pConversation = cmd.Parameters.Add("@ConversationHandle", SqlDbType.UniqueIdentifier);
  259. pConversation.Value = conversationHandle;
  260. cmd.ExecuteNonQuery();
  261. }
  262. }
  263. catch (SqlException ex)
  264. {
  265. if (!helper.IsTimeRemaining)
  266. {
  267. throw new TimeoutException(String.Format("Timed out while ending conversation {0}. Timeout value was {1} seconds", conversationHandle,timeout.TotalSeconds), ex);
  268. }
  269. else
  270. {
  271. throw new CommunicationException(String.Format("An exception occurred while ending conversation {0}.", conversationHandle), ex);
  272. }
  273. }
  274. }
  275. /// <summary>
  276. /// Ends the Service Broker conversation and sends a custom error message on the conversation.
  277. /// Issues this command to SQL SErver
  278. /// END CONVERSATION @ConversationHandle WITH ERROR = @errorCode DESCRIPTION = @errorDescription
  279. /// </summary>
  280. /// <param name="errorCode">The error code passed to END CONVERSATION</param>
  281. ///
  282. /// <param name="errorDescription">The Error description passed to END CONVERSATION</param>
  283. ///
  284. public void EndConversationWithError(int errorCode, string errorDescription)
  285. {
  286. ThrowIfDisposedOrNotOpen();
  287. SsbConversationContext conversation = OperationContext.Current.IncomingMessageProperties[SsbConstants.SsbConversationMessageProperty] as SsbConversationContext;
  288. try
  289. {
  290. string SQL = "END CONVERSATION @ConversationHandle WITH ERROR = @error DESCRIPTION = @description";
  291. SqlCommand cmd = new SqlCommand(SQL, con);
  292. cmd.Parameters.Add("@ConversationHandle", SqlDbType.UniqueIdentifier).Value = conversation.ConversationHandle;
  293. cmd.Parameters.Add("@error", SqlDbType.Int).Value = errorCode;
  294. cmd.Parameters.Add("@description", SqlDbType.NVarChar, 3000).Value = errorDescription;
  295. cmd.ExecuteNonQuery();
  296. }
  297. catch (SqlException ex)
  298. {
  299. throw new CommunicationException(String.Format("An exception occurred while ending conversation {0}.", conversation.ConversationHandle), ex);
  300. }
  301. }
  302. protected override TimeSpan DefaultCloseTimeout
  303. {
  304. get { return SsbConstants.DefaultCloseTimeout; }
  305. }
  306. protected override TimeSpan DefaultOpenTimeout
  307. {
  308. get { return SsbConstants.DefaultOpenTimeout; }
  309. }
  310. /// <summary>
  311. /// The Reciever always needs a transaction to protect its conversation, but a Service may wish to enlist other work
  312. /// in the transaction. TakeTransaction will detach the transaction that the conversation uses from the reciever and allow
  313. /// the server to enlist other work in it and control its fate.
  314. /// </summary>
  315. /// <returns></returns>
  316. public CommittableTransaction TakeTransaction()
  317. {
  318. CommittableTransaction tran = this.tx;
  319. tx = null;
  320. return tran;
  321. }
  322. /// <summary>
  323. /// The Reciever always needs a transaction to protect its conversation, but a Service may wish to enlist other work
  324. /// in the transaction. GetTransaction will return a non-commitable reference to the Reciever's transaction for the
  325. /// service to enlist work in, but the transaction will still be commited on closing the Reciever, or rolled back on
  326. /// aborting it.
  327. /// </summary>
  328. /// <returns></returns>
  329. public Transaction GetTransaction()
  330. {
  331. return this.tx.Clone();
  332. }
  333. /// <summary>
  334. /// This method just allows service code to execute SqlCommands over the connection used by the Reciever. Any commands executed
  335. /// over this connection will automatically be enlisted in the Reciever's transaction.
  336. /// </summary>
  337. /// <returns></returns>
  338. public SqlConnection GetConnection()
  339. {
  340. return con;
  341. }
  342. protected override void OnAbort()
  343. {
  344. cancelEvent.Set();
  345. lock (rdrlock)
  346. {
  347. if (this.rdr != null)
  348. {
  349. this.rdr.Dispose();
  350. }
  351. if (this.tx != null)
  352. {
  353. this.tx.Rollback();
  354. }
  355. if (this.con != null)
  356. {
  357. this.con.Dispose();
  358. }
  359. }
  360. }
  361. protected override void OnClose(TimeSpan timeout)
  362. {
  363. cancelEvent.Set();
  364. lock (rdrlock)
  365. {
  366. if (this.rdr != null)
  367. {
  368. this.rdr.Dispose();
  369. }
  370. //The ConversationGroupReciver
  371. if (this.tx != null && tx.TransactionInformation.Status == TransactionStatus.Active)
  372. {
  373. this.tx.Commit();
  374. }
  375. if (this.con != null)
  376. {
  377. this.con.Dispose();
  378. }
  379. }
  380. }
  381. protected override void OnOpen(TimeSpan timeout)
  382. {
  383. }
  384. #region Async Wrappers
  385. protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state)
  386. {
  387. CloseDelegate d = new CloseDelegate(this.Close);
  388. return d.BeginInvoke(timeout, callback, state);
  389. }
  390. protected override void OnEndClose(IAsyncResult result)
  391. {
  392. CloseDelegate d = new CloseDelegate(this.Close);
  393. d.EndInvoke(result);
  394. }
  395. protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state)
  396. {
  397. OpenDelegate d = new OpenDelegate(this.Open);
  398. return d.BeginInvoke(timeout, callback, state);
  399. }
  400. protected override void OnEndOpen(IAsyncResult result)
  401. {
  402. OpenDelegate d = new OpenDelegate(this.Open);
  403. d.EndInvoke(result);
  404. }
  405. #endregion
  406. }
  407. }