I've recently been refactoring Play's iteratees to use implicit ExecutionContexts and I've found the need to prepare ExecutionContexts before they are used has been quite awkward when writing asynchronous code. I know ExecutionContext preparation is simple in concept—just call the "prepare" method before use—but I've actually found it to be quite error-prone in practice!
I'd like to suggest a change to the semantics of ExecutionContext preparation, but first I'll talk a bit more about the problems I've found.
-
As I understand it, this is the way ExecutionContext preparation works at the moment:
- every method that accepts an implicit ExecutionContext must call the prepare method on the ExecutionContext it receives as an argument
- the prepare method must be called in the caller's thread, so that any thread local state can be gathered by the ExecutionContext if necessary
- only the prepared ExecutionContext should be used for execution
- the (possibly unprepared) ExecutionContext originally supplied to the method should not be used for execution
(Please correct me if any of my assumptions are incorrect.)
While refactoring Play's iteratees, despite being careful, I've made mistakes with all of those requirements:
- I've forgotten to prepare the ExecutionContext.
- I've prepared the ExecutionContext in the wrong thread, e.g. by accidentally preparing inside a call-by-name parameter that runs on a different thread.
- I've prepared the ExecutionContext properly, but then used the unprepared one accidentally (easy to do since the unprepared ExecutionContext is implicit in the method's scope).
Errors are difficult to spot, because prepared and unprepared ExecutionContexts both have the same type and so are indistinguishable to both compiler and programmer. The unprepared ExecutionContext, which should not be used, is also implicit within the method's scope, so it's incredibly easy to use accidentally.
Also, when errors in preparation do occur, they often go undected. This is because most ExecutionContexts don't need preparation at all and function fine without preparation. For this type of ExecutionContext, errors with preparation won't lead to any visible errors in program execution. Only more "exotic" ExecutionContexts, such as those that propagate thread local information, will actually have any chance of showing errors when they're used incorrectly. Unfortunately these ExecutionContext may only occur once the async code in question has shipped and is being used in production!
Finally, when errors do occur, they will likely be subtle and it possibly quite difficult to debug. We may be able to deduce that a thread local variable is not being propagated. But where exactly in a long chain of asynchronous calls did we forget to prepare the ExecutionContext? Did we prepare it on the wrong thread? Or perhaps we prepared it but then accidentally used the unprepared ExecutionContext instead?
In the Play iteratee library I've tried to write tests to ensure that preparation happens properly. I test that preparation occurs, that preparation happens in the right thread, and that only the correctly prepared ExecutionContexts are actually used for executing code. Every asynchronous method needs tests for ExecutionContext preparation. Despite this effort, I suspect there are probably still some bugs related to ExecutionContext prepration in the library…
-
So, I'd like to suggest a simpler approach to ExecutionContext preparation.
I propose that ExecutionContexts should be prepared when they are constructed. Once constructed, no further preparation is ever needed. The prepare() method can be deprecated.
To me this seems to me like quite a nice idea. It makes working with ExecutionContexts much, much easier, since if you have an ExecutionContext then you can work with it without needing to worry about whether it is prepared or unprepared. Since the prepare method doesn't need to be called, it reduces the amount of code needed in asynchronous code, and it reduces the amount of code that can have bugs.
Most ExecutionContexts, i.e. those that don't don't need any preparation at all, would obviously be unaffected by this change. Like now, must users would be able to import an implicit value and use that as their ExecutionContext when writing asynchronous code.
But what about ExecutionContexts that do need preparation? If the ExecutionContext is provided explicitly then it can be obtained, like any other object, by constructing it or by calling a method. Most users will probably prefer to use an implicit import to provide their ExecutionContext. Preparation can still occur in this case, by importing an implicit method rather than an implicit value. Methods that require an implicit ExecutionContext will be provided one by the compiler. The compiler will call the implicit method to provide the argument as needed. The method will have the opportunity to do any preparation that is needed. Another benefit is that the method will automatically be called in the caller thread, which is the correct thread for preparation—another common source of errors avoided.
So I can see that there are some big benefits to changing the preparation semantics, and no major downsides. Or maybe I've missed something? :)
Benefits:
- user code looks the same as before—import an implicit value, now either a val or a def
- async library code is simplified and has have fewer bugs
- less code to write and execute
Downsides:
- ?
-
I'd like to hear other people's thoughts on this proposal. I feel like my suggestion is so simple that it must have been considered at some time in the past and discarded for some reason. If there's something obvious I'm missing, I'm ready to be educated!
I think it would be nice for Play if we could stop preparing ExecutionContexts within every asynchronous method. We'd be able to remove a lot of code from the iteratee library that is solely devoted to managing and tracking ExecutionContext preparation. It would probably also fix a few bugs. I think some simplification around ExecutionContext preparation would be of benefit to other people writing asynchronous code too.
Cheers