NLog 1.0 supports asynchronous logging, but there is no good support for asynchronous exception handling. This is because wrappers targets are not capable of receiving exceptions which are raised on other threads.
Since NLog 2.0 is going to support Silverlight where entire networking stack is completely asynchronous, it is critical to enable wrappers for those scenarios. Without it some important wrapper-based features, such as load balancing or failover would not work properly.
This post will present new APIs to support asynchronous logging features that are coming in the next release of NLog.
Exception Handling in NLog 1.0
NLog 1.0 uses very simple, synchronous exception handling pattern:
The problem arises if the code block inside try { } clause performs an asynchronous operation such as network call which may result in an exception, as in the following example:
NLog cannot handle exceptions in such cases, since the original stack frame is gone, so it just swallows exceptions raised asynchronously and logs them to the internal log. Not catching exceptions on background threads would be fatal and might result in application termination.
You can probably see why swallowing exceptions prevents wrappers, such as RetryingWrapper from working. If you write declare the following wrappers in your configuration file, the outermost wrapper will never implement any retry logic, since AsyncWrapper will never pass any asynchronous exceptions to RetryingWrapper.
Asynchronous Exception Handling in NLog 2.0
In order to implement proper asynchronous exception handling we need to let asynchronous methods know what to do in case of success and failure. This is typically done through continuation functions. There are many ways to represent continuation information, I’ve decided to represent it as an interface with two methods:
The Target.Write() API will be refactored to look like this:
As you can see, by default the asynchronous code gets forwarded to the synchronous Write method. This lets us keep the existing extensibility interface for targets. If you want to implement asynchronous target, you need to override both synchronous and asynchronous write methods:
Target.Flush() method will be changed in a similar way, except it will be asynchronous only:
LogManager and LogFactory will also be enhanced with asynchronous Flush() methods. Their synchronous overloads will not be available in Silverlight, since there is no way to wait on a potential network call without causing a deadlock:
Working with continuations
NLog 2.0 will provide default implementation of continuations creatable through AsyncHelpers.MakeContinuation() factory method:
In addition to this I am planning to expose helpers which will make working with and composing continuations easier:
Impact on wrappers
Because of the way the API is designed, the impact on existing targets should be very limited. Unfortunately this does not apply to wrappers, which have to be completely rewritten to be fully asynchronous. Asynchronous code tends to be larger and more difficult to read and follow, as demonstrated in the following example:
For example, the code for retrying wrapper in NLog 1.0 looked like this:
The code for the same operation in NLog 2.0 is much more complex:
Summary
Asynchronous processing is a very difficult matter, and it is very difficult to write correct and robust asynchronous code. I am hoping that proposed APIs and abstraction level are the right ones and will not make the source code too difficult to read and maintain.