I have an ASP.NET app targeting .NET 4.6 in which I coded a global "event monitor". The purpose of this singleton is two-fold:
The normal request processing pipeline reports various business events to it. These events are put in a queue of a dedicated background thread which, a) writes them to a database, and b) forwards them to any interested observers (see #2).
Administrative requests (a special kind of long-polling HTTP request made by administrative session) on the other hand, may choose to observe or listen to certain types of events that happen in normal requests very close to real-time (hence the term "event monitoring") based on a combination of event filters (e.g. invalid log-in attempts, or modification of sensitive business data, etc.) and then react to them.
Observing can also be performed by any normal request that wishes to monitor its own events (i.e. to self-observe). For example, a request that involves reading from a database might want to keep high-level tabs on what exactly is being read and in what order. This is especially useful during development and debugging, because it doesn't involve a separate administrative request just to find out which SQL statements are being issued to the database or which files are being read. Information like this is invaluable when trying to piece together everything that complex request handling entails.
The problem I'm currently having is with this self-observing. The request pipeline uses async all the way and works without any problems. That is, until I enter awaiting of observed events.
Here's the basic API that I coded:
var observer = new EventObserver();
using (EventMonitor.Instance.Observe(...params..., observer))
{
await MyComplexBusinessWorkThatReportsManyEvents();
}
var events = await observer.Task;
Debug.WriteLine(events.ToJson());
As you can see, the observer API is async as well. When the end of using
is reached, the resulting call to Dispose
detaches the observer from the monitor. However, since the reported events are processed out of band in a background thread and might therefore be just slightly out of sync, I now have to await the detached observer until all the events up to detachment time trickle in. This usually happens very quickly, but still - slow enough that it has to be awaited.
The observer.Task
is really an encapsulation of a TaskCompletionSource.Task
, which gets completed from a background thread (the one that processes all reported events in queue). And this is where the code hangs. Now, I realize I could have coded this API differently... I could have used the usual thread synchronization primitives, such as ManualResetEvent
. But since that means I'd have to block the request thread, I opted for TaskCompletionSource
and async/await. It was purely a design decision, not a necessity.
Of course, the hang goes away as soon as I make the following change:
var events = await observer.Task.ConfigureAwait(false);
Which tells me the problem has to do with the captured context and the fact that the task is being completed on a different thread. The explicit ConfigureAwait(false)
call itself wouldn't be a problem, if all it took was writing it once: in the line above. Unfortunately, I've seen that I have to carry it all the way up in the await chain. In other words, the hang only goes away when ALL awaits from the start of request pipeline down to the await observer.Task
specify ConfigureAwait(false)
.
I realize I must be doing something stupid, but this baffles me. I'd appreciate an explanation from experts... What did I miss?