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

/SignalR/MessageBus/InProcessMessageBus.cs

https://github.com/kpmrafeeq/SignalR
C# | 359 lines | 275 code | 59 blank | 25 comment | 31 complexity | 95e878aba046befe34535fad46139acb MD5 | raw file
Possible License(s): MIT
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.Globalization;
  6. using System.Linq;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. using SignalR.Infrastructure;
  10. namespace SignalR
  11. {
  12. public class InProcessMessageBus : InProcessMessageBus<ulong>
  13. {
  14. public InProcessMessageBus(IDependencyResolver resolver)
  15. : this(resolver.Resolve<ITraceManager>(),
  16. garbageCollectMessages: true)
  17. {
  18. }
  19. public InProcessMessageBus(ITraceManager trace, bool garbageCollectMessages)
  20. : base(trace,
  21. garbageCollectMessages,
  22. new DefaultIdGenerator())
  23. {
  24. }
  25. private class DefaultIdGenerator : IIdGenerator<ulong>
  26. {
  27. private ulong _id;
  28. public ulong GetNext()
  29. {
  30. return ++_id;
  31. }
  32. public ulong ConvertFromString(string value)
  33. {
  34. return UInt64.Parse(value, CultureInfo.InvariantCulture);
  35. }
  36. public string ConvertToString(ulong value)
  37. {
  38. return value.ToString(CultureInfo.InvariantCulture);
  39. }
  40. }
  41. }
  42. public class InProcessMessageBus<T> : IMessageBus where T : IComparable<T>
  43. {
  44. private static List<InMemoryMessage<T>> _emptyMessageList = new List<InMemoryMessage<T>>();
  45. private readonly ConcurrentDictionary<string, LockedList<Action<IList<InMemoryMessage<T>>>>> _waitingTasks =
  46. new ConcurrentDictionary<string, LockedList<Action<IList<InMemoryMessage<T>>>>>(StringComparer.OrdinalIgnoreCase);
  47. private readonly ConcurrentDictionary<string, LockedList<InMemoryMessage<T>>> _cache =
  48. new ConcurrentDictionary<string, LockedList<InMemoryMessage<T>>>(StringComparer.OrdinalIgnoreCase);
  49. private readonly ReaderWriterLockSlim _cacheLock = new ReaderWriterLockSlim();
  50. private readonly TimeSpan _cleanupInterval = TimeSpan.FromSeconds(10);
  51. private readonly IIdGenerator<T> _idGenerator;
  52. private T _lastMessageId;
  53. private long _gcRunning = 0;
  54. private readonly Timer _timer;
  55. private readonly ITraceManager _trace;
  56. public InProcessMessageBus(IDependencyResolver resolver, IIdGenerator<T> idGenerator)
  57. : this(resolver.Resolve<ITraceManager>(),
  58. garbageCollectMessages: true,
  59. idGenerator: idGenerator)
  60. {
  61. }
  62. public InProcessMessageBus(ITraceManager traceManager, bool garbageCollectMessages, IIdGenerator<T> idGenerator)
  63. {
  64. _trace = traceManager;
  65. _idGenerator = idGenerator;
  66. if (garbageCollectMessages)
  67. {
  68. _timer = new Timer(RemoveExpiredEntries, null, _cleanupInterval, _cleanupInterval);
  69. }
  70. }
  71. private TraceSource Trace
  72. {
  73. get
  74. {
  75. return _trace["SignalR.InProcessMessageBus"];
  76. }
  77. }
  78. public Task<MessageResult> GetMessages(IEnumerable<string> eventKeys, string id, CancellationToken cancel)
  79. {
  80. if (String.IsNullOrEmpty(id))
  81. {
  82. // Wait for new messages
  83. Trace.TraceInformation("New connection waiting for messages");
  84. return WaitForMessages(eventKeys, cancel, default(T));
  85. }
  86. try
  87. {
  88. // We need to lock here in case messages are added to the bus while we're reading
  89. _cacheLock.EnterReadLock();
  90. T uuid = _idGenerator.ConvertFromString(id);
  91. if (uuid.CompareTo(_lastMessageId) > 0)
  92. {
  93. // BUG 24: Connection already has the latest message, so reset the id
  94. // This can happen if the server is reset (appdomain or entire server incase of self host)
  95. Trace.TraceInformation("Connection asking for message id {0} when the largest is {1}. Resetting id", id, _lastMessageId);
  96. uuid = default(T);
  97. }
  98. else if (uuid.CompareTo(_lastMessageId) == 0)
  99. {
  100. // Connection already has the latest message, so start wating
  101. Trace.TraceInformation("Connection waiting for new messages from id {0}", uuid);
  102. return WaitForMessages(eventKeys, cancel, uuid);
  103. }
  104. var messages = eventKeys.SelectMany(key => GetMessagesSince(key, uuid));
  105. if (messages.Any())
  106. {
  107. // Messages already in store greater than last received id so return them
  108. Trace.TraceInformation("Connection getting messages from cache from id {0}", uuid);
  109. return TaskAsyncHelper.FromResult(GetMessageResult(messages.OrderBy(msg => msg.Id).ToList()));
  110. }
  111. // Wait for new messages
  112. Trace.TraceInformation("Connection waiting for new messages from id {0}", uuid);
  113. return WaitForMessages(eventKeys, cancel, uuid);
  114. }
  115. finally
  116. {
  117. _cacheLock.ExitReadLock();
  118. }
  119. }
  120. public Task Send(string connectionId, string eventKey, object value)
  121. {
  122. var list = _cache.GetOrAdd(eventKey, _ => new LockedList<InMemoryMessage<T>>());
  123. InMemoryMessage<T> message = null;
  124. try
  125. {
  126. // Take a write lock here so we ensure messages go into the list in order
  127. _cacheLock.EnterWriteLock();
  128. // Only 1 save allowed at a time, to ensure messages are added to the list in order
  129. message = new InMemoryMessage<T>(eventKey, value, GenerateId());
  130. Trace.TraceInformation("Saving message {0} with eventKey '{1}' to cache on AppDomain {2}", message.Id, eventKey, AppDomain.CurrentDomain.Id);
  131. list.AddWithLock(message);
  132. // Send to waiting callers.
  133. // This must be done in the write lock to ensure that messages are sent to waiting
  134. // connections in the order they were saved so that they always get the correct
  135. // last message id to resubscribe with. Moving this outside the lock can enable
  136. // a subsequent send to overtake the previous send, resulting in the waiting connection
  137. // getting a last message id that is after the first save, hence missing a message.
  138. Broadcast(eventKey, message);
  139. }
  140. finally
  141. {
  142. _cacheLock.ExitWriteLock();
  143. }
  144. return TaskAsyncHelper.Empty;
  145. }
  146. private T GenerateId()
  147. {
  148. return _lastMessageId = _idGenerator.GetNext();
  149. }
  150. private void Broadcast(string eventKey, InMemoryMessage<T> message)
  151. {
  152. LockedList<Action<IList<InMemoryMessage<T>>>> callbacks;
  153. if (_waitingTasks.TryGetValue(eventKey, out callbacks))
  154. {
  155. var delegates = callbacks.CopyWithLock();
  156. var messages = new[] { message };
  157. if (delegates.Count == 0)
  158. {
  159. Trace.TraceInformation("Sending message {0} with eventKey '{1}' to 0 waiting connections", message.Id, eventKey);
  160. return;
  161. }
  162. Trace.TraceInformation("Sending message {0} with eventKey '{1}' to {2} waiting connections", message.Id, eventKey, delegates.Count);
  163. foreach (var callback in delegates)
  164. {
  165. if (callback != null)
  166. {
  167. callback.Invoke(messages);
  168. }
  169. }
  170. }
  171. }
  172. private IList<InMemoryMessage<T>> GetMessagesSince(string eventKey, T id)
  173. {
  174. LockedList<InMemoryMessage<T>> list = null;
  175. _cache.TryGetValue(eventKey, out list);
  176. if (list == null || list.CountWithLock == 0)
  177. {
  178. return _emptyMessageList;
  179. }
  180. // Create a snapshot so that we ensure the list isn't modified within this scope
  181. var snapshot = list.CopyWithLock();
  182. if (snapshot.Count > 0 && snapshot[0].Id.CompareTo(id) > 0)
  183. {
  184. // All messages in the list are greater than the last message
  185. return snapshot;
  186. }
  187. var index = snapshot.FindLastIndex(msg => msg.Id.CompareTo(id) <= 0);
  188. if (index < 0)
  189. {
  190. return _emptyMessageList;
  191. }
  192. var startIndex = index + 1;
  193. if (startIndex >= snapshot.Count)
  194. {
  195. return _emptyMessageList;
  196. }
  197. return snapshot.GetRange(startIndex, snapshot.Count - startIndex);
  198. }
  199. private Task<MessageResult> WaitForMessages(IEnumerable<string> eventKeys, CancellationToken cancel, T lastId)
  200. {
  201. var tcs = new TaskCompletionSource<MessageResult>();
  202. int callbackCalled = 0;
  203. Action<IList<InMemoryMessage<T>>> callback = null;
  204. CancellationTokenRegistration registration = default(CancellationTokenRegistration);
  205. registration = cancel.Register(() =>
  206. {
  207. try
  208. {
  209. if (Interlocked.Exchange(ref callbackCalled, 1) == 0)
  210. {
  211. string id = _idGenerator.ConvertToString(_lastMessageId);
  212. tcs.TrySetResult(new MessageResult(id));
  213. }
  214. // Remove callback for all keys
  215. foreach (var eventKey in eventKeys)
  216. {
  217. LockedList<Action<IList<InMemoryMessage<T>>>> callbacks;
  218. if (_waitingTasks.TryGetValue(eventKey, out callbacks))
  219. {
  220. callbacks.RemoveWithLock(callback);
  221. }
  222. }
  223. }
  224. finally
  225. {
  226. registration.Dispose();
  227. }
  228. });
  229. callback = receivedMessages =>
  230. {
  231. try
  232. {
  233. // REVIEW: Consider the case where lastId is a referene type and is null.
  234. // What wouls this return? Does it matter?
  235. var messages = receivedMessages.Where(m => m.Id.CompareTo(lastId) > 0)
  236. .ToList();
  237. if (messages.Count == 0)
  238. {
  239. return;
  240. }
  241. if (Interlocked.Exchange(ref callbackCalled, 1) == 0)
  242. {
  243. tcs.TrySetResult(GetMessageResult(messages));
  244. }
  245. // Remove callback for all keys
  246. foreach (var eventKey in eventKeys)
  247. {
  248. LockedList<Action<IList<InMemoryMessage<T>>>> callbacks;
  249. if (_waitingTasks.TryGetValue(eventKey, out callbacks))
  250. {
  251. callbacks.RemoveWithLock(callback);
  252. }
  253. }
  254. }
  255. finally
  256. {
  257. registration.Dispose();
  258. }
  259. };
  260. // Add callback for all keys
  261. foreach (var eventKey in eventKeys)
  262. {
  263. var callbacks = _waitingTasks.GetOrAdd(eventKey, _ => new LockedList<Action<IList<InMemoryMessage<T>>>>());
  264. callbacks.AddWithLock(callback);
  265. }
  266. return tcs.Task;
  267. }
  268. private MessageResult GetMessageResult(IList<InMemoryMessage<T>> messages)
  269. {
  270. var id = messages[messages.Count - 1].Id;
  271. return new MessageResult(messages.ToList<Message>(), _idGenerator.ConvertToString(id));
  272. }
  273. private void RemoveExpiredEntries(object state)
  274. {
  275. if (Interlocked.Exchange(ref _gcRunning, 1) == 1 || Debugger.IsAttached)
  276. {
  277. return;
  278. }
  279. try
  280. {
  281. // Take a snapshot of the entries
  282. var entries = _cache.ToList();
  283. // Remove all the expired ones
  284. foreach (var entry in entries)
  285. {
  286. entry.Value.RemoveWithLock(item => item.Expired);
  287. }
  288. }
  289. catch (Exception ex)
  290. {
  291. // Exception on bg thread, bad! Log and swallow to stop the process exploding
  292. Trace.TraceInformation("Error during InProcessMessageStore clean up on background thread: {0}", ex);
  293. }
  294. finally
  295. {
  296. Interlocked.Exchange(ref _gcRunning, 0);
  297. }
  298. }
  299. }
  300. }