You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Originally posted by vladyslav-burylov July 12, 2023
Hi team, we have discovered a weird ContextVars behaviour when uvicorn being installed without [standard] extensions.
Can you please take a look - should this be considered as a bug or maybe you can help to explain why it happens?
Summary
Since uvicorn creates async task for each specific request, it is expected that any context vars set while processing would be isolated within a request scope
However, above works well with 'uvicorn[standard]' edition only
Workarounds
Use uvicorn[standard] instead of plain uvicorn (seems to be a good option)
Use ASGI scope to store request-bound data (not always possible: for example opentelemetry cloud tracing middleware stores current trace span inside context var and this logic cannot be changed)
importtimeimportuuidimportrequestsprint("")
print("Starting...")
defmain():
# Everything is good when payload is smallhuge_payload=""forxinrange(10000):
huge_payload+=f"{str(uuid.uuid4())}\n"# Send request each 1 second reusing single connection (without connection reuse it works well)withrequests.Session() assession:
whileTrue:
try:
response=session.post('http://127.0.0.1:8002/test', huge_payload)
print(response.text) # server will be responding with either 'OK' or 'FAIL'# In case if server reports 'FAIL' - we reproduced the issue and are OK to exitifresponse.text=="FAIL":
breakexceptKeyboardInterrupt:
breakexceptBaseExceptionasex:
print(str(ex))
time.sleep(0.1)
main()
server.py
importuuidimportuvicornfromcontextvarsimportContextVar# This var expected to store some request-bound value. Here 'request_id' as example.request_id: ContextVar[str] =ContextVar("request_id", default="")
# Minimalistic ASGI app which don't use any 3pt librariesclassMyApp:
asyncdef__call__(self, scope, receive, send) ->None:
# https://asgi.readthedocs.io/en/latest/specs/lifespan.htmlifscope["type"] =="lifespan":
whileTrue:
message=awaitreceive()
ifmessage['type'] =='lifespan.startup':
awaitsend({'type': 'lifespan.startup.complete'})
elifmessage['type'] =='lifespan.shutdown':
awaitsend({'type': 'lifespan.shutdown.complete'})
returnprint("")
# request_id expected to be empty at this point: request has just startedval=request_id.get()
# Validate it and record unexpected behaviour if anyfailed=Falseifval:
failed=Trueprint("UNEXPECTED request_id value: ", val)
# Generate new GUID and store it inside request_id for this requestguid=str(uuid.uuid4())
token=request_id.set(guid)
# Primitive request processing: reply with OK/FAIL body# FAIL means that bug has been reproduced (request_id has been polluted by some of previous request)try:
print("(1) ", guid) # Debug loggingawaitsend({"type": "http.response.start", "status": 200, "headers": []})
print("(2) ", guid) # Debug loggingawaitsend({"type": "http.response.body", "body": b"FAIL"iffailedelseb"OK"})
print("(3) ", guid) # Debug logging# Request completed. # Reset request_id to an empty valuefinally:
request_id.reset(token)
print("(4) ", guid) # Debug logging# Simplistic server without any magicserver=uvicorn.Server(config=uvicorn.Config(
MyApp(),
host="127.0.0.1",
port=8002,
workers=1,
access_log=False,
proxy_headers=False,
server_header=False
))
server.run()
Executing test
Start server
# cd testsource ./test/bin/activate
python server.py
Start client
# cd testsource ./test/bin/activate
python client.py
Expected behaviour
No errors reported, both server and client running forever
Actual behaviour
request_id context var value at the beginning of requests gets polluted with previous value eventually
server.py log
INFO: Started server process [32392]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8002 (Press CTRL+C to quit)
(1) 57f92756-b50e-4627-824e-da5a93865316
(2) 57f92756-b50e-4627-824e-da5a93865316
(3) 57f92756-b50e-4627-824e-da5a93865316
(4) 57f92756-b50e-4627-824e-da5a93865316
UNEXPECTED request_id value: 57f92756-b50e-4627-824e-da5a93865316
(1) dbd3ce8a-65ec-4796-b829-6f8da4287c3e
(2) dbd3ce8a-65ec-4796-b829-6f8da4287c3e
(3) dbd3ce8a-65ec-4796-b829-6f8da4287c3e
(4) dbd3ce8a-65ec-4796-b829-6f8da4287c3e
client.py log
Starting...
OK
FAIL
Notes
It seems to be occurring periodically (unstable behaviour) - constants within test script adjusted to give high replication probability
Test code uses requests library to make sample requests, however other client libraries give the same result (originally I have replicated it using much more sophisticated httpx.AsyncClient)
Works well with uvicorn[standard] - maybe it's happening because standard edition utilises uvloop?
Important
We're using Polar.sh so you can upvote and help fund this issue.
We receive the funding once the issue is completed & confirmed by you.
Thank you in advance for helping prioritize & fund our backlog.
The text was updated successfully, but these errors were encountered:
Discussed in #2044
Originally posted by vladyslav-burylov July 12, 2023
Hi team, we have discovered a weird ContextVars behaviour when uvicorn being installed without [standard] extensions.
Can you please take a look - should this be considered as a bug or maybe you can help to explain why it happens?
Summary
Workarounds
uvicorn[standard]
instead of plainuvicorn
(seems to be a good option)scope
to store request-bound data (not always possible: for example opentelemetry cloud tracing middleware stores current trace span inside context var and this logic cannot be changed)Steps to reproduce
Hardware/OS/tools
Prepare environment
Prepare test scripts - create following files:
client.py
server.py
Executing test
Expected behaviour
Actual behaviour
Notes
uvicorn[standard]
- maybe it's happening because standard edition utilisesuvloop
?Important
The text was updated successfully, but these errors were encountered: