-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add WebSocket and SSE options #1272
Conversation
Interesting. I already support completely transparent asynchronous web sockets in falcon. It uses Of course with this model supporting SSE is also trivial. Regarding the event driven model which is being exposed via Regarding the overall design - careful thought needs to be given to how HTTP/2 fits into this picture. Ideally, SSE, WebSockets, and other "interactive" streams, transit via reading from the request body and writing to the response body. That's the model which I feel works best with HTTP/2, where each request/response is encapsulated into a full duplex stream. |
Bare in mind that This is also one of the challenges of implementing |
I think the HTTP/2 layer is something that's handled only by the server. It shouldn't change the I think the idea is great because it decouples the application from any network logic. While EDIT: I also published a blog post about this PR and tried to explain this difference on a Reddit thread. I'm not sure how well I did explaining my thought on the matter, but I think it's a wonderful approach. |
@boazsegev While I see where you are coming from, I think that your idea of a semantic model is much higher than what is exposed by HTTP. Given the direction of HTTP/2, it's clear to me that the base HTTP semantic model is a full-duplex un-buffered stream of chunks. Whether you implement this with multiple HTTP/1 connections or HTTP/2 streams (which to me is the network layer), is irrelevant to the application. The application simply sees a full duplex stream of chunks. That's the semantic model for the application. On top of this you can implement your desired web sockets, sse, and other interesting things, like streaming responses, and so on. Your semantic model has a tight coupling with your desired application level protocols, but I think that's a very limiting design. For example, you can't implement general streaming responses like https://github.com/socketry/falcon/blob/master/examples/beer/config.ru (the fact it uses |
I think I should mention this discussion here since it seems relevant: #1148 |
I'd like to point out that Rack is event based as are the WebSocket and SSE API/specs. The Rack hijack offers a streaming back door that is not addressed by the spec. This PR attempts to formalize an approach to using WebSocket and SSE that is consistent with the use pattern common in modern web applications with JavaScript front ends. It is not an attempt to get rid of the hijack option. There is certainly reason to continue support for hijacking but that should not be a reason to reject this PR. |
@ioquatix , you're totally right, I can't implement that code... nobody can unless they implement As for streaming solutions: SSE is a streaming solution. If you refer to video streaming or binary data requirements, WebSockets are perfect for that. If you meant old-school HTTP streaming... yes, that's true. I'm not even sure that I should, but I'm aware that I don't. In fact, I'm pretty sure the Rack model wouldn't be ideal for streaming anyway. |
If anything, rack is based on composable middleware. There is no event handling.
Sorry, but this is complete dump trucks:
This is the guts of the matter. Why is this necessary? It's clearly already possible to do it - so we have to ask the question, do we need to formalise this in rack, or is there something simpler that we could formalise which allows more possibilities. We should accept or reject this PR based on the merits of what it adds to Rack, as a specification. I think there are better, more generic, options. |
I don't think I proposed that anywhere. I just said, with the current Rack spec, you can do it, and it scales well. We don't need to change the rack spec to have a great implementation of web sockets.
It only works in one direction and it only works in a very specific context. It's not general response streaming, as in
The Rack model is great for streaming responses. That's the whole point of the response body responding to |
By the way, I'm not trying to shit all over your PR, although it probably feels like it. Congratulations for making something, and implementing it and proposing it. Basically, I just don't see the logic of it though. I think there are better things in Rack that need to be addressed, and I think the direction of HTTP/2 confirms this. |
Of course Rack is event based. The trigger for all Rack code is the |
I'm paraphrasing slightly to focus on your question, forgive me and correct me if I misunderstood:
I believe this is necessary because There's a difference between simple and easy. It might seem simple for Rack and maybe even simple in the server, but it's a super complex solution that requires applications to add IO handling code. The Sure, we might need to keep On the other hand, the callback approach in this PR is simple, but it's clearly not easy. Servers will have to put in some work (less work than applications need to put in for However, the solution is simple and follows both DRY and SOLID principles. The callback approach isn't a generic solution, it's just good for things that lend themselves to using callbacks, but this is the advantage of this approach. IMHO, This is a perfect fit for everything callback related and it's extendible (if WebSockets 2.0 come around, nothing needs to be changed on the Rack/Application side, just the server). |
@ioquatix , I was looking through the falcon repo and you seem to have done great work there. I understand your concerns and your wish to have a better solution. It seems to me that you yourself put in a lot of time and effort into improving the sated of affairs and making things easier and better for everyone. I appreciate your approach and I find myself slightly surprised by the challenges you raise against this PR. It seems to me that you're using similar semantics with different method names (i.e., your WebSocket Sure, my implementation is evented and your implementation is multi-threaded, but both lend themselves to the same approach - application side "callbacks". Your implementation could just as easily remain threaded and hide the I'm not sure I understand your reservations. |
Those are all really good points and I appreciate your thinking. To be honest, in falcon, implementing your proposed API wouldn't be more than about 100 lines of code, perhaps less. So, it's not complicated to implement, because falcon already has a well defined and good concurrency model. That's the context in which I'm thinking about your PR, by the way. Whether the complexity is in the server, or the application code, is an interesting point and one that I can agree with to a certain extent. I don't think there is a strong argument either way.
Thanks, I can tell we are going to get along well now.
Yes, but I still haven't made a 1.0 release yet. It's coming soon hopefully.
Fair enough.
That's almost right. Async doesn't use threads, it uses fibers which are cooperatively scheduled. They have much less overhead than threads.
Async is event driven. When a fiber performs an operation that would block, it yields back to the reactor which resumes once the operation can continue. I deliberately try to avoid callbacks because in my experience they lead to callback hell. The difference is that with callbacks you need to use variables to track state, and each time the callback is invoked you have to process that state to figure out what to do next. With fibers, you naturally resume where you were with your stack intact. You can implement complicated state machines with ease.
Yes, this is feasible.
I like this PR from the point of view that it tries to provide some generic interface for web sockets and server sent events. Making a generic interface for that is both simple and difficult. I admire the you've done it and implemented it. My main reservation is that your proposed API is incomplete. The rack specification as it stands, is really simple and allows for a lot of flexibility. What you've proposed is an extension which fundamentally encodes a model for concurrency into Rack, something that it hasn't had except in a very precise location (response body To look at it another way, Your proposed API exposes an incomplete model for concurrency and the phrase that comes to my mind is "thar be the dragons". What happens if you stick a Whatever way you cut it, Ruby has a pretty frustrating model for concurrency right now (and IO too, but it's slowly getting better). People are working on it, myself, others (Eric, https://bugs.ruby-lang.org/issues/13618), ko1 (guilds) and I have no doubt that good things are coming to this space. But, Rack, right now, has to be a specification that works with Ruby as it is today. If |
@ioquatix interesting, thanks for taking the time to explain. I love the fiber approach, as it hides away the event reactor and make things more comfortable to manage.
Yes, you are right that there are concerns related to the proposed approach. It doesn't deal with IPC or client<=>client communications. It also allows developers to violate multi-threading best practices. However, this proposal seems pretty balanced. It doesn't enforce a huge change on existing servers and the "missing" pieces are easily complemented by existing solutions (Redis being just one of them). Personally, I implement pub/sub in iodine to "fill the gap". However, I'm not sure this should be part of the specification since other developers might wish to "fill the gap" using a different approach, such as limiting the server to a single process and using an array.
I'm not sure I understand this part. The server can be a single threaded server, a thread-per connection server, a fiber based server and practically any type of concurrency model can be implemented. I'm not sure I see where the PR requires a specific concurrency model.
That really depends on the server, I guess. What happens if I call It's probably the same answer. Iodine, for example, supports multi-threading and cluster mode (pretty much the same model as Puma). Agoo (I think) supports multi-threading. For both of these servers I would recommend avoiding blocking calls within the thread, but they both offer some level of protection before experiencing DoS. I don't use fibers so I'm not sure how they would react to this scenario. Hopefully they will react better. Either way, I doubt the question of having fibers replace threads is part of the discussion here.
I agree with it, but I don't see why that matters. Even the node model needs to be adjusted when scaling horizontally. All models have properties that define their strengths and weaknesses. IMHO, this PR proposes a mechanism that's independent of model used by the server, making it flexible enough for everyone to implement. |
How do servers support your proposal? By pulling in
Invoking a Rack app is for the most part trivial. It's a function call that returns a result. No real model for concurrency is needed for this basic definition. You scale up by executing the function on different CPU cores, but fundamentally you can't change it to a non-linear event driven callback model or some other asynchronous model ( This proposal embeds non-linear flow control into the Rack spec. What I mean is, it's impossible to implement the given proposal without some kind concurrency. I'm not saying that any particular model for concurrency is being encoded, but just that by your approach a model for currency is now necessary. This has a huge down-stream effect, since all code that depends on Rack now has to be aware of and capable of asynchronous execution. For example, how would you change I think this equally applies to
In Have you tried using ActiveRecord in your
Because with your proposed API, implementing a basic Node style server like this is not possible. Right from the get go you need additional machinery to do pub/sub or other kinds of communication. Even the situation with streaming responses is not improved without additional work. It's a very specific proposal designed for a very specific kind of scalability. It's far too specific IMHO. What we need is a proposal that better aligns with HTTP/2 streams since it's clear to me that it should be the future of HTTP and Rack. It should have a clear model for concurrency that fits with the existing multiprocess/multithread/worker implementations - i.e. reading from Streaming request and response bodies in But this PR requires significant changes to existing apps for any kind of benefit. Not only that, but it only supports a very specific kind of scalability with an under-specified concurrency model (i.e. what happens if you block in Let me finish with the following question: Do even think there is a future for WebSockets? https://datatracker.ietf.org/doc/draft-hirano-httpbis-websocket-over-http2/ hasn't been touched since 2014. There is an interesting write up here: https://daniel.haxx.se/blog/2016/06/15/no-WebSockets-over-http2/ - WebSockets are something which has never been a good fit for the request/response paradigm and that's something which fundamentally underpins Rack (assuming that |
SPEC
Outdated
on_shutdown() # may be called before a connection is closed due to server shutdown. | ||
on_close() # called when the connection is closed | ||
on_drained # may be called when the number of pending writes drops to zero. | ||
The object will be extended by the server to include: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will break method caches. I'd prefer a solution that doesn't lean on runtime method extension. Maybe something like this:
class MyWSEObject
def initialize io
@io = io
end
def on_open
@io.write("neat")
end
end
app = lambda do |env|
env['rack.upgrade'] = MyWSEObject.new(env['rack.wse.object'])
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
class MyWSEObject
def on_open(io)
io.write("neat")
end
end
app = lambda do |env|
env['rack.upgrade'] = MyWSEObject.new
end
would be simpler
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be fine too. 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, the next question, is it a good idea to write into the request env
to essentially send a response? To me, that's confusing. Perhaps at best it could be something like
if upgrade = env['rack.upgrade']
return upgrade.call(MyWSEObject.new)
end
return [400, {}, []]
Perhaps the server could signal exactly what APIs are supported as in:
if upgrade = env['rack.upgrade.web_socket'] # or rack.upgrade.event_stream
return upgrade.call(MyWSEObject.new)
end
return [400, {}, []]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBH it doesn't really matter to me. Unfortunately though, env
is really the only way that middleware can communicate. So writing to env
is the only way that one middleware could indicate to the next "I'm responding by writing to a websocket". If the intention is that any "callable" (middleware / app) that sets the WSE object must not call anything else in the middleware chain, then that needs to be explicitly stated in SPEC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ioquatix having the env['rack.upgrade']
respond to some kind of registration method would be a reasonable approach in my opinion. I feel a little uncomfortable having it provide a return value since it makes it less obvious what is being returned but my feelings are not very strong on that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For C extensions, there's double the amount of objects per connection.
Why is this?
@tenderlove , I assume you know that the C server doesn't need an IO object, it uses a simple integer to keep track of IO.
This means that a C extension just needs to keep the callback object itself and add a secret variable to that object with the socket number.
This secret variable isn't possible in Ruby, but C extensions are allowed to create them. Since that variable is a Number, it's very small (it doesn't create an Object in the ObjectSpace realm and it's immutable).
This means a single object in C (the callback object). If we add the IO object, it's two objects - a 200% increase in object count.
In Ruby, we will have the callback handler, the raw IO object (both pre-existing) and an IO wrapper, a 150% increase in object count.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This means a single object in C (the callback object). If we add the IO object, it's two objects - a 200% increase in object count.
What % increase is that in an actual app?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What % increase is that in an actual app?
That really depends on the app's use-case.
A micro services app running a WebSocket API will get hit harder than an HTTP app with the occasional WebSocket upload manager.
I think the issue is more pronounced because we are talking about long term objects.
Short term object count doesn't effect memory fragmentation as much as the fact that there's a long-term object blocking a memory "block" / "arena".
The more long-term objects are created (and eventually destroyed), the faster the fragmentation.
Having said that, even a 200% increase in object count is probably better than the solutions we currently employ when implementing WebSockets using hijack
.
I won't make an issue out of it. I'll do what everyone thinks best, but I know in very practical terms (having coded both implementations in C) that extend
is cheaper both in memory and in the number of method tree traversals required.
Hi all! Websockets and rack again, let's do this! Up front, last time this came up, I prototyped it in puma (puma/puma#1054). It was an experiment we didn't merge in, but it totally works and we might revisit it. The API of on_* methods for events is totally fine, no issue there. Extending the object to decorate it with a write method is pretty ugly, from both an implementation statement as well as a performance one. Passing an object that implements #write to the on_* methods to write back is less code and performs better. If frameworks want to take that object and use extend to make it's methods available on the handler, that's totally fine and up to the framework. Rack should not do that on it's own, it should provide a lower requirement. |
You should be able to opt to the "type" of upgrade you are after I can already see 2 use cases that this protocol does not help at all With the current example use of:
I would recommend
I am not super happy about |
Rather than |
Another thing that makes me somewhat uneasy here is that it still suffers from the very ugly trait that deep down the middleware stack someone returns rubbish to all the rest of the pieces that then gets thrown in bin. So... to mitigate this very bad problem I would recommend:
Overall I am leaning toward not even mucking with rack long term for a lot of this stuff and moving to fiber servers that can pause and resume more cleanly, multiplexing tons of connections cleanly. A big blocker there though is the MRI does not let you ship fibers between threads now which is not ideal. That said this works ... so ... yay ?https://github.com/SamSaffron/performance/blob/master/fiber_server/server.ru |
Also... one extra big question here is ... do we want to bloat all of our Rack servers out there with all this extra logic when simply adding a single dependency to middlware that implements this today is already feasible on top of hijack? should we not start with |
Fibers are actually bound to the thread that creates them by definition. Some designs use a more generic coroutine structure (e.g. green threads) which you can move between threads. If you are interested in this, you might find https://bugs.ruby-lang.org/issues/13618 interesting. If you are interested in Fiber based servers, check out falcon. https://github.com/socketry/falcon/blob/master/examples/beer/config.ru and https://github.com/socketry/ which is a complete stack of asynchronous components.
Yes, I strongly agree with this, and the answer IMHO is no. |
@evanphx I appreciate you opinions and it is good to have a different perspective than what we had. I can't agree that the performance would be any different in one case over the other though. The other points are certainly worth considering. |
yes, I am across Eric's work there and hope we get something in MRI, I also think Koichi is open to having a protocol for allowing to move Fibers between threads, and I want to see proper green threads back. Also seen Falcon, I agree that a fiber based server is very very appealing for a bunch of workloads it simplifies so much of the mess we have now with pure threaded servers and slow requests, especially cause you can walk down the middleware stack properly in the right time. |
TBH I'm OK with it if it's an opt-in. This proposal seems simple enough that someone could implement a "hijack" based approach using a middleware. The upside of formalizing it is that if webservers want to implement a high performance version of |
@SamSaffron in regard to on_open taking an io, env, and repsonse. By the time |
@tenderlove My issue though is that encourages a whole bunch of code duplication... say Puma takes this on now it needs to ship with a websocket protocol as a strong dependency, or worst still it will carry its own duplicate websocket protocol thingy. Chunked encoding is easy enough but there is a fair amount of code to do websocket upgrade and encoding depending on how far you want to take it and how many protocol variants you want to support. |
I only think |
Reading through the thread I noticed how hard it might be to predict a This means that user code must use Since a failing I think this will improve performance and also prevent the obvious nesting of exception handling code (on the server and the app). Thoughts? |
I had assumed raising an exception was the natural behavior but maybe that is not the best approach. Even returning true or false does not necessarily indicate the message was delivered. I think the real question is, what information would be useful to the app. Maybe nothing. Callbacks exist for closing which lets the app know writes will no longer be delivered. Is an acknowledged protocol desired on top of WebSockets? I think that might be a step too far. |
I think the next steps are:
|
There are currently two implementations of the originally proposed spec, Agoo and Iodine. I'd think that if proof that the spec is workable. Waiting for several applications to be implemented against a spec that has not been approved seems rather unlikely. Does that pretty much means not changes can be made to the spec because there is no spec to follow. Rather circular logic. So the purpose of this PR was to refine the proposal and then get a vote which would be result in this PR being either approved or rejected. How do we get to that phase? |
I don't think so. Making some sample apps is a great way to document the API, provides a great starting point for new users, etc. And finally, if the sample apps can't work between servers, what's the point of a shared SPEC? |
I think we need to see a working implementation in at least Puma & Passenger as a baseline since that is what is very common for Ruby development and production. |
I believe the suggested bar was several practical applications. There are already sample applications in the Agoo and Iodine repositories. |
Something more practical than "Hello World". For example, a shared real-time Todo list :p I think we should be involving some database activity in the callback, probably using ActiveRecord or Redis. If you've already got several sample applications, once we finalise a draft SPEC and we all implement it, we should be able to all run those apps on our respective servers, right? That's the whole point of having a shared SPEC. |
I still don't see a statement about whether I'm also unconvinced that pending/drained is an effective solution for non-blocking writes. I remain of the opinion that this should remain a draft / small-e extension while real-world implementations adopt it and fully exercise it. Easy-mode for convincing me would be a PR to Rails adapting Action Cable to this API -- which would also allow like-for-like benchmarking. |
Although I understand the suggestion (adding a Rails PR), I find that quite unfair. I can program a server in C and I can wrap in a gem for Ruby MRI... but I know nothing about ActionCable, it's inner workings or the Client<=>Application protocol used above the WebSocket layer. The learning curve and time spent on a Rails patch would only be viable if the PR is conceptually acceptable and we all intend to move forward with the proposal and implement it. I really doubt if pushing a PR to Rails is something that should happen before we agree that the PR is conceptually acceptable (i.e., contingent on benchmarks, or whatever). As a side note, I also don't think this should enter a Rails 5.x update release. This PR has the potential to retire the Rails IO handling layer (the one based on |
@matthewd , I assumed (since there's no statement) that it would be implementation defined. I'm assuming that blocking servers will be avoided by applications that are effected by the behavior. But I totally understand if you believe there should be a concurrency model requirement regarding |
on_drained(client) # may be called when the number of pending writes drops to zero. | ||
The <tt>client</tt> is used for writing and checking status of the upgraded connect. It has these methods. | ||
write(message) # writes to the WebSocket or SSE connection | ||
close() # forces a close of the WebSocket or SSE connection |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to have that, implement it as close_write
which is what Ruby calls shutdown(SHUT_WR)
. close
should guarantee after returning that the underlying socket is closed. Otherwise, you are in for a world of pain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agoo treats a cal to close as a request just like a write and places the close on the queue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ohler55, so does iodine. I place a "close" marker on the packet queue, so close
is always performed after all scheduled data is sent.
@ioquatix , I think your missing the point of abstracting away the network and the protocol.
My suggestion was about clarifying this little bit, not changing what both iodine and agoo already implement.
We aren't authoring a network layer. We are authoring an abstracted application side API.
The reasonable exception is that write
is performed before close
. i.e., if my code is:
write "hello"
close
The reasonable expectation is that "hello" is actually written.
There's no force_close
or close_write
in the specification because the application shouldn't be concerned with these things. If the application doesn't want data written, it can avoid writing it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can make close call flush
, or you can flush after every write. But you make close call flush, you better be careful about EPIPE
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a high level protocol like this, calling flush after each write would make sense to me.
It provides the user with a strong expectation, that after calling write, the data has been sent, and pending a network failure, would arrive, or else the write fails right then, with, say, EPIPE
. Otherwise you'll just end up with a spaghetti state machine trying to handle all these conditions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hope I don't seem too blunt or crude. I very much appreciate the interest and willingness to polish the specification and make it both as clear and as practical as can be.
However, please consider the model to be totally separate from the network - there is no network. There's only this API.
We can change the API if we need features, but we don't expose network bits or logic because part of our job is to abstract these things away - so there is no network, there is no protocol (as much as possible).
In this sense, flush
doesn't exist. It's a network / server detail that the application never sees, abstracted away by the server.
The closest an application can come to ask about these things is to ask about all the pending
outgoing write
events that haven't yet completed. This allows an application to know if the on_drain
callback is somewhere in their future.
The pending
query doesn't to expose the network, it exposes the progress of existing API calls. This provides important information about the possibility of a slow client (or a slow clearing "queue") allowing an application to stop serving a resource hungry "client".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand your response you are answering yes to the question of whether or not a flush
method should be added. Then in any application you wrote you would block until the write completes instead of making use of the on_drained
callback. That is your choice of course.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, please consider the model to be totally separate from the network - there is no network. There's only this API.
Fair enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand your response you are answering yes to the question of whether or not a flush method should be added. Then in any application you wrote you would block until the write completes instead of making use of the on_drained callback. That is your choice of course.
I really find the inverted flow control of callback style programming horrible. So, I prefer #flush
over an #on_drained
callback. Callbacks = state machine spaghetti = hard to maintain/buggy code. It's just my personal opinion, FYI.
@matthewd would a statement leaving the blocking behavior up the the server be enough or maybe a method to ask if blocking would block other threads? |
I have an initial development implementation of the updated draft, placed in iodine's 0.6.0 version branch (installable from source). There's some features being re-written (such as pub/sub extensions, including Redis integration), but the core WebSocket/SSE and pub/sub API will work. I think this would be enough to author a test application using a monkey-patched Rails... @matthewd , any final changes / requests / tips before we start woking on a "like-for-like benchmarking" example app? How do you want to benchmark this? Should we use the websocket-shootout approach? (it might not test what we're looking at, as memory consumption and performance will still depend heavily on the pub/sub system rather than the connection system...) |
The Agoo develop branch is also compatible with the spec as described in the current state of this PR. Should make a versioned release later this week once the named pub-sub feature is completed. |
I cleaned up the formatting of the SPEC around the upgrade section. An HTML version is at http://www.ohler.com/agoo/rack/file.SPEC.html. |
@evanphx , how is the Puma adaptation going? So far, the API iodine implements for the It includes a number of methods not listed in the specification besides the Pub/Sub extensions (the I also added the Anyway, I hope to see this move forward. Iodine has been serving clients like this for the last couple of year or so (albeit using |
If you need extensions to make anything useful they need to be part of the spec IMHO. |
@ioquatix , this was my thought as well, which is why I wrote it here. Perhaps these added methods ( On the other hand, I don't have to have the EDIT I should note that the |
The entire pub-sub pattern can be implemented by keeping an Array of connections. A publish would then call write on each one. The reason pub-sub was not included in the PR was that it added another new use pattern to the spec that would take even more effort for servers to comply with. Like @boazsegev and Iodine Agoo includes a pub-sub implementation that is compatible to a large degree with Iodine. In then end we wanted something basic that could be used with a minimum of changes in Rack and the way developers use it. The current PR focuses just on Push without also introducing pub-sub. Maybe a future PR can propose the addition of pub-sub. One step at a time. |
This wouldn't work with falcon in forked mode, since it forks one process per CPU core and they all handle connections independently. Even in threaded mode, some kind of synchronisation is required. I believe the same problem would affect puma in cluster mode, for example. My opinion is that this spec needs to be feature complete, as in, it's possible within the confines of the spec to implement actual useful websocket applications. Otherwise, what's the point? If every server supports the core spec but provides their own incompatible models on top of that, this isn't going to work out very well. |
The PR, as it stands, is feature complete for WebSocket and SSE. Sure, extending the PR to include pub-sub would be a problem for servers that don't share state but that has nothing to do with WebSocket and SSE support proposed here. @ioquatix, Slamming @boazsegev example code that is meant to demonstrate not only the proposed Rack additions but also other features of Iodine and then claiming that example invalidates this PR is ludicrous. The PR offers a clean and simple way for even new developers to take advantage of WebSockets and SSE. Some of the comments such as those from @tenderlove were constructive and genuinely aimed toward enhancing the current Rack spec with the addition of WebSockets and SSE. You have fought tooth and nail against it from the start with an extreme passion to block any variation on the PR. I don't understand why you feel so threatened by this PR. Can you explain why you have such strong feelings about offering Rack users an simple to use approach to WebSocket and SSE? |
Okay, this conversation clearly needs to go back on ice for a bit. I don't agree that pub/sub is within scope, but if we can't do each other the courtesy of assuming genuine opinions and positive/constructive intentions, then we're not going to get anywhere useful. |
TBH I'm kind of worried about committing to an API without a real world app using it. The reason is because I want to make sure we don't forget anything we need to implement actual apps. If we could get Action Cable running on top of a server that used this API, then I'd be satisfied to merge. |
There has been some great discussion here. Thanks everyone for their effort and passion. I am going to close this issue, because I don't think we reached a consensus. That being said, further discussion is required, backed by concrete implementations. Specifically, I think more emphasis should be placed on HTTP/2 (and HTTP/3) and union of the semantic model when HTTP/1 is also included in the mix. We welcome proposals for a better stream model for Rack 3.0 and we are currently considering how to make |
This suggested addition is the result of a collaboration between @boazsegev of Iodine fame and myself. Both Agoo and Iodine will support these suggested additions. At this point I don't think any other servers are supporting WebSockets and SSE so this would set the standard for others to follow.