Skip to content
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

WebSocket #23

Closed
jodosha opened this issue Jun 25, 2014 · 18 comments
Closed

WebSocket #23

jodosha opened this issue Jun 25, 2014 · 18 comments
Assignees

Comments

@jodosha
Copy link
Member

jodosha commented Jun 25, 2014

WebSocket support! 💖

Example:

module Chat
  class Show
    include Lotus::Action
    include Lotus::Action::WebSocket

    def call(params)
      websocket.on_open do
        send_data "Hi"
      end

      websocket.on_message do |message|
        send_data "Echo: #{ message }"
      end
    end
  end
end

The corresponding client side code:

document.addEventListener("DOMContentLoaded", function(event) { 
  var socket       = new WebSocket("ws://localhost:2300/chat");

  socket.onmessage = function(message) {
    console.log(message);
    window.alert(message.data);
  };

  socket.send("Hello");
}
@mistersourcerer
Copy link

I've started a POC in this matters. I really hope to help with this part of the framework.
My super-experimental code can be found here, for now it's just as ugly as it is untested. But it's just a poc: https://github.com/ricardovaleriano/controller/blob/8ccce690763fd80a3141b00864882c1edbf1ce9d/lib/lotus/action/streaming.rb.

To see it working: https://www.youtube.com/watch?v=39-sssoUw_I

The current implementation only works with non evented servers. But I'll work to integrate this solution with the EventMachine land as well.

The api that I come up for now was:

require 'lotus/controller'
require 'lotus/action/streaming'
require 'rb-fsevent'

class SSEAction
  include Lotus::Action
  include Lotus::Action::Streaming

  def call(params)
    stream do |out|
      out.write "streaming from lotus!"

      directories = [ File.join(File.expand_path("../", __FILE__)) ]
      fsevent = FSEvent.new
      fsevent.watch(directories) { |dirs|
        out.write({ dirs: dirs }, event: "refresh")
      }
      fsevent.run
    end
  end
end

What do you guys think about it?

@lucasas
Copy link
Contributor

lucasas commented Jul 30, 2014

Hey @ricardovaleriano. I try to implement something using sinatra ideas here: https://github.com/lucasas/controller/blob/master/lib/lotus/action/streaming.rb

Your code solve other problems like keep connection open. I'll try to help you to figure out a way to implement this.

@mistersourcerer
Copy link

Hey @lucasas! This is the sinatra one right? I haven't worked too much around this code, it's true, but I haven't find a way yet to keep the connection open given the event loop nature. Probably I'm missing something that you have already figured out. I adapted this branch (https://github.com/ricardovaleriano/controller/tree/sse_sinatra_stream) to work with this implementation. If you can make the demo work there, it will be very helpful.

But I'd like to add that particularly I'll prefer a implementation that don't couple us with the event machine vocabulary, but this is just me being grumpy.

In the meantime, I'll be probably do something like this: https://github.com/intridea/rack-stream/blob/master/lib/rack/stream/deferrable_body.rb to get it working with the idea that I proposed in my POC branch. So we can add any transport implementation latter, even though the main one that I'm testing right now is the SSE one.

What do you think?

@lucasas
Copy link
Contributor

lucasas commented Jul 30, 2014

Yes @ricardovaleriano, my implementation is completely equals to sinatra, but it is using EventMachine interface, and is not a fancy implementation. I haven't figured out how to keep connection open, but I know that EventMachine has it built in. So servers like Thin or Rainbows which are implemented using EM, certainly, has a way to achieve that. My idea is figuring out that part.

Rack-Stream are using EM::Deferrable which is basic the same thing that EventMachine.defer, except defer uses a EM internal queue. But I agree with you, encapsulating this logic in another class decreases coupling from Stream with EventMachine.

@mistersourcerer
Copy link

Hey guys, I have some good news!

Just figured out the a way to get this working with Thin (keep in mind that the code still just a POC): https://github.com/ricardovaleriano/controller/blob/05b8d76e5e0e3e6abba4088293666e5991f76180/lib/lotus/action/streaming.rb.

The old implementation already works in puma, unicorn, rainbows, passenger... So, I think this is the way to go.

Now I'll work on:

  1. verify if my POC uses some code that is not in the oficial rack api (I'm looking into the throw :async line)
  2. refactor all the stuff with adequate tests

I'll keep you posted.

@jodosha
Copy link
Member Author

jodosha commented Jul 31, 2014

@ricardovaleriano @lucasas Thanks for taking care of this.

Before to dive into a code review, I'll let you first to get a stable and well tested solution. For now I have only a request: don't assume that EM will be available in applications.

If possible, we can have a default implementation, like the first that @ricardovaleriano posted here, and eventually inject some other code only if EM is detected.

What do you think?

@mistersourcerer
Copy link

@jodosha We are totally aligned.
It's exact what the current poc version do. I'll work to evolve this idea, and will open a PR when something more ready to production appears here.
Thank's!

@mistersourcerer
Copy link

Ok guys, I need your opinion before move forward on it.

TL;DR: Appears to me that if users decide to make a blocking call inside a #stream in a Lotus::Controller Action while using Thin, they need to tell this to Lotus::Controller explicitly. If not, Lotus::Controller will try to use the best way to do the stream using EM when inside Thin, or just threads otherwise.

Sorry for the verbose doubt that follow, but this is the best way I can explain it.

PS.: the current (draft) implementation is here:
https://github.com/ricardovaleriano/controller/blob/http_stream/lib/lotus/action/streaming.rb

and the usage example is here:
https://github.com/ricardovaleriano/controller/blob/http_stream/examples/sse.ru

Let's go!

I can see 2 main scenarios where one would like to use the streaming feature:

The first scenario:

  • The controller has a blocking code, and need to update clients when this blocking code yields some response (the classic example from tenderlove: tenderlovemaking.com/2012/07/30/is-it-live.html).

This example is not the "normal" usage for Event Machine, because the blocking call need to be done in a EM.defer block, to be executed in a EM internal thread. By default, AFAIK, EM starts with 20 threads in it's pool. So, the following example, would support only 20 simultaneous connections inside an evented server:

class Blocking
  require 'rb-fsevent'

  include Lotus::Action
  include Lotus::Action::Streaming

  def self.call(env)
    new.call(env)
  end

  def call(params)
    stream will_block: true do |out|
      out.write "streaming from lotus!"
      directories = [ File.join(File.expand_path("../", __FILE__)) ]
      fsevent = FSEvent.new
      out.write "you can push messages at any time..."
      fsevent.watch(directories) { |dirs|
        out.write({dirs: dirs }, event: "refresh")
      }
      fsevent.run
    end
  end
end

Given all that, my proposal in this case is, if the our users want to do this blocking call, even inside an evented server, they have to make it explicitly:

def call(params)
    stream will_block: true do |out|
      # code that will block here...
    end
end

The will_block: true option tells Lotus::Controller that the block should be executed in a EM Thread. The same code can be used in non-evented server, and the will_block option will be ignored. So the code above, is ready to run in evented or threaded servers.

The second main usage:

  • The controller has a non-blocking code, but need to keep the connection open. Mainly the chat example, right?

This second scenario can be done with or without EM, right? In my current proposal/implementation the non evented servers use the same mechanics (Threads) for this case and the first one. So let's see the specifics of EM.

The same logic in the Blocking action on the first example here, could be implemented with a EM aware gem, in this case, the listen gem. See the logic that matters to us implemented with the listen:

    stream do |out|
      out.write "streaming from lotus!"
      directories = [ File.join(File.expand_path("../", __FILE__)) ]
      out.write "you can push messages at any time..."
      listener = Listen.to(*directories) { |modified, added, moved|
        out.write({dirs: modified}, event: "refresh")
      }
      listener.start
    end

If the above code is inside a Lotus::Controller action, running on an evented server: the Lotus::Controller will just leave the connection open and execute the streaming without use a EM.defer call, but using EM.next_tick and such. This way, the user using this code on Thin is in a "pure" EM land. So, this works.

Do you think that I should go with this api implementation?
Sorry again for the lengthy doubt.

@jodosha jodosha changed the title Streaming WebSocket Jun 30, 2015
@jodosha jodosha self-assigned this Jun 30, 2015
@jodosha jodosha modified the milestone: v0.5.0 Jun 30, 2015
@xiy
Copy link

xiy commented Jul 5, 2015

What happened to @ricardovaleriano's SSE implementation in the end?

@jodosha
Copy link
Member Author

jodosha commented Jul 7, 2015

@xiy I guess he didn't had the time to experiment/deliver anymore. 😄

@jodosha jodosha removed this from the v0.5.0 milestone Sep 14, 2015
@jodosha
Copy link
Member Author

jodosha commented Sep 16, 2015

Sadly, we have to postpone this because the implementation based on Rack Hijiack and Puma doesn't scale. Closing this for now. 😢

@jodosha jodosha closed this as completed Sep 16, 2015
@tak1n
Copy link
Member

tak1n commented Feb 13, 2017

@jodosha maybe hanami can adopt https://github.com/palkan/litecable which provides different production variants (go server, erlang server) through https://github.com/anycable/anycable.

For development mode it also uses the rack hijack approach.

Hanami could also provide it's own logic handler if litecable does not fit: https://github.com/anycable/anycable/wiki/Using-AnyCable-with-Ruby-(non-Rails)#custom-backend-framework

@jodosha
Copy link
Member Author

jodosha commented Feb 13, 2017

@tak1n Thanks for the heads up.

These are great solutions that work out of the box. 👍

@boazsegev
Copy link

I love the idea of using litecable, even though it requires the ActionCable client (javascript).

As for raw WebSockets, I invite you to help refine the WebSocket Rack Extension Draft in the iodine server branch.

I'm the author for the iodine server and I'm working on redesigning the pub/sub layer for the upcoming 0.5.0 release, so it's a good time to incorporate any feature requests.

Iodine's approach to WebSocket upgrade is very effective. It also minimizes object creation (no need for multiple Proc objects per connection).

@jodosha
Copy link
Member Author

jodosha commented Aug 7, 2018

@boazsegev Thanks for joining in this discussion. I've read the SPEC and LGTM. I imagine that Iodine already implements it. Did you discuss with other Ruby Rack server people and/or Rack folks to gather feedback and try to understand if they want to adopt your proposal?

Also, the advantage of Litecable is to have an unified experience both on the client and server side. What's the usual client side library that you suggest to use with Iodine. I know it can be anything, but that do you recommend? Thanks.

@boazsegev
Copy link

@jodosha, thank you for allowing me to join the discussion.

I imagine that Iodine already implements it.

Yes, iodine was implementing variations on these specifications for a number of years and helped gather the experience that tested the API's design.

The Agoo server also implements the specifications, though the more established servers (such as Puma and Passenger) are still considering the specification.

Did you discuss with other Ruby Rack server people and/or Rack folks to gather feedback and try to understand if they want to adopt your proposal?

There were a number of attempts to solidify this specification and the latest discussion can be reviewed in the Rack PR#1272 thread.

The discussion was locked for being "too heated", but the general feeling was positive.

Evan Phoenix (Puma) was generally positive about the suggestion, but had some reservations that were addressed by changes to the proposal.

I also authored a Rails PR to include support for the specifications ActiveCable to follow up with the summery of the discussion.

What's the usual client side library that you suggest to use with Iodine. I know it can be anything, but that do you recommend? Thanks.

I'm partial to raw WebSockets and custom authored solutions. I find that many libraries mutate too often with time and become harder to adjust as projects grow.

Also, the basic JavaScript API is easy enough and routing JSON messages and events is often a breeze. The only thing to watch is probably the reconnection concerns.

Having said that, the interface for LiteCable and ActionCable appears to have a stable design that works. I'm not in love with the implementation for ActionCable (to say the least), so I would probably go with LiteCable if I had to choose between the two.

@jodosha
Copy link
Member Author

jodosha commented Aug 9, 2018

@boazsegev

I would probably go with LiteCable if I had to choose between the two.

Do you mean that a) you'd pick LiteCable as a user that wants to build WS with Iodine or b) Iodine will work out of the box with LiteCable JavaScript code?

Sorry if the question is silly, but I'm not familiar with Iodine. Thanks. 👍

@boazsegev
Copy link

@jodosha

I meant that I'd pick LiteCable as a user that wants to build WS with Iodine.

Iodine doesn't enforce any WebSocket sub-protocol on the user. It supports raw WebSockets to allow for maximum flexibility and separation of concerns.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants
@jodosha @xiy @lucasas @mistersourcerer @tak1n @boazsegev and others