views:

454

answers:

2

Hi, I created the following encapsulation over the SQL Dependency object:

public class DependencyTracker
    {
        private SqlDependency _SQLDependency = null;

        public string ConnectionString
        { get; private set; }

        public string CommandNotifier
        { get; private set; }

        public delegate void Refresh();
        public event Refresh OnRefresh;

        public DependencyTracker(string connectionString, string commandNotifier)
        {
            ConnectionString = connectionString;
            CommandNotifier = commandNotifier;
        }

        public void StartDependency()
        {
            SqlDependency.Start(ConnectionString);
        }

        public void StopDependency()
        {
            SqlDependency.Stop(ConnectionString);
        }

        public void TrackForChanges()
        {
            using (SqlConnection sqlConn = new SqlConnection(ConnectionString))
            {
                sqlConn.Open();
                using (SqlCommand sqlCommand = new SqlCommand(CommandNotifier, sqlConn))
                {
                    sqlCommand.CommandType = CommandType.StoredProcedure;
                    _SQLDependency = new SqlDependency(sqlCommand);
                    _SQLDependency.OnChange += new OnChangeEventHandler(dependency_OnChange);
                    sqlCommand.ExecuteReader();
                }
            }
        }

        void dependency_OnChange(object sender, SqlNotificationEventArgs e)
        {
            SqlDependency sqlDependency = (SqlDependency)sender;
            sqlDependency.OnChange -= dependency_OnChange;

            if (OnRefresh != null)
            {
                OnRefresh();
            }
        }

        public bool HasChanges
        {
            get
            {
                return _SQLDependency.HasChanges;
            }
        }
    }

It's not an original work, it's based on this text. From my repository I do the following:

public bool Updatexxx(Ixxx xsxs)
        {
            try
            {
                SqlConnection sqlConn = new SqlConnection(_ConnectionString);
                sqlConn.Open();
                ...

                bool result = sqlComm.ExecuteNonQuery() == 1;

                _ResetEvent.WaitOne();

                return result;
            }
            catch ...
            catch ...
        }

The callback is

public void RefreshData()
        {
            FindAllxxx();
            _ResetEvent.Set();
        }

And

public HashSet<Iddd> Finddadas()
        {
            DependencyTracker tracker = _Container.Resolve<DependencyTracker>("dada");
            UnityHashSet<IT24Route> hash = _Container.Resolve<UnityHashSet<dadas>>("Tdsadas");
            if (tracker.HasChanges || hash.Count == 0)
            {
                hash = new UnityHashSet<dsda>(_Container);
                hash.ImportHashSet(FindAlldsdsNonCached());
                _Container.RegisterInstance<UnityHashSet<dsds>>("dasda", hash);
                tracker.TrackForChanges();
            }
            return hash;
        }

and FindAllXXXNonCached() does the real select from database. As you see, I am caching everything. My question is why does this NOT work. The symptom: from dependecy tracker the callback is called twice then it block. I implemented this because my notifications were coming after the refresh of the page began. I tried to put a manual reset event to give notification a chance, then set the manual reset event and the refresh the UI. As I said, after passing twice through the OnChange, it freezes. What can I do? Dependency Tracker is instance registered in Unity container, Repository is type registered.

+2  A: 
void dependency_OnChange(object sender, SqlNotificationEventArgs e)

You are expected to inspect the SqlNotificationEventArgs and see what you're notified for, data change or something else. Check the Info to be Insert/Update/Delete. Check the Source to be Data. Check the Type to be Change.

Most likely your query is signalled immedeatly that it does not conform to the restrictions imposed on Query Notifications. Yes, I know that link points to Indexed Views, and if you want to understand why, read The Mysterious Notification.

You also have a race condition between your Update waiting for _ResetEvent and callback signaling _ResetEvent. T1 calls Update. Meantime an unrelated update occurs in the data and callback is called. _ResetEvent is set. T1 finishes the update and waits on _ResteEvent, which is signaled, so it proceeds. The caller assumes the callback for its own update occured and cache is refreshed, but this is not true.

A second more serious problem is that the code is incorrect in the presence of transactions. UpdateXXX assumes the callback for its own update will occur immedeatly and waits for it. The query notification will be delivered by the engine only after the Update commits, so when a TransactionScope is present the UpdateXXX method will block waiting for a notification cannot come until UpdateXXX returns (live deadlock).

Also is not clear what is the purpose of TrackForChanges. You are reading a SqlDataReader (sqlCommand.ExecuteReader) but ignore the result. With Query Notifications you submit a query , read itse result and you'll be notified when that result has changed.

And finally, never read data in the SqlDependency notification callback.

Remus Rusanu
1. Related to the queries used for notification, they look like: select column1, column2, column3 from dbo.table.
DaeMoohn
0. Thanks for the hints about SqlNotificationEventArgs.
DaeMoohn
2. About transactions, I can make the assumption they won't appear in this application. Still, the issue remains, and thanks for pointing it out. It didn't cross my mind.
DaeMoohn
3. Thanks again for "never read data in the SqlDependency notification callback."
DaeMoohn
4. The TrackForChanges is used for setting up each notification based on the connection string and command. I had to call the ExecuteReader to "build the initial values", but as the dependency tracker is not in the same module with the repositories, the first time the application fires up, i execute each notification twice. So, for 3 notifications, I have 6 initial sql selects.
DaeMoohn
4 again. I can't return a list with results from the depency tracker as it would violate the repository's role.
DaeMoohn
4. That is not how it is supposed to work. Read the data, put it in cache, clear the cache on notification. On next request after cache clear, read the data again. This is how caches and notifications work.
Remus Rusanu
What do you mean by "Meantime an unrelated update occurs in the data and callback is called" ? I have one notification for each entity.
DaeMoohn
Reply to 4. This is how it works: Executes the 3 selects (for 3 entities) only at App_Start, then executes the 3 selects again, caching the results. Assuming insert. Inserts, then notifies cache dirty, then runs again the select for the affected entity and fills the specific cache.
DaeMoohn
My problem with the implementation and the decision to adopt a ManualResetEvent is that the cache isn't awear imediately after the update/insert/delete that it is dirty, the "soft" select (in cache) executes faster than the dependency.HasChanges is true. So, while the page is refreshing, the OnChange event is fired, but only the next "soft" select will become an sql select.
DaeMoohn
The problem with blocking the thread was because the repositories where type registered, not instance registered. I will have to further investigate the race conditions that Remus pointed out, but still I have a starting point. He has my vote!
DaeMoohn
A: 

One more question I would have: My dependencies and caches are to be used by two distinct applications, a web service (with intensive use) and a web site ( the management side). The modifications over data occur on the website. Given the fact that both apps use the same notifications, can I let the dependency started even after the app is exiting? Is there a timeout value implemented after which the notification is dropped?

DaeMoohn
You cannot have two appdomains share notifications. Each appdomain will have to call SqlDependency.Start on its own and each appdomain will create its own SqlDependency instances, corresponding to its own query notification subscriptions.
Remus Rusanu