diff options
Diffstat (limited to 'actionpack/lib/action_controller/metal/streaming.rb')
-rw-r--r-- | actionpack/lib/action_controller/metal/streaming.rb | 223 |
1 files changed, 223 insertions, 0 deletions
diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb new file mode 100644 index 0000000000..8dc01a5eb9 --- /dev/null +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -0,0 +1,223 @@ +# frozen_string_literal: true + +require "rack/chunked" + +module ActionController #:nodoc: + # Allows views to be streamed back to the client as they are rendered. + # + # By default, Rails renders views by first rendering the template + # and then the layout. The response is sent to the client after the whole + # template is rendered, all queries are made, and the layout is processed. + # + # Streaming inverts the rendering flow by rendering the layout first and + # streaming each part of the layout as they are processed. This allows the + # header of the HTML (which is usually in the layout) to be streamed back + # to client very quickly, allowing JavaScripts and stylesheets to be loaded + # earlier than usual. + # + # This approach was introduced in Rails 3.1 and is still improving. Several + # Rack middlewares may not work and you need to be careful when streaming. + # Those points are going to be addressed soon. + # + # In order to use streaming, you will need to use a Ruby version that + # supports fibers (fibers are supported since version 1.9.2 of the main + # Ruby implementation). + # + # Streaming can be added to a given template easily, all you need to do is + # to pass the :stream option. + # + # class PostsController + # def index + # @posts = Post.all + # render stream: true + # end + # end + # + # == When to use streaming + # + # Streaming may be considered to be overkill for lightweight actions like + # +new+ or +edit+. The real benefit of streaming is on expensive actions + # that, for example, do a lot of queries on the database. + # + # In such actions, you want to delay queries execution as much as you can. + # For example, imagine the following +dashboard+ action: + # + # def dashboard + # @posts = Post.all + # @pages = Page.all + # @articles = Article.all + # end + # + # Most of the queries here are happening in the controller. In order to benefit + # from streaming you would want to rewrite it as: + # + # def dashboard + # # Allow lazy execution of the queries + # @posts = Post.all + # @pages = Page.all + # @articles = Article.all + # render stream: true + # end + # + # Notice that :stream only works with templates. Rendering :json + # or :xml with :stream won't work. + # + # == Communication between layout and template + # + # When streaming, rendering happens top-down instead of inside-out. + # Rails starts with the layout, and the template is rendered later, + # when its +yield+ is reached. + # + # This means that, if your application currently relies on instance + # variables set in the template to be used in the layout, they won't + # work once you move to streaming. The proper way to communicate + # between layout and template, regardless of whether you use streaming + # or not, is by using +content_for+, +provide+ and +yield+. + # + # Take a simple example where the layout expects the template to tell + # which title to use: + # + # <html> + # <head><title><%= yield :title %></title></head> + # <body><%= yield %></body> + # </html> + # + # You would use +content_for+ in your template to specify the title: + # + # <%= content_for :title, "Main" %> + # Hello + # + # And the final result would be: + # + # <html> + # <head><title>Main</title></head> + # <body>Hello</body> + # </html> + # + # However, if +content_for+ is called several times, the final result + # would have all calls concatenated. For instance, if we have the following + # template: + # + # <%= content_for :title, "Main" %> + # Hello + # <%= content_for :title, " page" %> + # + # The final result would be: + # + # <html> + # <head><title>Main page</title></head> + # <body>Hello</body> + # </html> + # + # This means that, if you have <code>yield :title</code> in your layout + # and you want to use streaming, you would have to render the whole template + # (and eventually trigger all queries) before streaming the title and all + # assets, which kills the purpose of streaming. For this purpose, you can use + # a helper called +provide+ that does the same as +content_for+ but tells the + # layout to stop searching for other entries and continue rendering. + # + # For instance, the template above using +provide+ would be: + # + # <%= provide :title, "Main" %> + # Hello + # <%= content_for :title, " page" %> + # + # Giving: + # + # <html> + # <head><title>Main</title></head> + # <body>Hello</body> + # </html> + # + # That said, when streaming, you need to properly check your templates + # and choose when to use +provide+ and +content_for+. + # + # == Headers, cookies, session and flash + # + # When streaming, the HTTP headers are sent to the client right before + # it renders the first line. This means that, modifying headers, cookies, + # session or flash after the template starts rendering will not propagate + # to the client. + # + # == Middlewares + # + # Middlewares that need to manipulate the body won't work with streaming. + # You should disable those middlewares whenever streaming in development + # or production. For instance, <tt>Rack::Bug</tt> won't work when streaming as it + # needs to inject contents in the HTML body. + # + # Also <tt>Rack::Cache</tt> won't work with streaming as it does not support + # streaming bodies yet. Whenever streaming Cache-Control is automatically + # set to "no-cache". + # + # == Errors + # + # When it comes to streaming, exceptions get a bit more complicated. This + # happens because part of the template was already rendered and streamed to + # the client, making it impossible to render a whole exception page. + # + # Currently, when an exception happens in development or production, Rails + # will automatically stream to the client: + # + # "><script>window.location = "/500.html"</script></html> + # + # The first two characters (">) are required in case the exception happens + # while rendering attributes for a given tag. You can check the real cause + # for the exception in your logger. + # + # == Web server support + # + # Not all web servers support streaming out-of-the-box. You need to check + # the instructions for each of them. + # + # ==== Unicorn + # + # Unicorn supports streaming but it needs to be configured. For this, you + # need to create a config file as follow: + # + # # unicorn.config.rb + # listen 3000, tcp_nopush: false + # + # And use it on initialization: + # + # unicorn_rails --config-file unicorn.config.rb + # + # You may also want to configure other parameters like <tt>:tcp_nodelay</tt>. + # Please check its documentation for more information: https://bogomips.org/unicorn/Unicorn/Configurator.html#method-i-listen + # + # If you are using Unicorn with NGINX, you may need to tweak NGINX. + # Streaming should work out of the box on Rainbows. + # + # ==== Passenger + # + # To be described. + # + module Streaming + extend ActiveSupport::Concern + + private + + # Set proper cache control and transfer encoding when streaming + def _process_options(options) + super + if options[:stream] + if request.version == "HTTP/1.0" + options.delete(:stream) + else + headers["Cache-Control"] ||= "no-cache" + headers["Transfer-Encoding"] = "chunked" + headers.delete("Content-Length") + end + end + end + + # Call render_body if we are streaming instead of usual +render+. + def _render_template(options) + if options.delete(:stream) + Rack::Chunked::Body.new view_renderer.render_body(view_context, options) + else + super + end + end + end +end |