-
Notifications
You must be signed in to change notification settings - Fork 20
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
async client #50
async client #50
Conversation
@ofpiyush Would you be open to including an async client such as this? Happy to discuss if you have other ideas about how I should go about it. Thanks! |
@@ -101,6 +101,6 @@ def _get_encoder_decoder(self, endpoint, headers): | |||
else: | |||
raise exceptions.TwirpServerException( | |||
code=errors.Errors.BadRoute, | |||
message="unexpected Content-Type: " + ctype | |||
message="unexpected Content-Type: " + str(ctype) |
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.
@chadawagner nice work! In principle, I love the idea of Async client and from design POV, I don't see any issues. Having said that, I haven't looked at Python code or kept up with Twirp in a good while, not enough to do a proper review. Tagging @thecreator232 and @avinassh to take a look. |
Hi @ofpiyush, @thecreator232, @avinassh just following up, any chance someone has some time for a review soon? |
The PR looks great to me! but I don't have write/merge access anymore. Tagging @thecreator232 @raghav39 |
Thanks @avinassh! |
twirp/async_client.py
Outdated
def __init__(self, address, timeout=5, session=None): | ||
self._address = address | ||
self._timeout = timeout | ||
self._session = session | ||
self._should_close_session = False | ||
|
||
def __del__(self): | ||
if self._should_close_session: | ||
try: | ||
loop = asyncio.get_event_loop() | ||
if loop.is_running(): | ||
loop.create_task(self._session.close()) | ||
elif not loop.is_closed(): | ||
loop.run_until_complete(self._session.close()) | ||
except RuntimeError: | ||
pass | ||
|
||
@property | ||
def session(self): | ||
if self._session is None: | ||
self._session = aiohttp.ClientSession( | ||
self._address, timeout=aiohttp.ClientTimeout(total=self._timeout) | ||
) | ||
self._should_close_session = True | ||
return self._session |
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 go implementation doesn't create a client automatically.
I think it should be the same here.
def __init__(self, address, timeout=5, session=None): | |
self._address = address | |
self._timeout = timeout | |
self._session = session | |
self._should_close_session = False | |
def __del__(self): | |
if self._should_close_session: | |
try: | |
loop = asyncio.get_event_loop() | |
if loop.is_running(): | |
loop.create_task(self._session.close()) | |
elif not loop.is_closed(): | |
loop.run_until_complete(self._session.close()) | |
except RuntimeError: | |
pass | |
@property | |
def session(self): | |
if self._session is None: | |
self._session = aiohttp.ClientSession( | |
self._address, timeout=aiohttp.ClientTimeout(total=self._timeout) | |
) | |
self._should_close_session = True | |
return self._session | |
def __init__(self, address: str, session: ClientSession) -> None: | |
self._address = address | |
self._session = session | |
async def aclose(self) -> None: | |
await self._session.close() |
It can be used like this:
http_client_options = {
"timeout": aiohttp.ClientTimeout(total=timeout_s)
}
async with aiohttp.ClientSession(**http_client_options) as session:
client = AsyncHaberdasherClient(address, session)
await client.MakeHat(...)
...
async with contextlib.aclosing(AsyncHaberdasherClient(
address,
aiohttp.ClientSession(**http_client_options)
)) as client:
await client.MakeHat(...)
...
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! Yes, I agree, and I didn't love the idea of creating the session automatically, but I wasn't sure if some would prefer a simpler way to use it without having to provide and manage the underlying session. (Because the existing sync implementation with requests
has no such session requirement, I was hesitant to add one, and was trying to keep the usage similar for those who wanted to keep it that way.)
However:
aiohttp.ClientSession
can only be created within a coroutine, so if you are required to provide a session when you init the twirp client then you can only do so within a coroutine. (This is not how we currently init our client dependencies within our app, so I had planned to pass the client session with each request, but that might feel cumbersome to some folks)- If the caller creates and manages the client session, I'd think they should probably handle the closing of it also, rather than having the twirp client close the session
Thoughts?
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 think you can solve your problem by turning the client into an async context manager.
It would be something like that:
def init():
client = AsyncHaberdasherClient(address, session=None)
...
async def use(client: AsyncHaberdasherClient):
async with (
aiohttp.ClientSession() as session,
client.with_session(session),
):
await client.MakeHat(...)
with_session
can be implemented like this:
@asynccontextmanager
async def with_session(session: ClientSession):
backup, self._session = self._session, session
yield
backup, self._session = None, backup
Or using a factory? (need testing)
def init():
async def session_factory():
return aiohttp.ClientSession()
client = AsyncHaberdasherClient(address, session_factory=session_factory)
...
async def use(client: AsyncHaberdasherClient):
async with client:
await client.MakeHat(...)
You would need to implement __aenter__
and __aexit__
on AsyncTwirpClient
to call the functions on the session.
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.
After thinking a bit more about this, maybe you could just pass the http_client_options
instead of a factory to the constructor when creating the twirp client.
Then you can use __aenter__
and __aexit__
to create a temporary session which can be reused for several calls.
But it could also create a session directly inside _make_request
on each call for simplicity when there is no need to reuse a client session.
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 for the suggestions @MiLk! I don't mind passing the session in each request, but do you think I should add with_session
or session_factory
as something that's generally useful for others? I pushed some changes removing the auto-created client, and would be fine merging as-is but would like to cover other common use cases.
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 didn't see that the session
could be passed directly with each request.
You can disregard my previous suggestions.
I think that's enough, and there is no need to make the code more complex.
@avinassh I pushed new changes to remove the auto-created client session (the caller is now required to provide a session either on init or per request). Just letting you know in case you'd like to take another look, I'll assume your approval still stands unless I hear otherwise 😄 |
@ofpiyush Looks like we have some consensus and a couple of approvals here, any chance we could merge and cut a |
Both @avinassh and I have moved on from the company and don't have write access anymore. Tagging @thecreator232 and @raghav39 to possibly check with internal services and make a release. |
@thecreator232 @raghav39 any hope for a merge/release here? This PR was opened 2 months ago 😭 |
@thecreator232 @raghav39 @avinassh @ofpiyush we seem to be at a standstill here. Is there any hope of getting a PR merge and release? Or is this repo officially abandoned? |
Let me get this reviewed this week for the current services dependent and we will have this actioned. |
Hi @ravipetlur, any luck? |
@chadawagner We are getting this checked internally, let me get this expedited. Sorry for the delay. |
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.
LGTM. I tested the async client with internal services as well. Didn't spot any problems. We can merge this 👍🏻
The only issue is when running setup.py
, it expects a version.txt
. PEP440 doesn't allow for a version like 0.0.8
so maybe we can add a file there with a version number 0.1.0
or something?
Hi @WarrierRajeev ! Yeah I'm 👍 to increasing the version, is that something y'all can handle internally? Idk what your release pipeline looks like. In addition to whatever |
Hi again @WarrierRajeev @ravipetlur any updates on a merge and release? 😬 |
Adds support for async clients
This PR addresses #43 and #46