Closed chris-telemetry closed 1 year ago
ouch, yes this is pretty bad. Will make this a priority.
Investigation led me to this function in starlite.middleware.logging.py
as the culprit:
async def send_wrapper(message: "Message") -> None:
if message["type"] == HTTP_RESPONSE_START:
set_starlite_scope_state(scope, HTTP_RESPONSE_START, message)
elif message["type"] == HTTP_RESPONSE_BODY:
set_starlite_scope_state(scope, HTTP_RESPONSE_BODY, message)
self.log_response(scope=scope)
await send(message)
return send_wrapper
Commenting out set_starlite_scope_state(scope, HTTP_RESPONSE_BODY, message)
solves the issue.
I tried overwriting the state after the response is logged:
async def send_wrapper(message: "Message") -> None:
if message["type"] == HTTP_RESPONSE_START:
set_starlite_scope_state(scope, HTTP_RESPONSE_START, message)
elif message["type"] == HTTP_RESPONSE_BODY:
set_starlite_scope_state(scope, HTTP_RESPONSE_BODY, message)
self.log_response(scope=scope)
set_starlite_scope_state(scope, HTTP_RESPONSE_START, '')
set_starlite_scope_state(scope, HTTP_RESPONSE_BODY, '')
await send(message)
But that didn't help. I may not understand how state is controlled though.
ouch, yes this is pretty bad. Will make this a priority.
Thanks for the quick response. There's a thread in the discord where we discussed this. There may be some additional context if that helps.
so, the problem is that message is an ASGIMessage object, which is actually a dictionary - hence it keeps a reference in memory. The fact this reference is saved inside another dictionary- the "scope" dictionary attached to the request instance, keeps the request alive and makes it not garbage collectable. We have to switch to using weak refs in this case or save data that is garbage collectable.
I think another solution would be to slightly restructure the middleware, so it doesn't need to store the message in the scope to begin with. If this can easily be done it would be preferable imo.
@chris-telemetry - can you test this branch. 1137-bug-loggingmiddleware-stores-all-responses-permanently-in-memory-memory-leak
? I will add a PR for it.
I'll test. Thank you!
@Goldziher It looks good to me. Used my prior test case of 60 requests with ~20MB responses.
[ Top 10 ]
/Users/christophermoyer/opt/anaconda3/lib/python3.9/json/decoder.py:353: size=24.0 KiB, count=318, average=77 B
/Users/christophermoyer/opt/anaconda3/lib/python3.9/abc.py:123: size=5022 B, count=54, average=93 B
/Users/christophermoyer/opt/anaconda3/lib/python3.9/asyncio/events.py:80: size=4728 B, count=12, average=394 B
/Users/christophermoyer/code/starlite/starlite/utils/extractors.py:159: size=3800 B, count=9, average=422 B
/Users/christophermoyer/code/starlite/starlite/parsers.py:65: size=2079 B, count=21, average=99 B
/Users/christophermoyer/Library/Caches/pypoetry/virtualenvs/starlite-fioynTQM-py3.9/lib/python3.9/site-packages/uvicorn/protocols/http/h11_impl.py:208: size=1866 B, count=21, average=89 B
/Users/christophermoyer/code/starlite/starlite/middleware/exceptions/middleware.py:47: size=1456 B, count=3, average=485 B
/Users/christophermoyer/code/starlite/starlite/connection/request.py:138: size=1336 B, count=3, average=445 B
/Users/christophermoyer/Library/Caches/pypoetry/virtualenvs/starlite-fioynTQM-py3.9/lib/python3.9/site-packages/h11/_connection.py:411: size=1312 B, count=3, average=437 B
/Users/christophermoyer/Library/Caches/pypoetry/virtualenvs/starlite-fioynTQM-py3.9/lib/python3.9/site-packages/h11/_connection.py:470: size=1288 B, count=3, average=429 B
@Goldziher It looks good to me. Used my prior test case of 60 requests with ~20MB responses.
[ Top 10 ] /Users/christophermoyer/opt/anaconda3/lib/python3.9/json/decoder.py:353: size=24.0 KiB, count=318, average=77 B /Users/christophermoyer/opt/anaconda3/lib/python3.9/abc.py:123: size=5022 B, count=54, average=93 B /Users/christophermoyer/opt/anaconda3/lib/python3.9/asyncio/events.py:80: size=4728 B, count=12, average=394 B /Users/christophermoyer/code/starlite/starlite/utils/extractors.py:159: size=3800 B, count=9, average=422 B /Users/christophermoyer/code/starlite/starlite/parsers.py:65: size=2079 B, count=21, average=99 B /Users/christophermoyer/Library/Caches/pypoetry/virtualenvs/starlite-fioynTQM-py3.9/lib/python3.9/site-packages/uvicorn/protocols/http/h11_impl.py:208: size=1866 B, count=21, average=89 B /Users/christophermoyer/code/starlite/starlite/middleware/exceptions/middleware.py:47: size=1456 B, count=3, average=485 B /Users/christophermoyer/code/starlite/starlite/connection/request.py:138: size=1336 B, count=3, average=445 B /Users/christophermoyer/Library/Caches/pypoetry/virtualenvs/starlite-fioynTQM-py3.9/lib/python3.9/site-packages/h11/_connection.py:411: size=1312 B, count=3, average=437 B /Users/christophermoyer/Library/Caches/pypoetry/virtualenvs/starlite-fioynTQM-py3.9/lib/python3.9/site-packages/h11/_connection.py:470: size=1288 B, count=3, average=429 B
Great, we'll get a patch out asap
Incredible. I really appreciate it.
I've been testing out the LoggingMiddleware and I believe it's holding the response data in memory. This is causing very high memory usage for some of our endpoints that return very large (~10-20MB) responses. This happens even when I set
response_log_fields=('status_code')
.I'm using using starlite = {extras = ["full"], version = "1.50.2"} so the logger should be using picologging.
Here's a snippet for you, the json file I'm using to test with is about 20MB.
Tracemalloc showing memory is being held on to (LoggingMiddleware enabled):
Tracemalloc with LoggingMiddleware disabled:
This issue causes logging to scale linearly with (uncompressed) requests. We caught this in production when memory spiked to 40GB after deploying LoggingMiddleware. Any help would be hugely appreciated. We've disabled the middleware for now.