diff options
Diffstat (limited to 'actionpack/lib/action_controller/metal/live.rb')
-rw-r--r-- | actionpack/lib/action_controller/metal/live.rb | 180 |
1 files changed, 84 insertions, 96 deletions
diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 58150cd9a9..fed99e6c82 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -1,9 +1,9 @@ -require 'action_dispatch/http/response' -require 'delegate' -require 'active_support/json' +require "action_dispatch/http/response" +require "delegate" +require "active_support/json" module ActionController - # Mix this module in to your controller, and all actions in that controller + # Mix this module into your controller, and all actions in that controller # will be able to stream data to the client as it's written. # # class MyController < ActionController::Base @@ -20,7 +20,7 @@ module ActionController # end # end # - # There are a few caveats with this use. You *cannot* write headers after the + # There are a few caveats with this module. You *cannot* write headers after the # response has been committed (Response#committed? will return truthy). # Calling +write+ or +close+ on the response stream will cause the response # object to be committed. Make sure all headers are set before calling write @@ -33,6 +33,20 @@ module ActionController # the main thread. Make sure your actions are thread safe, and this shouldn't # be a problem (don't share state across threads, etc). module Live + extend ActiveSupport::Concern + + module ClassMethods + def make_response!(request) + if request.get_header("HTTP_VERSION") == "HTTP/1.0" + super + else + Live::Response.new.tap do |res| + res.request = request + end + end + end + end + # This class provides the ability to write an SSE (Server Sent Event) # to an IO stream. The class is initialized with a stream and can be used # to either write a JSON string or an object which can be converted to JSON. @@ -70,7 +84,6 @@ module ActionController # Note: SSEs are not currently supported by IE. However, they are supported # by Chrome, Firefox, Opera, and Safari. class SSE - WHITELISTED_OPTIONS = %w( retry event id ) def initialize(stream, options = {}) @@ -131,8 +144,8 @@ module ActionController def write(string) unless @response.committed? - @response.headers["Cache-Control"] = "no-cache" - @response.headers.delete "Content-Length" + @response.set_header "Cache-Control", "no-cache" + @response.delete_header "Content-Length" end super @@ -149,14 +162,6 @@ module ActionController end end - def each - @response.sending! - while str = @buf.pop - yield str - end - @response.sent! - end - # Write a 'close' event to the buffer; the producer/writing thread # uses this to notify us that it's finished supplying content. # @@ -196,60 +201,36 @@ module ActionController def call_on_error @error_callback.call end - end - class Response < ActionDispatch::Response #:nodoc: all - class Header < DelegateClass(Hash) # :nodoc: - def initialize(response, header) - @response = response - super(header) - end + private - def []=(k,v) - if @response.committed? - raise ActionDispatch::IllegalStateError, 'header already sent' + def each_chunk(&block) + loop do + str = nil + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + str = @buf.pop + end + break unless str + yield str end - - super - end - - def merge(other) - self.class.new @response, __getobj__.merge(other) end + end - def to_hash - __getobj__.dup - end - end - + class Response < ActionDispatch::Response #:nodoc: all private - def before_committed - super - jar = request.cookie_jar - # The response can be committed multiple times - jar.write self unless committed? - end - - def before_sending - super - request.cookie_jar.commit! - headers.freeze - end - - def build_buffer(response, body) - buf = Live::Buffer.new response - body.each { |part| buf.write part } - buf - end - - def merge_default_headers(original, default) - Header.new self, super - end + def before_committed + super + jar = request.cookie_jar + # The response can be committed multiple times + jar.write self unless committed? + end - def handle_conditional_get! - super unless committed? - end + def build_buffer(response, body) + buf = Live::Buffer.new response + body.each { |part| buf.write part } + buf + end end def process(name) @@ -260,39 +241,55 @@ module ActionController # This processes the action in a child thread. It lets us return the # response code and headers back up the rack stack, and still process # the body in parallel with sending data to the client - Thread.new { - t2 = Thread.current - t2.abort_on_exception = true - - # Since we're processing the view in a different thread, copy the - # thread locals from the main thread to the child thread. :'( - locals.each { |k,v| t2[k] = v } - - begin - super(name) - rescue => e - if @_response.committed? - begin - @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html - @_response.stream.call_on_error - rescue => exception - log_error(exception) - ensure - log_error(e) - @_response.stream.close + new_controller_thread { + ActiveSupport::Dependencies.interlock.running do + t2 = Thread.current + + # Since we're processing the view in a different thread, copy the + # thread locals from the main thread to the child thread. :'( + locals.each { |k, v| t2[k] = v } + + begin + super(name) + rescue => e + if @_response.committed? + begin + @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html + @_response.stream.call_on_error + rescue => exception + log_error(exception) + ensure + log_error(e) + @_response.stream.close + end + else + error = e end - else - error = e + ensure + @_response.commit! end - ensure - @_response.commit! end } - @_response.await_commit + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @_response.await_commit + end + raise error if error end + # Spawn a new thread to serve up the controller in. This is to get + # around the fact that Rack isn't based around IOs and we need to use + # a thread to stream data from the response bodies. Nobody should call + # this method except in Rails internals. Seriously! + def new_controller_thread # :nodoc: + Thread.new { + t2 = Thread.current + t2.abort_on_exception = true + yield + } + end + def log_error(exception) logger = ActionController::Base.logger return unless logger @@ -309,14 +306,5 @@ module ActionController super response.close if response end - - def set_response!(request) - if request.env["HTTP_VERSION"] == "HTTP/1.0" - super - else - @_response = Live::Response.new - @_response.request = request - end - end end end |