Repository navigation
Add support for trio to client - #458
Conversation
Signed-off-by: Anuraag Agrawal <anuraaga@gmail.com>
…nto client-trio
Signed-off-by: Anuraag Agrawal <anuraaga@gmail.com>
Signed-off-by: Anuraag Agrawal <anuraaga@gmail.com>
| except RuntimeError: | ||
| # Not asyncio, so trio: the only other event loop supported. | ||
| from ._server_trio import TrioLoop # noqa: PLC0415 | ||
| from ._loop_trio import TrioLoop # noqa: PLC0415 |
There was a problem hiding this comment.
I think we ought to pick the loop the same way pyqwest does (sniffio), since right now the two can disagree. e.g. trio guest mode on an asyncio host: sniffio says trio, but get_running_loop() succeeds so we hand back AsyncioLoop, and every unary call fails, deadline or not:
import asyncio
import sniffio
import trio
from pyqwest import Client
from pyqwest.testing import ASGITransport
from connectrpc._loop import current_loop
from test.connectrpc.example.haberdasher_connect import Haberdasher, HaberdasherASGIApplication, HaberdasherClient
from test.connectrpc.example.haberdasher_pb import Hat, Size
class Fast(Haberdasher):
async def make_hat(self, request, ctx):
return Hat(size=request.inches)
async def trio_main():
print(sniffio.current_async_library(), type(current_loop()).__name__) # trio AsyncioLoop
app = HaberdasherASGIApplication(Fast())
async with HaberdasherClient("http://localhost", http_client=Client(ASGITransport(app))) as client:
# ConnectError: INTERNAL 'Timeout should be used inside a task', with or without timeout_ms
await client.make_hat(Size(inches=3))
async def host():
loop = asyncio.get_running_loop()
done = loop.create_future()
trio.lowlevel.start_guest_run(
trio_main,
run_sync_soon_threadsafe=loop.call_soon_threadsafe,
done_callback=lambda outcome: loop.call_soon_threadsafe(done.set_result, outcome),
)
print(await done)
asyncio.run(host())Probably pretty niche, but sniffio.current_async_library() plus a module-level TrioLoop is also a fair bit cheaper on trio (~50ns + ~17ns vs ~490ns for the raise/catch and a new TrioLoop per call). (The server already detects the loop this way, so that side predates this PR.)
| # ASGITransport cancels the application once the client has the | ||
| # whole response. A server lets it finish, so the shield blocks | ||
| # that cancellation. | ||
| with trio.CancelScope(shield=True), trio.CancelScope() as self._scope: |
There was a problem hiding this comment.
Since the app runs shielded, nothing outside can stop a hung handler. Tried a handler that just does await trio.sleep_forever() behind Wrapper: the fail_after(1) around the call never fires, and the test sits there until pytest-timeout (1800s here). Without the wrapper it fails after ~1s with TooSlowError. Maybe a trio.fail_after inside the shield, so a hang still fails fast?
| finally: | ||
| # A call can end before the request stream does, for example when the | ||
| # server responds early. | ||
| await _aclose(msgs) |
There was a problem hiding this comment.
Closing the request stream seems right for generators (on main their finally doesn't run until GC), but _aclose closes anything with an aclose, including things the caller owns. e.g. pass the receive side of an anyio memory stream to a client-stream call that the server ends early, and the producer's next send raises BrokenResourceError. Not sure what we want here; maybe only close async generators (inspect.isasyncgen)?
…nto client-trio
6e0e39d to
58a8c90
Compare
|
Thanks - fixed them |
Signed-off-by: Anuraag Agrawal <anuraaga@gmail.com>
A large part of this is parametrizing anyio tests (instead of pytest-asyncio) to run on both loops, similar to pyqwest / pyvoy. I also went ahead and replaced hypercorn in unit tests with ASGI transport