-
-
Notifications
You must be signed in to change notification settings - Fork 858
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
Issue warning on unclosed AsyncClient
.
#1197
Issue warning on unclosed AsyncClient
.
#1197
Conversation
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.
Thanks! ✨😀
Seems simple on the surface, but there's a few bits & pieces to work through.
In particular I'd prefer we don't use the decorator style, and prefer a plain approach instead.
Direct, simple, easier to reason through, neater tracebacks.
I've included some comments to help work through what I think could be tweaked.
httpx/_client.py
Outdated
@@ -56,6 +56,18 @@ | |||
KEEPALIVE_EXPIRY = 5.0 | |||
|
|||
|
|||
def check_not_closed(method: typing.Callable) -> typing.Callable: |
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'd prefer for us not to use a decorator style.
I'll use inline comments below to walk through the alternative...
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've removed the decorator. Simple is better than complex.
httpx/_client.py
Outdated
@@ -619,6 +639,7 @@ def _transport_for_url(self, url: URL) -> httpcore.SyncHTTPTransport: | |||
|
|||
return self._transport | |||
|
|||
@check_not_closed |
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 guess we should instead put the check around def send()
rather than def request()
.
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.
Rather than the decorator, I'd prefer the plainer style of starting the method with...
if self._is_closed:
raise RuntimeError("Cannot send requests on a closed client instance.")
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 guess we should instead put the check around def send() rather than def request().
Done
Rather than the decorator, I'd prefer the plainer style of starting the method with...
Done, but I used self.is_closed
in the condition
httpx/_client.py
Outdated
@@ -1015,6 +1036,7 @@ def delete( | |||
timeout=timeout, | |||
) | |||
|
|||
@check_not_closed |
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.
Could we instead just let things pass silently if the client is already closed, and .close()
is called again?
So...
if not self._is_closed:
self._transport.close()
for proxy in self._proxies.values():
if proxy is not None:
proxy.close()
httpx/_client.py
Outdated
@@ -1628,6 +1654,8 @@ async def aclose(self) -> None: | |||
if proxy is not None: | |||
await proxy.aclose() | |||
|
|||
self._is_closed = True |
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.
We should put self._is_closed = True
as the very first line, rather than the very last line, since that'd handle potential race conditions more cleanly.
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.
@tomchristie It a bit confuses me, since there should be another ways to avoid race conditions, e.g. locks.
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 saw several same places where race condition is possible
I can try to write a test, which strictly cause a race condition here (if it's possible)
350ae77
to
5e2197c
Compare
I wrote the test, which shows the race condition. I can fix it (but I'd like to do it in separate PR since it involves things such as Here is the test: class SlowTestTransport(httpcore.SyncHTTPTransport):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.closing_counter = 0
def request(
self,
method: bytes,
url: URL,
headers: Headers = None,
stream: SyncByteStream = None,
timeout: TimeoutDict = None,
) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], SyncByteStream]:
return b"", 0, b"", [(b"", b"")], ByteStream(b"Hello")
def close(self) -> None:
self.closing_counter += 1
class SlowClient(httpx.Client):
@property
def is_closed(self) -> bool:
result = super().is_closed
time.sleep(0.5)
return result
def test_that_client_handles_race_condition_while_being_closed():
slow_transport = SlowTestTransport()
client = SlowClient(transport=slow_transport)
futures = []
with ThreadPoolExecutor(max_workers=2) as executor:
futures.append(executor.submit(client.close))
futures.append(executor.submit(client.close))
for _ in concurrent.futures.as_completed(futures):
pass
assert slow_transport.closing_counter == 1 |
Let's not get into that level of testing at the moment. Yes you can potentially do silly things if you're concurrently closing clients and issuing requests at the same time, but we don't necessarily need to protect users from themselves at quite that level of granularity. So, working through this has actually made it more clear to me exactly what behaviour we do want here...
That's because we don't actually want simply importing this... client = httpx.AsyncClient() To raise a warning. Which if we treat the client as open simply by instantiating it, we would. The client is only opened once you're either using...
|
Nice, then I'm going to implement that (in this PR) |
e56a711
to
7eff422
Compare
@tomchristie I've done with the changes you mentioned.
I prevent the clients from being double-closed: class AsyncClient(BaseClient):
# ...
async def aclose(self) -> None:
"""
Close transport and proxies.
"""
if not self.is_closed:
self._is_closed = True
await self._transport.aclose()
for proxy in self._proxies.values():
if proxy is not None:
await proxy.aclose() Am I right, or I should remove this check? |
AsyncClient
.
So I tried this locally, and it took me a while to figure out that I wasn't seeing anything because We could issue it as a |
@tomchristie yes, I didn't pay attention to the fact, that
(c) python language reference (link) Update |
"\t>>> async with httpx.AsyncClient() as client:\n" | ||
"\t>>> ...", | ||
ResourceWarning, | ||
) |
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.
Right, I'm going to suggest that we use UserWarning
for this case, for better visibility.
We'll eventually end up tracking individual connections with proper a ResourceWarning
against each of those, but this is a good case for a higher-level warning I think.
Also, let's make sure to use a more concise warning format.
Perhaps...
warnings.warn("Unclosed {self!r}. See https://www.python-httpx.org/async/#opening-and-closing-clients for details.")
Then we can add a little more context in the documentation.
What do we think?
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.
Developing libraries/SDKs, I try to avoid to use such warnings as UserWarning
. This warning is for users' applications, not for a middle layer, presented by the SDK. Users should be able to separate their warning and httpx
warnings.
But I definitely agree with you, that ResourceWarning
(unfortunately) is too silent for this case.
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.
@tomchristie
I changed the warning type to UserWarning
, then I have to fix a lot of warning in the project tests (it's a separated commit)
458e6c9
to
9f88f8f
Compare
@cdeler - Thanks for all your fantastic work on this. I think it probably is worth issuing a PR just dealing with the "close async clients properly in our test cases" first, since we can pull that one into the |
Yes, sure, I'm to create this "close async clients properly in our test cases" PR today Update: I created the PR #1219 |
…ich is too quiet) to UserWarning (encode#871)
e898c07
to
8d85e87
Compare
According to your comment, I rebased this branch to the The only thing which is not done is
|
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.
Great stuff, let's get this in now! 👍
As a part of #871 it has been decided to prevent
httpx.Client
andhttpx.AsyncClient
from sending requests after being closed.Closes #871