10000 Rails 4, streaming blocked by `@body.respond_to?(:body_parts)` · Issue #210 · jruby/jruby-rack · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content
Rails 4, streaming blocked by @body.respond_to?(:body_parts) #210
Closed
@kevinmtrowbridge

Description

@kevinmtrowbridge

In the response.rb#write_body method, there's this line (in the if / elseif block):

elsif @body.respond_to?(:body_parts)

This was added by this commit:
7b87c78

... with the commit message:

with Rails 3.x ... thus check for body_parts on the body object

... we use raw data streaming (via passing an enumerator to reponse_body) with jruby-rack & Jetty. The streaming worked fine with Rails 3. About a year ago we upgraded to Rails 4. This introduced a subtle bug in that, the streaming no longer worked, but it did not fail outright, what happened is that it fell back to behavior where it would buffer the entire response body in memory, then send it to the client when done.

In many cases this was ok, however, we're streaming the results of an SQL query to the client, so it could result in arbitrarily huge files. In some cases, it was essentially behaving as a massive memory leak which could crash the application server.

I dug into it and found eventually that the line @body.respond_to?(:body_parts) blocks the write_body method. body_parts method in Rails 4 looks like so:

def body_parts
  parts = []
  @stream.each { |x| parts << x }
  parts
end

I can only assume that the body_parts method hangs out there till the entire response body has been streamed, blocking the flushing of the response. If I comment out that block, ie, make it look like this:

      def write_body(response)
        body = nil
        begin
          if @body.respond_to?(:call) && ! @body.respond_to?(:each)
            @body.call response.getOutputStream
          elsif @body.respond_to?(:to_path) # send_file
            send_file @body.to_path, response
          elsif @body.respond_to?(:to_channel) &&
              ! object_polluted_with_anyio?(@body, :to_channel)
            body = @body.to_channel # so that we close the channel
            transfer_channel body, response.getOutputStream
          elsif @body.respond_to?(:to_inputstream) &&
              ! object_polluted_with_anyio?(@body, :to_inputstream)
            body = @body.to_inputstream # so that we close the stream
            body = Channels.newChannel(body) # closing the channel closes the stream
            transfer_channel body, response.getOutputStream
          # elsif @body.respond_to?(:body_parts) && @body.body_parts.respond_to?(:to_channel) &&
          #     ! object_polluted_with_anyio?(@body.body_parts, :to_channel)
          #   # ActionDispatch::Response "raw" body access in case it's a File
          #   body = @body.body_parts.to_channel # so that we close the channel
          #   transfer_channel body, response.getOutputStream
          else
            if dechunk?
              write_body_dechunked response.getOutputStream
            else
              output_stream = response.getOutputStream
              # 1.8 has a String#each method but 1.9 does not :
              method = @body.respond_to?(:each_line) ? :each_line : :each
              @body.send(method) do |line|
                output_stream.write(line.to_java_bytes)
                output_stream.flush if flush?
              end
            end
          end
        rescue LocalJumpError
          # HACK: deal with objects that don't comply with Rack specification
          @body = [ @body.to_s ]
          retry
        rescue java.io.IOException => e
          raise e if ! client_abort_exception?(e) || ! self.class.swallow_client_abort?
        ensure
          @body.close if @body.respond_to?(:close)
          body && body.close rescue nil
        end
      end

... as I said if I comment out that part, the streaming starts working again.

The commit message mentioned this fix being for Rails 3, so I am hoping this part is not necessary for Rails 4. I will report back if our QA process finds any issues with this change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0