-
-
Notifications
You must be signed in to change notification settings - Fork 951
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Preserve changes to contexvars made in threadpools #1258
Conversation
Accidental close. Sorry. I merged in master, made the extra method private and cleaned up tests / coverage. Tests and linting are passing locally now. |
This should have been fixed by anyio 3.4.0: https://anyio.readthedocs.io/en/stable/versionhistory.html Can you confirm? @adriangb |
It seems like they implemented context propagation, but not capturing of changes in the context. You can verified this by taking this branch, replacing In other words, we can probably remove the contextvar stuff here: starlette/starlette/concurrency.py Lines 32 to 35 in a11dbe0
|
@pytest.mark.anyio | ||
async def test_restore_context_from_thread_previously_set(): | ||
"""Value outside of threadpool is overwitten with value set in threadpool""" | ||
ctxvar: contextvars.ContextVar[str] = contextvars.ContextVar("ctxvar") | ||
ctxvar.set("spam") | ||
|
||
def sync_task(): | ||
ctxvar.set("ham") | ||
|
||
await run_in_threadpool(sync_task) | ||
assert ctxvar.get() == "ham" | ||
|
||
|
||
@pytest.mark.anyio | ||
async def test_restore_context_from_thread_previously_unset(): | ||
"""Value outside of threadpool is set to value in threadpool""" | ||
ctxvar: contextvars.ContextVar[str] = contextvars.ContextVar("ctxvar") | ||
|
||
def sync_task(): | ||
ctxvar.set("ham") | ||
|
||
await run_in_threadpool(sync_task) | ||
assert ctxvar.get() == "ham" | ||
|
||
|
||
@pytest.mark.anyio | ||
async def test_restore_context_from_thread_new_cvar(): | ||
"""Value outside of threadpool is set for a cvar created in the threadpool""" | ||
ctxvars: List[contextvars.ContextVar[str]] = [] | ||
|
||
def sync_task(): | ||
ctxvar: contextvars.ContextVar[str] = contextvars.ContextVar("ctxvar") | ||
ctxvar.set("ham") | ||
ctxvars.append(ctxvar) | ||
|
||
await run_in_threadpool(sync_task) | ||
assert len(ctxvars) == 1 | ||
assert next(iter(ctxvars)).get() == "ham" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This are the failing tests, which get to the root of the problem in the PR post without jumping through hoops to exercise the issue
Are there any cons of doing this? Are we going to break anything with this? Is this really necessary? Is it possible to do what the PR proposes without adding it to Starlette itself, but a Starlette user can do it on their own? |
I've asked about this on As Gitter history is deleted after some time, I'll paste the conversation for the record: |
@agronholm opened a discussion on discuss.python.org: https://discuss.python.org/t/back-propagation-of-contextvar-changes-from-worker-threads/15928 |
Looks like in 3.11 we might be able to use this for async tasks as well: python/cpython#31837 |
Can you clarify? I'm not sure how this addition will help Starlette. |
We have a similar issue with I also think that in that case it's better to push people away from using it instead of trying to fix it, but we may be able to also try to fix it to make the situation better for users that can't / won't drop So in theory in 3.11 I think we should be able to do: ctx = copy_context()
with anyio.create_task_group() as tg:
tg.start_soon(..., context=ctx) # assuming you add this to anyio
_restore_context(ctx) # this hack |
Not sure if this is appropriate to post here, but I am interested in this PR for exactly the use case @adriangb mentions in the OP. I am trying to implement AWS X-Ray tracing SDK into a FastAPI application but am blocked because the SDK stores the traces in the thread's context, which is lost when the controller endpoint thread is ran in the threadpool. |
Yup that is appropriate! User feedback is always appreciated. Could you try this branch and see if it solves things for you? |
Quite relevant: django/asgiref#267 So there are some edge cases with this implementation. In Django this is a problem because there are two different threads, in Starlette we just have one, so that usage doesn't make sense in Starlette and hence I don't think we'd run into this issue. Besides, it is currently also broken, this makes it no better or worse. |
@graingert would you mind sharing why we shouldn't go with this? Just to have the record here. 😟 🙏 |
I do not know what @graingert would say, but I would also opine that, on the balance of it, restoring contextvars is not a game that we should play, as the only guarantee given by contextvars is that it will flow downstream - there is no guarantee about either the scope of the context (is it the process? A HTTP request? Something else?), or that any variable set will flow upstream to the calling function. Implementing upstream In other words, I currently think that “setting a context variable so that it can be picked up by upstream code” is a misuse of the I believe that for the issues highlighted here, alternative solutions can be implemented, among which:
|
I think the issue is that for a FastAPI user this is downstream: def dep1() -> None:
ctx.set("foo")
def dep2(d1: None = Depends()) -> None:
ctx.get() This should be modeled as
FWIW I don't disagree with this. I have a gut feeling that there will be some issues with implementing this, even if it's been in asgiref for years and hasn't caused any issues.
This won't solve things for users that are just using a library that uses context vars, but I think providing an explicit context would be great. Flask does it. This would allow FastAPI to manipulate the context as it sees fit to make it work with the dependency injection system. Starlette could use this context to make propagating data from lifespans to requests less akward/not use globals. I already proposed this in asgiref: django/asgiref#322 |
The definition of what I mean by "downstream" for In this sense, However, I would rather avoid in-depth discussion of how to solve FastAPI's dependency issue in this repository - FastAPI's dependency system, and, in particular, any runtime/execution guarantees that FastAPI wishes to provide to its users, need to be part of FastAPI's feature set. In this case, I sincerely believe that it is possible for FastAPI to use the In order to enable FastAPI to support these cases more easily, I would propose that
I already knew that. :) |
I do agree on this 😄, I think FastAPI should not be using these functions from Starlette (I think I even remember Tom saying we should have made these private from the get-go).
This is something I keep thinking about. Although this has been in asgiref for a long time and as far as I can remember from all of these discussions no one has pointed out a concrete issue with the idea or implementation, I can't help but agree with you that it just feels like there's going to be some edge cases if we go down this route. To be honest I'm not enthused with this solution, it's not a problem I personally deal with and there doesn't seem to be much support amongst our team for it. I'm going to close the PR for now and if things change in the future we can re-evaluate. |
I think this addresses fastapi/fastapi#953.
Thank you @smagafurov for the link in fastapi/fastapi#953 (comment)
What does this do for users?
This fixes a confusing behavior where their endpoint works differently depending on if it is sync or async.
And in FastAPI (which for better or worse re-uses
run_in_threadpool
), it impacts dependencies as well.This is the current behavior:
I wouldn't expect a user to write this themselves, but some library they are using very well could use contextvars to set something that then gets picked up by a middleware (for example, some sort of tracing library). Since this is an implementation detail, and the failure mode is pretty tricky, it would be difficult for a user to figure out what happened.