Here's simplified test case:
https://gist.github.com/4086529 - it is reproducible with any asynchronous operation, even add_callback.
I have suspicion it has something to do with recursion.
Here's supposed workflow:
1. go_for_it() calls fetch()
2. fetch() creates nested ExceptionStackContext
3. fetch() calls add_callback() and it will wrap() callback to maintain current ExceptionStackContext
4. on next tick, IOLoop will call wrapped callback function
5. wrap() will explicitly restore chain of the ExceptionStackContext by calling __enter__ for each item in stack and then continue fetch() execution
6. fetch() will call callback() with restored stack context
7. callback() will return control to go_for_it() with restored stack context
8. go_for_it() will start next 'while' iteration by calling go_for_it() while in restored stack context
9. jump to #2
I found only one workaround: wrap loop code in go_for_it() with NullContext.
Unfortunately, using add_callback to create endless loop won't work either - ExceptionStackContext stack will continue to grow, as it will run in the fetch() context.
Not sure how to fix it, to be honest, as it is working "as expected", though it took me hour to figure out what's going on.
P.S. enter/leave flow looks like this:
! fetch()
Leave 1
Leave 0
Enter 0
Enter 1
Enter 2
! fetch()
Leave 2
Leave 1
Leave 0
Enter 0
Enter 1
Enter 2
Enter 3
! fetch()
Leave 3
Leave 2
Leave 1
Leave 0
... and so on. Number next to enter/leave = len(_state.contexts)
Serge.