django / asgiref

ASGI specification and utilities
https://asgi.readthedocs.io/en/latest/
BSD 3-Clause "New" or "Revised" License
1.46k stars 207 forks source link

What am I allowed to do with the send/receive callables? #436

Closed Hawk777 closed 7 months ago

Hawk777 commented 7 months ago

I can think of a lot of weird things an application could theoretically do with the send and receive callables:

  1. asyncio.create_task some other coroutine and toss them over there. The new task sends and receives things, and the original task awaits the new task.
  2. asyncio.create_task some other coroutine and toss them over there. The new task sends and receives things, while the original task immediately terminates.
  3. Create a thread and toss them over there. The new thread calls send and receive. It presumably can’t await the returned awaitables, because it doesn’t have an event loop, so it just sends the awaitables back to the application to be awaited.
  4. Create a thread and toss them over there. The new thread creates its own independent asyncio event loop and tries to call them and then await their return values there.
  5. Call receive (or send, or both!) 17 times, pass all the return values to asyncio.gather, and expect it to do something sensible.
  6. Stash them somewhere global and go to sleep. Have some other request, running in its own task, unstash, call, and await them.

Which of these are actually legal in the ASGI spec?

Intuitively, it seems like (1) should be legal, and the Extra Coroutines text “applications should ensure that all coroutines launched as part of running an application are terminated either before or at the same time as the application’s coroutine.” somewhat suggests that, as long as applications do that, the extra tasks will work OK, but I’m not certain whether it’s fully explicitly called out.

I’m pretty sure (2) is not legal, given the same text from the Extra Coroutines section, and the subsequent “Any coroutines that continue to run outside of this window have no guarantees about their lifetime and may be killed at any time.”

(3) doesn’t seem especially useful, but it does seem like something that might or might not work depending on the ASGI server implementation—if the callables are just async defs, it’ll be fine because they just stash their parameters and immediately return without any side effects, and the only thing you can do with the returned awaitable is send it back to the event loop where the side effects actually happen. On the other hand, the spec doesn’t actually say that they must be async defs; they could be ordinary defs that do other things and then return any awaitable (a call to an async def, a manually created Future, or whatever), in which case the initial side effects would happen on the wrong thread, presumably making this pattern not work. But I don’t see any text explicitly prohibiting it.

(4) is completely insane, but also AFAICT isn’t actually prohibited.

(5) seems potentially reasonable, and also seems like something that, absent any explicit specifications, could very well work on some servers but not others—a server that does its I/O in a separate task and uses queues to ship events (in their ASGI dictionary form) to and from the application will work totally fine if you do this, while a server that does its I/O directly in the send and receive callables will probably fall over badly when the incoming bytes get split up among the different receive tasks, particularly if you’re still in the middle of receiving headers.

(6) also seems potentially reasonable; I could imagine someone making something like a WebSocket-based chat server where each task calls and awaits its receive callable to get a chat message, and then forwards that chat message to the other users in the room by simply directly calling and awaiting the send callbacks belonging to all the other connections (and perhaps asyncio.gathering them)—crude, and probably lacking some robustness in terms of flow control, but a potentially workable starting point, and not entirely unreasonable since WebSockets have message framing to separate messages coming from different sources. Again, however, I can imagine this going wrong for certain server implementations for the same reason that (5) could.

Is it worth writing some text about what exactly an application is and is not allowed to do with the callables?

andrewgodwin commented 7 months ago

1 is fine, 2 is not as it is outside of the return of the main application callable so the spec has those covered.

3 and 4 won't work as the other thread won't have the same event loop as event loops are basically thread-bound, and I don't believe you can cross things over between event loops like that.

5 is perfectly fine (calling send() a lot is kind of encouraged by the spec). 6 is legal in the same way 1 is, but doesn't seem very useful.

All the situations you've mentioned are thus basically covered already; I don't see a lot of value in expanding on this much further as the only thing that's really questionable here is 4, and if you've got to those lengths then you should know perfectly well not to do that!