Repository: encode/httpx

Issue:
Make ASGI dispatcher asyncio-agnostic
*Note: this issue breaks apart steps originally taken in #217.*

The ASGI dispatcher currently relies on asyncio-specific APIs to perform various operations, which is a blocker towards supporting alternative concurrency backends.

We want to make sure all of the I/O inside `ASGIDispatch` goes through the backend instead.

This will need the following, most of which can be tackled in independant PRs:

- [x] #257 Hard-code `self.backend = AsyncioBackend()` on `ASGIDispatch` to be able to iteratively defer asyncio-specific APIs to the backend.

- [x] #257 Introduce a queue-like interface:

```python
class BaseQueue:
    async def get(self): ...
    async def put(self, value: typing.Any): ...
```

along with a `.create_queue()` method on `ConcurrencyBackend`, to be used instead of/within the `BodyIterator`.

(For trio we'll probably need to add a `.close()` method as well, but that can be confirmed and tackled in due time.)

- [x] #260 Introduce a `BaseEvent` interface:

```python
class BaseEvent:
    def set(self): ...
    def is_set(self): ...
    async def wait(self): ...
```

along with a `.create_event()` method on `ConcurrencyBackend` to abstract `asyncio.Event()`.

- [x] #261 Remove usage of loop.create_task() via the background manager interface.

- [x] Lastly, turn the hard-coded `backend` into a parameter. Make sure it is optional and we use `AsyncioBackend` as a default.


Discussion:
> to be used instead of BodyIterator. The current .iterate() method on BodyIterator can be implemented adhoc inside ASGIDispatch.

I'd probably suggest "to be used *by* the BodyIterator" rather than "to be used instead of BodyIterator". That'd likely be a smaller change footprint. (Tho we can thrash that level of detail out against the pull request once we're there)

> Allow waiting for the first background task to finish on BackgroundManager

I think the proposed API there is decent, yup. To an extent it doesn't matter too much so long as we have something reasonable here, that we can rejig later if we wanted.
A thought here: we could probably forgo needing to implement a `wait_first_completed()` by rejigging the implementation to use queues for signalling completion from the two tasks.
Going to see if I can take a crack at dropping the `asyncio.FIRST_COMPLETED` bit of implementation, since that'd make this issue somewhat narrower in scope.
I’ll update the issue description soon taking into account the simplified concurrency in the ASGI dispatcher. :+1:
Would this affect https://github.com/encode/httpx/pull/52 at all? Or possibly the design of it?
Yeah it'll tie in. We'll want to get this side of things all nice and clean and sorted first, then we'll be able to take a proper look at #52 again.
I thought as much. Started on it but got sidetracked. I'll wait on this to settle first though, as it's more important for sure.

Resolving patch:
diff --git a/httpx/dispatch/asgi.py b/httpx/dispatch/asgi.py
--- a/httpx/dispatch/asgi.py
+++ b/httpx/dispatch/asgi.py
@@ -48,13 +48,13 @@ def __init__(
         raise_app_exceptions: bool = True,
         root_path: str = "",
         client: typing.Tuple[str, int] = ("127.0.0.1", 123),
+        backend: ConcurrencyBackend = None,
     ) -> None:
         self.app = app
         self.raise_app_exceptions = raise_app_exceptions
         self.root_path = root_path
         self.client = client
-        # This will need to be turned into a parameter on this class at some point.
-        self.backend: ConcurrencyBackend = AsyncioBackend()
+        self.backend = AsyncioBackend() if backend is None else backend
 
     async def send(
         self,


Test patch: