0

I've been looking for ways of triggering server-side events with ActionController::Live without the use of polling. It seems like the only documented pub/sub solution for Live is to use Redis, but I'd really like to just use the stack I currently have. I came across ActiveSupport::Notifications, which seems to do just what I want.

However, when I use it nearly in-place of Redis, Live throws an IOError and the stream gets closed. I don't see any valid reason as to why this would happen.

For example, this supposedly works:

def stream
    response.headers['Content-Type'] = 'text/event-stream'
    redis = Redis.new
    redis.subscribe('namespaced:stream') do |on|
        response.stream.write "hello world\n"
    end
    rescue IOError
        # Client Disconnected
    ensure
        response.stream.close
end

But when I try this I get an IO Error:

def stream
    response.headers['Content-Type'] = 'text/event-stream'
    ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
        response.stream.write "hello world\n"
    end
    rescue IOError
        # Client Disconnected
    ensure
        response.stream.close
end

I'm really at a loss as to why this is happening. Nothing in the server log indicates an actual problem, and the notification subscription works otherwise; I can tell because even if I get an IO Error I can still get the subscription to puts a string to the console when a "process_action.action_controller" notification occurs.

When the notification doesn't occur, the connection works just fine but as soon as there is a notification(which I'd think should just write "hello world" to the stream), I get that silly IO Error.

By the way, I do plan on instrumenting my own notifications; just testing with existing notifications is easier.

Oh, and here's a copy of what happens in the terminal when I visit the route that uses the stream controller method:

IOError (closed stream):
  /usr/local/lib/ruby/gems/2.0.0/gems/actionpack-4.0.2/lib/action_dispatch/http/response.rb:76:in `write'
  /usr/local/lib/ruby/gems/2.0.0/gems/actionpack-4.0.2/lib/action_controller/metal/live.rb:47:in `write'
  /usr/local/lib/ruby/gems/2.0.0/gems/actionpack-4.0.2/lib/action_controller/metal/live.rb:135:in `rescue in block in process'
  /usr/local/lib/ruby/gems/2.0.0/gems/actionpack-4.0.2/lib/action_controller/metal/live.rb:145:in `block in process'



IOError (closed stream):
  /usr/local/lib/ruby/gems/2.0.0/gems/actionpack-4.0.2/lib/action_dispatch/http/response.rb:76:in `write'
  /usr/local/lib/ruby/gems/2.0.0/gems/actionpack-4.0.2/lib/action_controller/metal/live.rb:47:in `write'
  /home/benjamin/Desktop/workspace/fremote/app/controllers/remotes_controller.rb:25:in `block in show'
  /usr/local/lib/ruby/gems/2.0.0/gems/activesupport-4.0.2/lib/active_support/notifications/fanout.rb:125:in `call'
  /usr/local/lib/ruby/gems/2.0.0/gems/activesupport-4.0.2/lib/active_support/notifications/fanout.rb:125:in `finish'
  /usr/local/lib/ruby/gems/2.0.0/gems/activesupport-4.0.2/lib/active_support/notifications/fanout.rb:40:in `block in finish'
  /usr/local/lib/ruby/gems/2.0.0/gems/activesupport-4.0.2/lib/active_support/notifications/fanout.rb:40:in `each'
  /usr/local/lib/ruby/gems/2.0.0/gems/activesupport-4.0.2/lib/active_support/notifications/fanout.rb:40:in `finish'
  /usr/local/lib/ruby/gems/2.0.0/gems/activesupport-4.0.2/lib/active_support/notifications/instrumenter.rb:36:in `finish'
  /usr/local/lib/ruby/gems/2.0.0/gems/activesupport-4.0.2/lib/active_support/notifications/instrumenter.rb:25:in `instrument'
  /usr/local/lib/ruby/gems/2.0.0/gems/activesupport-4.0.2/lib/active_support/notifications.rb:159:in `instrument'
  /usr/local/lib/ruby/gems/2.0.0/gems/actionpack-4.0.2/lib/action_controller/metal/instrumentation.rb:30:in `process_action'
  /usr/local/lib/ruby/gems/2.0.0/gems/actionpack-4.0.2/lib/action_controller/metal/params_wrapper.rb:245:in `process_action'
  /usr/local/lib/ruby/gems/2.0.0/gems/actionpack-4.0.2/lib/abstract_controller/base.rb:136:in `process'
  /usr/local/lib/ruby/gems/2.0.0/gems/actionpack-4.0.2/lib/abstract_controller/rendering.rb:44:in `process'
  /usr/local/lib/ruby/gems/2.0.0/gems/actionpack-4.0.2/lib/action_controller/metal/live.rb:132:in `block in process'

I apologize if I am getting any terminology wrong, for I am new to SSEs and Rails in general, so I would greatly appreciate any help with this.

EDIT: In case you need to know, I am using the following:

  • Rails 4.0.2
  • Ruby 2.0.0p353 (2013-11-22 revision 43784) [i686-linux]
Ten Bitcomb
  • 2,316
  • 1
  • 25
  • 39

1 Answers1

0

here's a good article on this topic: http://37signals.com/svn/posts/3091-pssst-your-rails-application-has-a-secret-to-tell-you

Well are you sure Redis and the ActiveSupport::Notifications have the same subscribe method?

So this might be what's causing the problem in rails:

  def close
    @response.commit!
    @closed = true
  end


  def write(string)
    raise IOError, "closed stream" if closed?

    @response.commit!
    @buf.push string
  end

You're ensuring that the stream is closed. When write it called, it throughs an IOError if closed?, which you're ensuring happens. I'm not 100% sure everything going on, but this seems like the most likely culprit.

SO I went back to look a some things:

in the initialize of Response we have:

  self.body, self.header, self.status = body, header, status

Here is body=

def body=(body)
  @blank = true if body == EMPTY

  if body.respond_to?(:to_path)
    @stream = body
  else
    synchronize do
      @stream = build_buffer self, munge_body_object(body)
    end
  end
end

which sets @stream = to a new Buffer

def build_buffer(response, body)
  Buffer.new response, body
end

So I think your culprit is actually you ensuring response.stream.close. Now, how it's being called before write is being called, I'm not sure. Are you doing the Redis first, then testing the Live write and are they using the same response? If so, you might be setting the response to be closed and then trying to use it to write, which will through the IOerror.

TalkativeTree
  • 609
  • 5
  • 10
  • Thank you very much for your response. I highly appreciate it. I think what you are talking about might have some part in why I was having an issue. But in response to your question, I've discovered that the subscribe method for Notifications behaves a bit differently than that of Redis. Again, I haven't used Redis, but in the videos I've seen on it the subscribe method seems to act like a loop whereas Notifications just subscribes and continues. By adding a sleep command under the ActiveSupport subscription, I was able to get "hello world" to send. I plan on posting my solution soon. – Ten Bitcomb Jan 31 '14 at 20:31
  • I'm never a fan of using sleep as a solution. Are you using a threaded server like thin or puma? If you're using Unicorn, you'll want to switch to one of those. The comments have some good explanations: http://ngauthier.com/2013/02/rails-4-sse-notify-listen.html – TalkativeTree Jan 31 '14 at 22:13
  • Yes, I am using Puma. I've actually gotten my solution to work so long as the sleep is timed rather than indefinite in order to allow old threads to die. It's not great(still looking for a better way to kill threads), but I don't see anything particularly bad about using sleep since Redis does basically the same thing(blocking until the subscription is triggered then continuously blocking). I have considered the solution you posted only I am using MongoDB. It may have a similar feature, though I'd prefer something database-agnostic. The comments still have some decent information, however. – Ten Bitcomb Feb 01 '14 at 10:54