SqlDependencyUtils.cs 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. //------------------------------------------------------------------------------
  2. // <copyright file="SqlDependencyUtils.cs" company="Microsoft">
  3. // Copyright (c) Microsoft Corporation. All rights reserved.
  4. // </copyright>
  5. // <owner current="true" primary="true">[....]</owner>
  6. // <owner current="true" primary="true">[....]</owner>
  7. // <owner current="false" primary="false">[....]</owner>
  8. //------------------------------------------------------------------------------
  9. namespace System.Data.SqlClient {
  10. using System;
  11. using System.Collections;
  12. using System.Collections.Generic;
  13. using System.Data.Common;
  14. using System.Diagnostics;
  15. using System.Security.Principal;
  16. using System.Security.AccessControl;
  17. using System.Text;
  18. using System.Threading;
  19. // This is a singleton instance per AppDomain that acts as the notification dispatcher for
  20. // that AppDomain. It receives calls from the SqlDependencyProcessDispatcher with an ID or a server name
  21. // to invalidate matching dependencies in the given AppDomain.
  22. internal class SqlDependencyPerAppDomainDispatcher : MarshalByRefObject { // MBR, since ref'ed by ProcessDispatcher.
  23. // ----------------
  24. // Instance members
  25. // ----------------
  26. internal static readonly SqlDependencyPerAppDomainDispatcher
  27. SingletonInstance = new SqlDependencyPerAppDomainDispatcher(); // singleton object
  28. // Dependency ID -> Dependency hashtable. 1 -> 1 mapping.
  29. // 1) Used for ASP.Net to map from ID to dependency.
  30. // 2) Used to enumerate dependencies to invalidate based on server.
  31. private Dictionary<string, SqlDependency> _dependencyIdToDependencyHash;
  32. // holds dependencies list per notification and the command hash from which this notification was generated
  33. // command hash is needed to remove its entry from _commandHashToNotificationId when the notification is removed
  34. sealed class DependencyList : List<SqlDependency> {
  35. public readonly string CommandHash;
  36. internal DependencyList(string commandHash) {
  37. this.CommandHash = commandHash;
  38. }
  39. }
  40. // notificationId -> Dependencies hashtable: 1 -> N mapping. notificationId == appDomainKey + commandHash.
  41. // More than one dependency can be using the same command hash values resulting in a hash to the same value.
  42. // We use this to cache mapping between command to dependencies such that we may reduce the notification
  43. // resource effect on SQL Server. The Guid identifier is sent to the server during notification enlistment,
  44. // and returned during the notification event. Dependencies look up existing Guids, if one exists, to ensure
  45. // they are re-using notification ids.
  46. private Dictionary<string, DependencyList> _notificationIdToDependenciesHash;
  47. // CommandHash value -> notificationId associated with it: 1->1 mapping. This map is used to quickly find if we need to create
  48. // new notification or hookup into existing one.
  49. // CommandHash is built from connection string, command text and parameters
  50. private Dictionary<string, string> _commandHashToNotificationId;
  51. // TIMEOUT LOGIC DESCRIPTION
  52. //
  53. // Every time we add a dependency we compute the next, earlier timeout.
  54. //
  55. // We setup a timer to get a callback every 15 seconds. In the call back:
  56. // - If there are no active dependencies, we just return.
  57. // - If there are dependencies but none of them timed-out (compared to the "next timeout"),
  58. // we just return.
  59. // - Otherwise we Invalidate() those that timed-out.
  60. //
  61. // So the client-generated timeouts have a granularity of 15 seconds. This allows
  62. // for a simple and low-resource-consumption implementation.
  63. //
  64. // LOCKS: don't update _nextTimeout outside of the _dependencyHash.SyncRoot lock.
  65. private bool _SqlDependencyTimeOutTimerStarted = false;
  66. // Next timeout for any of the dependencies in the dependency table.
  67. private DateTime _nextTimeout;
  68. // Timer to periodically check the dependencies in the table and see if anyone needs
  69. // a timeout. We'll enable this only on demand.
  70. private Timer _timeoutTimer;
  71. // -----------
  72. // BID members
  73. // -----------
  74. private readonly int _objectID = System.Threading.Interlocked.Increment(ref _objectTypeCount);
  75. private static int _objectTypeCount; // Bid counter
  76. internal int ObjectID {
  77. get {
  78. return _objectID;
  79. }
  80. }
  81. private SqlDependencyPerAppDomainDispatcher() {
  82. IntPtr hscp;
  83. Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependencyPerAppDomainDispatcher|DEP> %d#", ObjectID);
  84. try {
  85. _dependencyIdToDependencyHash = new Dictionary<string, SqlDependency>();
  86. _notificationIdToDependenciesHash = new Dictionary<string, DependencyList>();
  87. _commandHashToNotificationId = new Dictionary<string, string>();
  88. _timeoutTimer = new Timer(new TimerCallback(TimeoutTimerCallback), null, Timeout.Infinite, Timeout.Infinite);
  89. // If rude abort - we'll leak. This is acceptable for now.
  90. AppDomain.CurrentDomain.DomainUnload += new EventHandler(this.UnloadEventHandler);
  91. }
  92. finally {
  93. Bid.ScopeLeave(ref hscp);
  94. }
  95. }
  96. // SQL Hotfix 236
  97. // When remoted across appdomains, MarshalByRefObject links by default time out if there is no activity
  98. // within a few minutes. Add this override to prevent marshaled links from timing out.
  99. public override object InitializeLifetimeService() {
  100. return null;
  101. }
  102. // ------
  103. // Events
  104. // ------
  105. private void UnloadEventHandler(object sender, EventArgs e) {
  106. IntPtr hscp;
  107. Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependencyPerAppDomainDispatcher.UnloadEventHandler|DEP> %d#", ObjectID);
  108. try {
  109. // Make non-blocking call to ProcessDispatcher to ThreadPool.QueueUserWorkItem to complete
  110. // stopping of all start calls in this AppDomain. For containers shared among various AppDomains,
  111. // this will just be a ref-count subtract. For non-shared containers, we will close the container
  112. // and clean-up.
  113. SqlDependencyProcessDispatcher dispatcher = SqlDependency.ProcessDispatcher;
  114. if (null != dispatcher) {
  115. dispatcher.QueueAppDomainUnloading(SqlDependency.AppDomainKey);
  116. }
  117. }
  118. finally {
  119. Bid.ScopeLeave(ref hscp);
  120. }
  121. }
  122. // ----------------------------------------------------
  123. // Methods for dependency hash manipulation and firing.
  124. // ----------------------------------------------------
  125. // This method is called upon SqlDependency constructor.
  126. internal void AddDependencyEntry(SqlDependency dep) {
  127. IntPtr hscp;
  128. Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependencyPerAppDomainDispatcher.AddDependencyEntry|DEP> %d#, SqlDependency: %d#", ObjectID, dep.ObjectID);
  129. try {
  130. lock (this) {
  131. _dependencyIdToDependencyHash.Add(dep.Id, dep);
  132. }
  133. }
  134. finally {
  135. Bid.ScopeLeave(ref hscp);
  136. }
  137. }
  138. // This method is called upon Execute of a command associated with a SqlDependency object.
  139. internal string AddCommandEntry(string commandHash, SqlDependency dep) {
  140. IntPtr hscp;
  141. string notificationId = string.Empty;
  142. Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependencyPerAppDomainDispatcher.AddCommandEntry|DEP> %d#, commandHash: '%ls', SqlDependency: %d#", ObjectID, commandHash, dep.ObjectID);
  143. try {
  144. lock (this) {
  145. if (!_dependencyIdToDependencyHash.ContainsKey(dep.Id)) { // Determine if depId->dep hashtable contains dependency. If not, it's been invalidated.
  146. Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.AddCommandEntry|DEP> Dependency not present in depId->dep hash, must have been invalidated.\n");
  147. }
  148. else {
  149. // check if we already have notification associated with given command hash
  150. if (_commandHashToNotificationId.TryGetValue(commandHash, out notificationId)) {
  151. // we have one or more SqlDependency instances with same command hash
  152. DependencyList dependencyList = null;
  153. if (!_notificationIdToDependenciesHash.TryGetValue(notificationId, out dependencyList))
  154. {
  155. // this should not happen since _commandHashToNotificationId and _notificationIdToDependenciesHash are always
  156. // updated together
  157. Debug.Assert(false, "_commandHashToNotificationId has entries that were removed from _notificationIdToDependenciesHash. Remember to keep them in [....]");
  158. throw ADP.InternalError(ADP.InternalErrorCode.SqlDependencyCommandHashIsNotAssociatedWithNotification);
  159. }
  160. // join the new dependency to the list
  161. if (!dependencyList.Contains(dep)) {
  162. Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.AddCommandEntry|DEP> Dependency not present for commandHash, adding.\n");
  163. dependencyList.Add(dep);
  164. }
  165. else {
  166. Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.AddCommandEntry|DEP> Dependency already present for commandHash.\n");
  167. }
  168. }
  169. else {
  170. // we did not find notification ID with the same app domain and command hash, create a new one
  171. // use unique guid to avoid duplicate IDs
  172. // prepend app domain ID to the key - SqlConnectionContainer::ProcessNotificationResults (SqlDependencyListener.cs)
  173. // uses this app domain ID to route the message back to the app domain in which this SqlDependency was created
  174. notificationId = string.Format(System.Globalization.CultureInfo.InvariantCulture,
  175. "{0};{1}",
  176. SqlDependency.AppDomainKey, // must be first
  177. Guid.NewGuid().ToString("D", System.Globalization.CultureInfo.InvariantCulture)
  178. );
  179. Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.AddCommandEntry|DEP> Creating new Dependencies list for commandHash.\n");
  180. DependencyList dependencyList = new DependencyList(commandHash);
  181. dependencyList.Add(dep);
  182. // map command hash to notification we just created to reuse it for the next client
  183. // do it inside finally block to avoid ThreadAbort exception interrupt this operation
  184. try {}
  185. finally {
  186. _commandHashToNotificationId.Add(commandHash, notificationId);
  187. _notificationIdToDependenciesHash.Add(notificationId, dependencyList);
  188. }
  189. }
  190. Debug.Assert(_notificationIdToDependenciesHash.Count == _commandHashToNotificationId.Count, "always keep these maps in [....]!");
  191. }
  192. }
  193. }
  194. finally {
  195. Bid.ScopeLeave(ref hscp);
  196. }
  197. return notificationId;
  198. }
  199. // This method is called by the ProcessDispatcher upon a notification for this AppDomain.
  200. internal void InvalidateCommandID(SqlNotification sqlNotification) {
  201. IntPtr hscp;
  202. Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependencyPerAppDomainDispatcher.InvalidateCommandID|DEP> %d#, commandHash: '%ls'", ObjectID, sqlNotification.Key);
  203. try {
  204. List<SqlDependency> dependencyList = null;
  205. lock (this) {
  206. dependencyList = LookupCommandEntryWithRemove(sqlNotification.Key);
  207. if (null != dependencyList) {
  208. Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.InvalidateCommandID|DEP> commandHash found in hashtable.\n");
  209. foreach (SqlDependency dependency in dependencyList) {
  210. // Ensure we remove from process static app domain hash for dependency initiated invalidates.
  211. LookupDependencyEntryWithRemove(dependency.Id);
  212. // Completely remove Dependency from commandToDependenciesHash.
  213. RemoveDependencyFromCommandToDependenciesHash(dependency);
  214. }
  215. }
  216. else {
  217. Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.InvalidateCommandID|DEP> commandHash NOT found in hashtable.\n");
  218. }
  219. }
  220. if (null != dependencyList) {
  221. // After removal from hashtables, invalidate.
  222. foreach (SqlDependency dependency in dependencyList) {
  223. Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.InvalidateCommandID|DEP> Dependency found in commandHash dependency ArrayList - calling invalidate.\n");
  224. try {
  225. dependency.Invalidate(sqlNotification.Type, sqlNotification.Info, sqlNotification.Source);
  226. }
  227. catch (Exception e) {
  228. // Since we are looping over dependencies, do not allow one Invalidate
  229. // that results in a throw prevent us from invalidating all dependencies
  230. // related to this server.
  231. if (!ADP.IsCatchableExceptionType(e)) {
  232. throw;
  233. }
  234. ADP.TraceExceptionWithoutRethrow(e);
  235. }
  236. }
  237. }
  238. }
  239. finally {
  240. Bid.ScopeLeave(ref hscp);
  241. }
  242. }
  243. // This method is called when a connection goes down or other unknown error occurs in the ProcessDispatcher.
  244. internal void InvalidateServer(string server, SqlNotification sqlNotification) {
  245. IntPtr hscp;
  246. Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependencyPerAppDomainDispatcher.Invalidate|DEP> %d#, server: '%ls'", ObjectID, server);
  247. try {
  248. List<SqlDependency> dependencies = new List<SqlDependency>();
  249. lock (this) { // Copy inside of lock, but invalidate outside of lock.
  250. foreach (KeyValuePair<string, SqlDependency> entry in _dependencyIdToDependencyHash) {
  251. SqlDependency dependency = entry.Value;
  252. if (dependency.ContainsServer(server)) {
  253. dependencies.Add(dependency);
  254. }
  255. }
  256. foreach (SqlDependency dependency in dependencies) { // Iterate over resulting list removing from our hashes.
  257. // Ensure we remove from process static app domain hash for dependency initiated invalidates.
  258. LookupDependencyEntryWithRemove(dependency.Id);
  259. // Completely remove Dependency from commandToDependenciesHash.
  260. RemoveDependencyFromCommandToDependenciesHash(dependency);
  261. }
  262. }
  263. foreach (SqlDependency dependency in dependencies) { // Iterate and invalidate.
  264. try {
  265. dependency.Invalidate(sqlNotification.Type, sqlNotification.Info, sqlNotification.Source);
  266. }
  267. catch (Exception e) {
  268. // Since we are looping over dependencies, do not allow one Invalidate
  269. // that results in a throw prevent us from invalidating all dependencies
  270. // related to this server.
  271. if (!ADP.IsCatchableExceptionType(e)) {
  272. throw;
  273. }
  274. ADP.TraceExceptionWithoutRethrow(e);
  275. }
  276. }
  277. }
  278. finally {
  279. Bid.ScopeLeave(ref hscp);
  280. }
  281. }
  282. // This method is called by SqlCommand to enable ASP.Net scenarios - map from ID to Dependency.
  283. internal SqlDependency LookupDependencyEntry(string id) {
  284. IntPtr hscp;
  285. Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependencyPerAppDomainDispatcher.LookupDependencyEntry|DEP> %d#, Key: '%ls'", ObjectID, id);
  286. try {
  287. if (null == id) {
  288. throw ADP.ArgumentNull("id");
  289. }
  290. if (ADP.IsEmpty(id)) {
  291. throw SQL.SqlDependencyIdMismatch();
  292. }
  293. SqlDependency entry = null;
  294. lock (this) {
  295. if (_dependencyIdToDependencyHash.ContainsKey(id)) {
  296. entry = _dependencyIdToDependencyHash[id];
  297. }
  298. else {
  299. Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.LookupDependencyEntry|DEP|ERR> ERROR - dependency ID mismatch - not throwing.\n");
  300. }
  301. }
  302. return entry;
  303. }
  304. finally {
  305. Bid.ScopeLeave(ref hscp);
  306. }
  307. }
  308. // Remove the dependency from the hashtable with the passed id.
  309. private void LookupDependencyEntryWithRemove(string id) {
  310. IntPtr hscp;
  311. Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependencyPerAppDomainDispatcher.LookupDependencyEntryWithRemove|DEP> %d#, id: '%ls'", ObjectID, id);
  312. try {
  313. lock (this) {
  314. if (_dependencyIdToDependencyHash.ContainsKey(id)) {
  315. Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.LookupDependencyEntryWithRemove|DEP> Entry found in hashtable - removing.\n");
  316. _dependencyIdToDependencyHash.Remove(id);
  317. // if there are no more dependencies then we can dispose the timer.
  318. if (0 == _dependencyIdToDependencyHash.Count) {
  319. _timeoutTimer.Change(Timeout.Infinite, Timeout.Infinite);
  320. _SqlDependencyTimeOutTimerStarted = false;
  321. }
  322. }
  323. else {
  324. Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.LookupDependencyEntryWithRemove|DEP> Entry NOT found in hashtable.\n");
  325. }
  326. }
  327. }
  328. finally {
  329. Bid.ScopeLeave(ref hscp);
  330. }
  331. }
  332. // Find and return arraylist, and remove passed hash value.
  333. private List<SqlDependency> LookupCommandEntryWithRemove(string notificationId) {
  334. IntPtr hscp;
  335. Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependencyPerAppDomainDispatcher.LookupCommandEntryWithRemove|DEP> %d#, commandHash: '%ls'", ObjectID, notificationId);
  336. try {
  337. DependencyList entry = null;
  338. lock (this) {
  339. if (_notificationIdToDependenciesHash.TryGetValue(notificationId, out entry)) {
  340. Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.LookupDependencyEntriesWithRemove|DEP> Entries found in hashtable - removing.\n");
  341. // update the tables - do it inside finally block to avoid ThreadAbort exception interrupt this operation
  342. try { }
  343. finally {
  344. _notificationIdToDependenciesHash.Remove(notificationId);
  345. // VSTS 216991: cleanup the map between the command hash and associated notification ID
  346. _commandHashToNotificationId.Remove(entry.CommandHash);
  347. }
  348. }
  349. else {
  350. Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.LookupDependencyEntriesWithRemove|DEP> Entries NOT found in hashtable.\n");
  351. }
  352. Debug.Assert(_notificationIdToDependenciesHash.Count == _commandHashToNotificationId.Count, "always keep these maps in [....]!");
  353. }
  354. return entry; // DependencyList inherits from List<SqlDependency>
  355. }
  356. finally {
  357. Bid.ScopeLeave(ref hscp);
  358. }
  359. }
  360. // Remove from commandToDependenciesHash all references to the passed dependency.
  361. private void RemoveDependencyFromCommandToDependenciesHash(SqlDependency dependency) {
  362. IntPtr hscp;
  363. Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependencyPerAppDomainDispatcher.RemoveDependencyFromCommandToDependenciesHash|DEP> %d#, SqlDependency: %d#", ObjectID, dependency.ObjectID);
  364. try {
  365. lock (this) {
  366. List<string> notificationIdsToRemove = new List<string>();
  367. List<string> commandHashesToRemove = new List<string>();
  368. foreach (KeyValuePair<string, DependencyList> entry in _notificationIdToDependenciesHash) {
  369. DependencyList dependencies = entry.Value;
  370. if (dependencies.Remove(dependency)) {
  371. Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.RemoveDependencyFromCommandToDependenciesHash|DEP> Removed SqlDependency: %d#, with ID: '%ls'.\n", dependency.ObjectID, dependency.Id);
  372. if (dependencies.Count == 0) {
  373. // this dependency was the last associated with this notification ID, remove the entry
  374. // note: cannot do it inside foreach over dictionary
  375. notificationIdsToRemove.Add(entry.Key);
  376. commandHashesToRemove.Add(entry.Value.CommandHash);
  377. }
  378. }
  379. // same SqlDependency can be associated with more than one command, so we have to continue till the end...
  380. }
  381. Debug.Assert(commandHashesToRemove.Count == notificationIdsToRemove.Count, "maps should be kept in [....]");
  382. for (int i = 0; i < notificationIdsToRemove.Count; i++ ) {
  383. // cleanup the entry outside of foreach
  384. // do it inside finally block to avoid ThreadAbort exception interrupt this operation
  385. try { }
  386. finally {
  387. _notificationIdToDependenciesHash.Remove(notificationIdsToRemove[i]);
  388. // VSTS 216991: cleanup the map between the command hash and associated notification ID
  389. _commandHashToNotificationId.Remove(commandHashesToRemove[i]);
  390. }
  391. }
  392. Debug.Assert(_notificationIdToDependenciesHash.Count == _commandHashToNotificationId.Count, "always keep these maps in [....]!");
  393. }
  394. }
  395. finally {
  396. Bid.ScopeLeave(ref hscp);
  397. }
  398. }
  399. // -----------------------------------------
  400. // Methods for Timer maintenance and firing.
  401. // -----------------------------------------
  402. internal void StartTimer(SqlDependency dep) {
  403. IntPtr hscp;
  404. Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependencyPerAppDomainDispatcher.StartTimer|DEP> %d#, SqlDependency: %d#", ObjectID, dep.ObjectID);
  405. try {
  406. // If this dependency expires sooner than the current next timeout, change
  407. // the timeout and enable timer callback as needed. Note that we change _nextTimeout
  408. // only inside the hashtable syncroot.
  409. lock (this) {
  410. // Enable the timer if needed (disable when empty, enable on the first addition).
  411. if (!_SqlDependencyTimeOutTimerStarted) {
  412. Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.StartTimer|DEP> Timer not yet started, starting.\n");
  413. _timeoutTimer.Change(15000 /* 15 secs */, 15000 /* 15 secs */);
  414. // Save this as the earlier timeout to come.
  415. _nextTimeout = dep.ExpirationTime;
  416. _SqlDependencyTimeOutTimerStarted = true;
  417. }
  418. else if(_nextTimeout > dep.ExpirationTime) {
  419. Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.StartTimer|DEP> Timer already started, resetting time.\n");
  420. // Save this as the earlier timeout to come.
  421. _nextTimeout = dep.ExpirationTime;
  422. }
  423. }
  424. }
  425. finally {
  426. Bid.ScopeLeave(ref hscp);
  427. }
  428. }
  429. private static void TimeoutTimerCallback(object state) {
  430. IntPtr hscp;
  431. Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependencyPerAppDomainDispatcher.TimeoutTimerCallback|DEP> AppDomainKey: '%ls'", SqlDependency.AppDomainKey);
  432. try {
  433. SqlDependency[] dependencies;
  434. // Only take the lock for checking whether there is work to do
  435. // if we do have work, we'll copy the hashtable and scan it after releasing
  436. // the lock.
  437. lock (SingletonInstance) {
  438. if (0 == SingletonInstance._dependencyIdToDependencyHash.Count) {
  439. // Nothing to check.
  440. Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.TimeoutTimerCallback|DEP> No dependencies, exiting.\n");
  441. return;
  442. }
  443. if (SingletonInstance._nextTimeout > DateTime.UtcNow) {
  444. Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.TimeoutTimerCallback|DEP> No timeouts expired, exiting.\n");
  445. // No dependency timed-out yet.
  446. return;
  447. }
  448. // If at least one dependency timed-out do a scan of the table.
  449. // NOTE: we could keep a shadow table sorted by expiration time, but
  450. // given the number of typical simultaneously alive dependencies it's
  451. // probably not worth the optimization.
  452. dependencies = new SqlDependency[SingletonInstance._dependencyIdToDependencyHash.Count];
  453. SingletonInstance._dependencyIdToDependencyHash.Values.CopyTo(dependencies, 0);
  454. }
  455. // Scan the active dependencies if needed.
  456. DateTime now = DateTime.UtcNow;
  457. DateTime newNextTimeout = DateTime.MaxValue;
  458. for (int i=0; i < dependencies.Length; i++) {
  459. // If expired fire the change notification.
  460. if(dependencies[i].ExpirationTime <= now) {
  461. try {
  462. // This invokes user-code which may throw exceptions.
  463. // NOTE: this is intentionally outside of the lock, we don't want
  464. // to invoke user-code while holding an internal lock.
  465. dependencies[i].Invalidate(SqlNotificationType.Change, SqlNotificationInfo.Error, SqlNotificationSource.Timeout);
  466. }
  467. catch(Exception e) {
  468. if (!ADP.IsCatchableExceptionType(e)) {
  469. throw;
  470. }
  471. // This is an exception in user code, and we're in a thread-pool thread
  472. // without user's code up in the stack, no much we can do other than
  473. // eating the exception.
  474. ADP.TraceExceptionWithoutRethrow(e);
  475. }
  476. }
  477. else {
  478. if (dependencies[i].ExpirationTime < newNextTimeout) {
  479. newNextTimeout = dependencies[i].ExpirationTime; // Track the next earlier timeout.
  480. }
  481. dependencies[i] = null; // Null means "don't remove it from the hashtable" in the loop below.
  482. }
  483. }
  484. // Remove timed-out dependencies from the hashtable.
  485. lock (SingletonInstance) {
  486. for (int i=0; i < dependencies.Length; i++) {
  487. if (null != dependencies[i]) {
  488. SingletonInstance._dependencyIdToDependencyHash.Remove(dependencies[i].Id);
  489. }
  490. }
  491. if (newNextTimeout < SingletonInstance._nextTimeout) {
  492. SingletonInstance._nextTimeout = newNextTimeout; // We're inside the lock so ok to update.
  493. }
  494. }
  495. }
  496. finally {
  497. Bid.ScopeLeave(ref hscp);
  498. }
  499. }
  500. }
  501. // Simple class used to encapsulate all data in a notification.
  502. internal class SqlNotification : MarshalByRefObject {
  503. // This class could be Serializable rather than MBR...
  504. private readonly SqlNotificationInfo _info;
  505. private readonly SqlNotificationSource _source;
  506. private readonly SqlNotificationType _type;
  507. private readonly string _key;
  508. internal SqlNotification(SqlNotificationInfo info, SqlNotificationSource source, SqlNotificationType type, string key) {
  509. _info = info;
  510. _source = source;
  511. _type = type;
  512. _key = key;
  513. }
  514. internal SqlNotificationInfo Info {
  515. get {
  516. return _info;
  517. }
  518. }
  519. internal string Key {
  520. get {
  521. return _key;
  522. }
  523. }
  524. internal SqlNotificationSource Source {
  525. get {
  526. return _source;
  527. }
  528. }
  529. internal SqlNotificationType Type {
  530. get {
  531. return _type;
  532. }
  533. }
  534. }
  535. }