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?
uvicorn[standard]
instead of plain uvicorn
(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)mkdir test && cd test
pyenv install 3.10.9 # unless already installed
pyenv local 3.10.9
python --version # expected to print 3.10.9
python -m venv test
source ./test/bin/activate
pip install requests
pip install 'uvicorn==0.22.0'
import time
import uuid
import requests
print("")
print("Starting...")
def main():
# Everything is good when payload is small
huge_payload = ""
for x in range(10000):
huge_payload += f"{str(uuid.uuid4())}\n"
# Send request each 1 second reusing single connection (without connection reuse it works well)
with requests.Session() as session:
while True:
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 exit
if response.text == "FAIL":
break
except KeyboardInterrupt:
break
except BaseException as ex:
print(str(ex))
time.sleep(0.1)
main()
import uuid
import uvicorn
from contextvars import ContextVar
# 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 libraries
class MyApp:
async def __call__(self, scope, receive, send) -> None:
# https://asgi.readthedocs.io/en/latest/specs/lifespan.html
if scope["type"] == "lifespan":
while True:
message = await receive()
if message['type'] == 'lifespan.startup':
await send({'type': 'lifespan.startup.complete'})
elif message['type'] == 'lifespan.shutdown':
await send({'type': 'lifespan.shutdown.complete'})
return
print("")
# request_id expected to be empty at this point: request has just started
val = request_id.get()
# Validate it and record unexpected behaviour if any
failed = False
if val:
failed = True
print("UNEXPECTED request_id value: ", val)
# Generate new GUID and store it inside request_id for this request
guid = 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 logging
await send({"type": "http.response.start", "status": 200, "headers": []})
print("(2) ", guid) # Debug logging
await send({"type": "http.response.body", "body": b"FAIL" if failed else b"OK"})
print("(3) ", guid) # Debug logging
# Request completed.
# Reset request_id to an empty value
finally:
request_id.reset(token)
print("(4) ", guid) # Debug logging
# Simplistic server without any magic
server = 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()
# cd test
source ./test/bin/activate
python server.py
# cd test
source ./test/bin/activate
python client.py
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
Starting...
OK
FAIL
uvicorn[standard]
- maybe it's happening because standard edition utilises uvloop
?Pay now to fund the work behind this issue.
Get updates on progress being made.
Maintainer is rewarded once the issue is completed.
You're funding impactful open source efforts
You want to contribute to this effort
You want to get funding like this too