ActionController::Live with SSE not working proper

2019-06-06 02:09发布

问题:

I'm trying to use Live Streaming in Rails 4.0.1 in one project but I see problems...

I have this action:

def realtime_push
    response.headers['Content-Type'] = 'text/event-stream'

    sse = SSE.new(response.stream)

    d = Domain.find(params[:domain_id])

    begin
      loop do
        backlinks = d.backlinks.page(params[:page]).per(10)
        pagination = render_to_string(:partial => 'backlinks/pagination', :layout => false, :locals => { :backlinks => backlinks })
        sse.write({ :html => pagination }, :event => 'pagination')
        sleep 1
      end
    rescue IOError
      # When the client disconnects, we'll get an IOError on write
      logger.debug "DISCONNECTED"
    ensure
      sse.close
    end
end

When I start Puma and try to get updates:

curl http://localhost:3000/domains/16/backlinks/realtime_push

curl immediately returns with no output.

Curl headers:

HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-UA-Compatible: chrome=1
Content-Type: text/event-stream
Cache-Control: no-cache
X-Request-Id: 1a07be2f-de8d-4ca8-87d0-eee2787ea649
X-Runtime: 0.250782
Transfer-Encoding: chunked

and Puma log shows:

Started GET "/domains/16/backlinks/realtime_push" for 127.0.0.1 at 2013-11-08 12:22:30 +0100
  ActiveRecord::SchemaMigration Load (0.7ms)  SELECT "schema_migrations".* FROM "schema_migrations"
Processing by BacklinksController#realtime_push as */*
  Parameters: {"domain_id"=>"16"}
  Domain Load (1.9ms)  SELECT "domains".* FROM "domains" WHERE "domains"."id" = $1 LIMIT 1  [["id", "16"]]
   (3.3ms)  SELECT COUNT(*) FROM "backlinks" WHERE "backlinks"."domain_id" = $1  [["domain_id", 16]]
  Rendered backlinks/_pagination.haml (60.6ms)
   (0.6ms)  SELECT COUNT(*) FROM "backlinks" WHERE "backlinks"."domain_id" = $1  [["domain_id", 16]]
  Rendered backlinks/_pagination.haml (36.0ms)
   (0.8ms)  SELECT COUNT(*) FROM "backlinks" WHERE "backlinks"."domain_id" = $1  [["domain_id", 16]]
  Rendered backlinks/_pagination.haml (37.5ms)
   (0.8ms)  SELECT COUNT(*) FROM "backlinks" WHERE "backlinks"."domain_id" = $1  [["domain_id", 16]]
  Rendered backlinks/_pagination.haml (35.6ms)
   (0.7ms)  SELECT COUNT(*) FROM "backlinks" WHERE "backlinks"."domain_id" = $1  [["domain_id", 16]]
  Rendered backlinks/_pagination.haml (38.7ms)
   (0.7ms)  SELECT COUNT(*) FROM "backlinks" WHERE "backlinks"."domain_id" = $1  [["domain_id", 16]]
  Rendered backlinks/_pagination.haml (37.0ms)

So these things are strange:

  • curl returned no output
  • log says it rendered pagination 6 times
  • there was no "DISCONNECT" message in the log

Any ideas? If I comment out the two lines above sse.write and return some text instead of pagination contents, it works...

Here is the SSE class:

class SSE
  def initialize io
    @io = io
  end

  def write object, options = {}
    options.each do |k,v|
      @io.write "#{k}: #{v}\n"
    end
    @io.write "data: #{JSON.dump(object)}\n\n"
  end

  def close
    @io.close
  end
end

回答1:

This is a bug in render_to_string.

Monkey patch to fix this (that actually doesn't fix the problem - see below):

def render_to_string(*)
  orig_stream = response.stream
  super
ensure
  if orig_stream
    response.instance_variable_set(:@stream, orig_stream)
  end
end

Source: http://blog.sorah.jp/2013/07/28/render_to_string-in-ac-live

UPDATE: this only appears to fix the problem... although it will cause the controller to actually send the data, the receiving end in JavaScript for some reason still won't get notified of events, see here: SSE (Server-sent events) not working