Have you looked at ASGI?
noteflakes opened this issue · comments
Hi Boaz, ma hamatsav?
Kudos on this initiative, I'm all for designing a successor to rack, for the same reasons you cite. Have you looked at the successor to Python's WSGI?
https://asgi.readthedocs.io/en/latest/
To me it looks really interesting, of course there's always the question of how to take into account different concurrency models. What do you think?
Hi @ciconia , Hakol Tov :)
Thank you for opening this discussion and sending the ASGI link.
I read through some of the documentation and it seems to me that ASGI's design is specifically designed for Coroutines (Ruby servers would have to use Fibers).
I think this will limit adoption in Ruby since, currently, Ruby servers use a wide range of concurrency models.
Puma, Iodine and Passenger all run on hybrid reactor loops (multi-process, multi-threaded event loops), Unicorn uses a forking approach (single-threaded), Flacon uses Fibers...
For this reason I think NeoRack should be implementable in any concurrency model, allowing servers to use their existing codebase. This is especially true for Puma, which is almost a community standard will eventually decide much about the future of NeoRack adoption.
Was there something specific you thought we could/should adopt from the ASGI specification?
The design I ended up with is a combination of various inspirations, including the current Rack specification, node.js, previous failed attempts (see iodine 0.1.x and the Rack::Push specification), the Rails API, and my own meandering experience as both server developer and application developer.
I wanted something that is both easy to use and relatively easy to implement.
I also wanted something that can be easily extended using 3rd party gems where possible and adjustable for backwards compatibility.
P.S.
I love how ASGI thinks about connections as "Protocol" objects. This its how iodine's engine (facil.io) handles connections under the hood (see here)... however, these are implementation details that might not fit every developer.
P.S.
@ciconia - I know that you are well versed and aware of most of the details I mentioned, however, since others might come across this thread, I thought it's better to explain things in more detail 👍🏻
The coming week proves very busy, so I'll have to postpone writing a detailed comment. I'd just like to mention some of the design considerations in ASGI that I find interesting and relevant:
- It's protocol agnostic.
- The app function takes three parameters:
scope
(a context object),send
&receive
. Bothsend
andreceive
are callables. - What is exchanged is not data or packets but rather events, which I find really interesting and much easier to extend in the future.
While ASGI is designed with Python's async/await in mind, and the send/receive callables are both awaitable, I think it should be possible to design an API using callables that can be used with either blocking calls (fiber/thread blocking) or callbacks as in EventMachine for example.
Thanks @ciconia for pointing out the advantages in the ASGI design.
Protocol agnostic
I agree with you about the importance of a protocol agnostic alternative - both after HTTP "upgrade" and as optional support for future connect
/ listen
features.
I chose to start pushing things in this direction with the Upgrade
extension draft. There was a lot of push and pull in the community about some of the details when the design was introduced to Rack, but I hope it will be adopted for NeoRack.
I think, after the design for "Upgrade" is finalized, it will be an easier to add a Connect / Listen draft that leverages the same design (i.e., adds a server.connect
method that adds a connection that behaves the same as an upgraded client).
The scope
parameter
scope
is similar in function to the env
in the CGI style Rack specification.
In NeoRack, the scope was divided between request-bound environment / detail, accessible using the request
Hash map, and global scope details, accessible using the request.server
/ DSL#server
object).
Using the request
object also for the local scope makes things easier when implementing backwards compatibility.
Much like the CGI style Rack env
, the request
object is designed so it could store information that's part of the request's lifetime, which makes that information always available to middleware and applications.
Exchanging events
It was very important for me that the design allows for backwards compatibility where possible, which influenced a lot of my choices for the core specifications.
Events would (and did) cause a lot of noise in the discussion, especially if design decisions end up creating a bias towards a specific concurrency model.
I am sure that once the design for evented connections is finalized and implemented, it would benefit a lot of applications.
Simplicity & Extendability
I am aiming for a simple core design that allows a simple NeoRack Server to be authored in a weekend or less.
I thinks this is important for many reasons, some of them are related to adoption rates and others to what I think of as transition and participation costs - the simpler the design, the more people can play around with it, participate in the discussion and smooth out the kinks.
This is why many of the more interesting features (present and future) will be placed in extensions rather than the core specification.
My personal roadmap includes Upgrade/WebSockets (see draft), pub/sub and client mode extensions.