-
-
Notifications
You must be signed in to change notification settings - Fork 960
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
Detect blocking calls in coroutines using BlockBuster #2858
base: master
Are you sure you want to change the base?
Conversation
starlette/datastructures.py
Outdated
@@ -438,7 +438,7 @@ async def write(self, data: bytes) -> None: | |||
if self.size is not None: | |||
self.size += len(data) | |||
|
|||
if self._in_memory: | |||
if self._in_memory and self.file.tell() + len(data) <= self.file._max_size: |
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.
The SpooledTemporaryFile
rollover
is blocking
@@ -288,7 +288,7 @@ async def receive() -> Message: | |||
await response_complete.wait() | |||
return {"type": "http.disconnect"} | |||
|
|||
body = request.read() | |||
body = await anyio.to_thread.run_sync(request.read) |
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.
Httpx request.read
can be blocking (eg: reading multipart file).
await request.aread()
can't be used as-is since the request stream
is a SyncByteStream
not an AsyncByteStream
.
@@ -23,7 +23,7 @@ def test_templates(tmpdir: Path, test_client_factory: TestClientFactory) -> None | |||
with open(path, "w") as file: | |||
file.write("<html>Hello, <a href='{{ url_for('homepage') }}'>world</a></html>") | |||
|
|||
async def homepage(request: Request) -> Response: | |||
def homepage(request: Request) -> Response: |
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.
Is it OK to change ?
templates.TemplateResponse
is blocking.
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.
Why is templates.TemplateResponse
blocking?
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.
It loads the index.html
from the FS using jinja2.FileSystemLoader.get_source()
which does blocking calls.
The path is:
test_templates.py:27: in homepage
return templates.TemplateResponse(request, "index.html")
../starlette/templating.py:208: in TemplateResponse
template = self.get_template(name)
../starlette/templating.py:131: in get_template
return self.env.get_template(name)
../venv/lib/python3.11/site-packages/jinja2/environment.py:1016: in get_template
return self._load_template(name, globals)
../venv/lib/python3.11/site-packages/jinja2/environment.py:975: in _load_template
template = self.loader.load(self, name, self.make_globals(globals))
../venv/lib/python3.11/site-packages/jinja2/loaders.py:126: in load
source, filename, uptodate = self.get_source(environment, name)
../venv/lib/python3.11/site-packages/jinja2/loaders.py:204: in get_source
if os.path.isfile(filename):
path.isfile
is a blocking call as it does an os.stat
.
The get_source
code also does a file.read()
blocking call later.
@pytest.fixture(autouse=True) | ||
def blockbuster(request): | ||
with blockbuster_ctx("starlette") as bb: | ||
bb.functions["os.stat"].can_block_in("/mimetypes.py", "init") |
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.
FileResponse
's constructor calls mimetypes .guess_type
which is blocking the first time it is called.
09e27f0
to
8a12d86
Compare
8a12d86
to
d9da912
Compare
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.
I'm not keen to add a dependency, but seems a cool library.
@@ -438,7 +439,7 @@ async def write(self, data: bytes) -> None: | |||
if self.size is not None: | |||
self.size += len(data) | |||
|
|||
if self._in_memory: | |||
if self._in_memory and self.file.tell() + len(data) <= getattr(self.file, "_max_size", sys.maxsize): |
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.
what's this about?
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.
If the data to write makes the file exceed the SpooledTemporaryFile
_max_size
, self.file.write
will do a blocking rollover
operation.
@@ -468,7 +468,7 @@ async def cancel_on_disconnect( | |||
|
|||
# A timeout is set for 0.1 second in order to ensure that | |||
# we never deadlock the test run in an infinite loop | |||
with anyio.move_on_after(0.1): | |||
with anyio.move_on_after(0.2): |
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.
Why?
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.
BlockBuster has a small toll on performance.
In CI, the test machines are already slow, the cancellation sometimes arrives too late and the test becomes flaky (I had no issue on my computer, only in CI).
@@ -23,7 +23,7 @@ def test_templates(tmpdir: Path, test_client_factory: TestClientFactory) -> None | |||
with open(path, "w") as file: | |||
file.write("<html>Hello, <a href='{{ url_for('homepage') }}'>world</a></html>") | |||
|
|||
async def homepage(request: Request) -> Response: | |||
def homepage(request: Request) -> Response: |
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.
Why is templates.TemplateResponse
blocking?
Co-authored-by: Marcelo Trylesinski <[email protected]>
@cbornet I'm curious about the choice to call out file IO as blocking. In my experience file IO is tricky:
Because of this my general approach has been to be conservative with chucking things into threads, especially file IO since it's often not a problem in practice. |
@adriangb thanks for your feedback.
|
Summary
This PR uses the blockbuster library to detect blocking calls made in the asyncio event loop during unit tests.
Avoiding blocking calls is hard as these can be deeply buried in the code or made in 3rd party libraries.
Blockbuster makes it easier to detect them by raising an exception when a call is made to a known blocking function (eg:
time.sleep
).Checklist