// ----------------------------------------- // Methods for Timer maintenance and firing. // ----------------------------------------- internal void StartTimer(SqlDependency dep) { IntPtr hscp; Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependencyPerAppDomainDispatcher.StartTimer|DEP> %d#, SqlDependency: %d#", ObjectID, dep.ObjectID); try { // If this dependency expires sooner than the current next timeout, change // the timeout and enable timer callback as needed. Note that we change _nextTimeout // only inside the hashtable syncroot. lock (this) { // Enable the timer if needed (disable when empty, enable on the first addition). if (!_SqlDependencyTimeOutTimerStarted) { Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.StartTimer|DEP> Timer not yet started, starting.\n"); _timeoutTimer.Change(15000 /* 15 secs */, 15000 /* 15 secs */); // Save this as the earlier timeout to come. _nextTimeout = dep.ExpirationTime; _SqlDependencyTimeOutTimerStarted = true; } else if (_nextTimeout > dep.ExpirationTime) { Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.StartTimer|DEP> Timer already started, resetting time.\n"); // Save this as the earlier timeout to come. _nextTimeout = dep.ExpirationTime; } } } finally { Bid.ScopeLeave(ref hscp); } }
// This method is called by SqlCommand to enable ASP.NET scenarios - map from ID to Dependency. internal SqlDependency LookupDependencyEntry(string id) { IntPtr hscp; Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependencyPerAppDomainDispatcher.LookupDependencyEntry|DEP> %d#, Key: '%ls'", ObjectID, id); try { if (null == id) { throw ADP.ArgumentNull("id"); } if (ADP.IsEmpty(id)) { throw SQL.SqlDependencyIdMismatch(); } SqlDependency entry = null; lock (this) { if (_dependencyIdToDependencyHash.ContainsKey(id)) { entry = _dependencyIdToDependencyHash[id]; } else { Bid.NotificationsTrace("<sc.SqlDependencyPerAppDomainDispatcher.LookupDependencyEntry|DEP|ERR> ERROR - dependency ID mismatch - not throwing.\n"); } } return(entry); } finally { Bid.ScopeLeave(ref hscp); } }
// Remove from commandToDependenciesHash all references to the passed dependency. private void RemoveDependencyFromCommandToDependenciesHash(SqlDependency dependency) { lock (_instanceLock) { List <string> notificationIdsToRemove = new List <string>(); List <string> commandHashesToRemove = new List <string>(); foreach (KeyValuePair <string, DependencyList> entry in _notificationIdToDependenciesHash) { DependencyList dependencies = entry.Value; if (dependencies.Remove(dependency)) { if (dependencies.Count == 0) { // this dependency was the last associated with this notification ID, remove the entry // note: cannot do it inside foreach over dictionary notificationIdsToRemove.Add(entry.Key); commandHashesToRemove.Add(entry.Value.CommandHash); } } // same SqlDependency can be associated with more than one command, so we have to continue till the end... } Debug.Assert(commandHashesToRemove.Count == notificationIdsToRemove.Count, "maps should be kept in sync"); for (int i = 0; i < notificationIdsToRemove.Count; i++) { // cleanup the entry outside of foreach _notificationIdToDependenciesHash.Remove(notificationIdsToRemove[i]); // Cleanup the map between the command hash and associated notification ID _commandHashToNotificationId.Remove(commandHashesToRemove[i]); } Debug.Assert(_notificationIdToDependenciesHash.Count == _commandHashToNotificationId.Count, "always keep these maps in sync!"); } }
// This method is called by SqlCommand to enable ASP.NET scenarios - map from ID to Dependency. internal SqlDependency LookupDependencyEntry(string id) { long scopeID = SqlClientEventSource.Log.TryNotificationScopeEnterEvent("<sc.SqlDependencyPerAppDomainDispatcher.LookupDependencyEntry|DEP> {0}, Key: '{1}'", ObjectID, id); try { if (null == id) { throw ADP.ArgumentNull("id"); } if (ADP.IsEmpty(id)) { throw SQL.SqlDependencyIdMismatch(); } SqlDependency entry = null; lock (this) { if (_dependencyIdToDependencyHash.ContainsKey(id)) { entry = _dependencyIdToDependencyHash[id]; } else { SqlClientEventSource.Log.TryNotificationTraceEvent("<sc.SqlDependencyPerAppDomainDispatcher.LookupDependencyEntry|DEP|ERR> ERROR - dependency ID mismatch - not throwing."); } } return(entry); } finally { SqlClientEventSource.Log.TryNotificationScopeLeaveEvent(scopeID); } }
private static void TimeoutTimerCallback(object state) { SqlDependency[] dependencies; // Only take the lock for checking whether there is work to do // if we do have work, we'll copy the hashtable and scan it after releasing // the lock. lock (SingletonInstance._instanceLock) { if (0 == SingletonInstance._dependencyIdToDependencyHash.Count) { // Nothing to check. return; } if (SingletonInstance._nextTimeout > DateTime.UtcNow) { // No dependency timed-out yet. return; } // If at least one dependency timed-out do a scan of the table. // NOTE: we could keep a shadow table sorted by expiration time, but // given the number of typical simultaneously alive dependencies it's // probably not worth the optimization. dependencies = new SqlDependency[SingletonInstance._dependencyIdToDependencyHash.Count]; SingletonInstance._dependencyIdToDependencyHash.Values.CopyTo(dependencies, 0); } // Scan the active dependencies if needed. DateTime now = DateTime.UtcNow; DateTime newNextTimeout = DateTime.MaxValue; for (int i = 0; i < dependencies.Length; i++) { // If expired fire the change notification. if (dependencies[i].ExpirationTime <= now) { try { // This invokes user-code which may throw exceptions. // NOTE: this is intentionally outside of the lock, we don't want // to invoke user-code while holding an internal lock. dependencies[i].Invalidate(SqlNotificationType.Change, SqlNotificationInfo.Error, SqlNotificationSource.Timeout); } catch (Exception e) { if (!ADP.IsCatchableExceptionType(e)) { throw; } // This is an exception in user code, and we're in a thread-pool thread // without user's code up in the stack, no much we can do other than // eating the exception. ADP.TraceExceptionWithoutRethrow(e); } } else { if (dependencies[i].ExpirationTime < newNextTimeout) { newNextTimeout = dependencies[i].ExpirationTime; // Track the next earlier timeout. } dependencies[i] = null; // Null means "don't remove it from the hashtable" in the loop below. } } // Remove timed-out dependencies from the hashtable. lock (SingletonInstance._instanceLock) { for (int i = 0; i < dependencies.Length; i++) { if (null != dependencies[i]) { SingletonInstance._dependencyIdToDependencyHash.Remove(dependencies[i].Id); } } if (newNextTimeout < SingletonInstance._nextTimeout) { SingletonInstance._nextTimeout = newNextTimeout; // We're inside the lock so ok to update. } } }
// This method is called upon Execute of a command associated with a SqlDependency object. internal string AddCommandEntry(string commandHash, SqlDependency dep) { string notificationId = string.Empty; long scopeID = SqlClientEventSource.Log.TryNotificationScopeEnterEvent("<sc.SqlDependencyPerAppDomainDispatcher.AddCommandEntry|DEP> {0}, commandHash: '{1}', SqlDependency: {2}", ObjectID, commandHash, dep.ObjectID); try { lock (this) { if (!_dependencyIdToDependencyHash.ContainsKey(dep.Id)) { // Determine if depId->dep hashtable contains dependency. If not, it's been invalidated. SqlClientEventSource.Log.TryNotificationTraceEvent("<sc.SqlDependencyPerAppDomainDispatcher.AddCommandEntry|DEP> Dependency not present in depId->dep hash, must have been invalidated."); } else { // check if we already have notification associated with given command hash if (_commandHashToNotificationId.TryGetValue(commandHash, out notificationId)) { // we have one or more SqlDependency instances with same command hash DependencyList dependencyList = null; if (!_notificationIdToDependenciesHash.TryGetValue(notificationId, out dependencyList)) { // this should not happen since _commandHashToNotificationId and _notificationIdToDependenciesHash are always // updated together Debug.Assert(false, "_commandHashToNotificationId has entries that were removed from _notificationIdToDependenciesHash. Remember to keep them in sync"); throw ADP.InternalError(ADP.InternalErrorCode.SqlDependencyCommandHashIsNotAssociatedWithNotification); } // join the new dependency to the list if (!dependencyList.Contains(dep)) { SqlClientEventSource.Log.TryNotificationTraceEvent("<sc.SqlDependencyPerAppDomainDispatcher.AddCommandEntry|DEP> Dependency not present for commandHash, adding."); dependencyList.Add(dep); } else { SqlClientEventSource.Log.TryNotificationTraceEvent("<sc.SqlDependencyPerAppDomainDispatcher.AddCommandEntry|DEP> Dependency already present for commandHash."); } } else { // we did not find notification ID with the same app domain and command hash, create a new one // use unique guid to avoid duplicate IDs // prepend app domain ID to the key - SqlConnectionContainer::ProcessNotificationResults (SqlDependencyListener.cs) // uses this app domain ID to route the message back to the app domain in which this SqlDependency was created notificationId = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0};{1}", SqlDependency.AppDomainKey, // must be first Guid.NewGuid().ToString("D", System.Globalization.CultureInfo.InvariantCulture) ); SqlClientEventSource.Log.TryNotificationTraceEvent("<sc.SqlDependencyPerAppDomainDispatcher.AddCommandEntry|DEP> Creating new Dependencies list for commandHash."); DependencyList dependencyList = new DependencyList(commandHash); dependencyList.Add(dep); // map command hash to notification we just created to reuse it for the next client // do it inside finally block to avoid ThreadAbort exception interrupt this operation try { } finally { _commandHashToNotificationId.Add(commandHash, notificationId); _notificationIdToDependenciesHash.Add(notificationId, dependencyList); } } Debug.Assert(_notificationIdToDependenciesHash.Count == _commandHashToNotificationId.Count, "always keep these maps in sync!"); } } } finally { SqlClientEventSource.Log.TryNotificationScopeLeaveEvent(scopeID); } return(notificationId); }