mbr / tinyrpc

A compact, modular transport and protocol agnostic RPC library. Does jsonrpc v2.
https://tinyrpc.readthedocs.org
MIT License
156 stars 53 forks source link

WIP: Asyncio dispatcher #65

Closed DrPyser closed 3 years ago

DrPyser commented 5 years ago

Gave a try on an implementation of an asyncio-based dispatcher, along with a basic test suites adapted from existing test_dispatch.py.

Tricky part was supporting both python 3.4/3.5 and python 3.6+ because of async/await syntax being only supported in 3.6+. I made it work by having two separate implementations in separate modules and exposing the right version through a conditional import. Maybe this could be simplified by using only 3.4 style coroutines, not sure of the tradeoffs(other than being able to drop 3.4 support more easily, and avoiding issues mentioned below).

TODO:

DrPyser commented 5 years ago
DrPyser commented 5 years ago

Also, I rebased on master, hence the number of commits.

DrPyser commented 5 years ago

@lnoor Just found #55. Not sure if my version adds anything, other perhaps an example of python 3.4 compatible support. Also, I think it would be a good idea to add testing with environments for 3.5, 3.6 and 3.7. I myself work exclusively with 3.6 and 3.7.

Not sure what's your roadmap for version 1.0, and your plan/vision for asyncio support. It can still be developed and packaged independently, as an extension, provided there's a stabilized api for 1.0.

After some thinking, what would mostly benefit from async implementations are the client and the server. From what I understand about single-threaded coroutine-based concurrency like asyncio(but which should also applies to equivalent alternatives like gevent, eventlet, etc), for an application to benefit from an async dispatcher, all methods would also need to be fully async, and most importantly, mostly io-bound(i.e. not cpu-intensive). An example would be a proxy-like application, where incoming requests are mostly handled by making external requests, with little actual work done on the data passing through. Or a simple CRUD api over a database. asyncio and equivalents make it possible to handle lots of io work like receiving and sending requests concurrently, since network io(io in general) is mostly about waiting, so scalibility depends on efficient task switching in a single thread(which coroutines enable). Any code which does cpu-intensive computation would not benefit much from single-threaded concurrency. In my case, I have http and websocket apis implemented using async frameworks like sanic and starlette. The actual method dispatch is run in a process pool to avoid blocking the main thread. I guess I'm still limited by my pool size in that situation.

So the RPCClient and RPCServer would benefit from async implementations to enable lots of concurrent non-blocking communication, but I think in most applications the dispatcher will probably have to call sync functions doing cpu-bound work. An async dispatcher could still be useful in some applications like I mentioned above. However, I think it's also possible for sync code to dispatch io-bound operations implemented using async code to an event loop in a separate thread, so you can have some sort of async-sync-async sandwich, with an async layer handling connections in the main thread, dispatching sync code in the next layer to a separate thread or process, which itself can dispatch io-bound work to an event loop in another thread.

Sorry for the huge chunk of text, hope it's useful.

lnoor commented 5 years ago

Hi Thanks for the huge amount of text :) and the code.

As for the 1.0 roadmap. I may add the https://github.com/mbr/tinyrpc/pull/6 PR but other than that I just want to rework documentation and examples and leave it at that.

Asyncio could become a 1.1 release. Packaging it as an extension appeals to me, that is an excellent idea!

There really is no point using Python in a multithreading application except when dealing with I/O. But in that case event loop based libraries are a lot more convenient. So I completely agree with your analysis of use cases.

Although I suppose Asyncio is the future I dislike the impact it has on every piece of code. Personally I prefer the low key approach of gevent. But it is asyncio that is part of the standard library.

I want to study your code and comments more closely but first I want to release 1.0. Also I need to 'play' for myself with asyncio and get more acquainted with it. As soon as that is done I really want to explore an asyncio extension. Hope that by then you still will want and have time to cooperate.

DrPyser commented 5 years ago

Your plan make sense.

I sympathize with your concern with asyncio/explicit coroutine style. However, as other have argued, there is value in making asynchronous code and yield points explicit. It makes it easier to reason about and debug. At any rate, I still have a lot to learn and understand about various approach to async programming(e.g. gevent & co. vs asyncio & co).

I also started learning about competitors to asyncio in the "explicit async" paradigm, e.g. trio and curio. They try to offer a better and simpler api than asyncio for async programming with async/await. Although asyncio was chosen to be part of the stdlib, it's not the only choice for async programming, and maybe not the best. Although obviously it makes it the default, but consider requests vs. urllib...

Anyway, looking forward to 1.0. What's do you think about adding testing support for 3.6 and 3.7?