Use contextvars when available, to support async frameworks #2072
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
This is a backwards-compatible change to use
contextvars
instead ofthreading.local
when available.contextvars
is only available in Python 3.7 and above, so the implementation falls back tothreading.local
when not available.This fixes errors for async frameworks (like FastAPI) when used with a load that requires more than one single request concurrently.
Description
threading.local
creates a value exclusive to the current thread, but an async framework would run all the "tasks" in the same thread, and possibly not in order. On top of that, an async framework could run some sync code in a threadpool (run_in_executor
), but belonging to the same "task".This means that, with the current implementation, multiple tasks could be using the same
threading.local
variable, and at the same time, if they execute sync IO-blocking code in a threadpool (run_in_executor
), that code won't have access to the variable, even while it's part of the same "task" and it should be able to get access to that.Reproducing the error
To demonstrate the error, take this example FastAPI application:
Run it with:
Open the browser at http://127.0.0.1:8000/docs in 5 tabs at the same time.
Use the "Try it out" button and the "Execute" button for the single
/
endpoint, in each one of the tabs. With a small time difference between each one (1 sec).After 15 seconds, one (or more) of the tabs will show the response, while the rest will show a 500 error.
And the console will show an error like:
What happens is that:
Note
An equivalent error would occur when using a middleware, with an example comparable to the one in the docs.
Although the current example in the docs starts a connection at application startup and closes it right before terminating the application. It doesn't really create a connection per-request, that would be achieved with a middleware (or better, with a dependency, as in the example above).
Proposed fix
The proposed fix uses tries to import
contextvars
(Python 3.7+) and updates the_ConnectionLocal
class to keep behaving as normal, but usecontextvars
underneath when available. When not, fall back tothreading.local
. It keeps the interface of_ConnectionLocal
the same to simplify any external code using it.contextvars
is backward-compatible. In Python 3.7+, with synchronous code, it will provide the same behavior thatthreading.local
would have.https://www.python.org/dev/peps/pep-0567/#backwards-compatibility
Running the same example from above, using this branch, should return the responses without errors.
Updated docs
I also updated the docs for FastAPI, to use normal
def
functions and a dependency, as that would probably be the best way to structure it in FastAPI.