1

I am experimenting with Rails 6 for an API use case. Users would post large requests which rails would receive and stream to another place. This would probably run with JRuby, but right now I am testing with MRI Ruby. The webserver is Puma (out of the box rails install).

Is there any way to read the request body as a stream as it is uploaded, or does Rails need to wait for the entire request to upload before it can access it?

Using this Curl command:

curl  -i -X PUT -T data.bin "http://localhost:3000/welcome/stream"

And this simple controller:

  def stream
    puts "The stream was called"

    io = request.body
    until io.eof?
      io.read(1024*1024)
      puts "Read 1MB"
    end

    head 200, content_type: "text/html"
  end

It seems as though the entire request is staged somewhere before rails get access to it, as there is a long delay before any of my puts appear in the console.

Searching a bit, it seems it may be the websever (Puma, Unicorn, etc) that buffers the data and it only invokes Rails when all the data has been received.

Is there anyway to start reading the stream as it comes in and or avoid buffering it?

Is it Rack or Puma which does the buffering? If so, where does it buffer the data?

Stephen ODonnell
  • 4,441
  • 17
  • 19
  • Mind sharing the repo? – DNNX Nov 12 '20 at 12:02
  • Found where it does the buffering: https://github.com/puma/puma/blob/master/lib/puma/server.rb#L501-L502. `io` variable in your example has the type of `Tempfile`. If the request body is greater than 112KB, the body is downloaded to the tempfile first as a whole first, and only then it's passed on to the Rails controller. As I understood, dumps the file to Tempfile in a streaming fashion, not consuming any RAM. As for the other part of your question - how to avoid this buffering - I do not have an answer yet. – DNNX Nov 12 '20 at 12:29
  • Well one option would be to monkey-patch that MAX_BODY constant from puma, but it may break things badly. – DNNX Nov 12 '20 at 12:34
  • Nothing was broken after I changed MAX_BODY to a way bigger constant, but it's still buffering the body, this time in memory. – DNNX Nov 12 '20 at 12:41
  • May be relevant (although the post is very old): https://stackoverflow.com/questions/4795205/streaming-web-uploads-to-socket-with-rack?rq=1. – DNNX Nov 12 '20 at 13:37
  • @DNNX There is no repo for my app, its just a new rails app with that stream method for POC. Thanks for the pointers. Its a shame Rack / Rails does not seem to support this. I'm looking at a max data size of about 5GB, so to stream all that to disk before my app can stream it onwards is a pain. I also found https://groups.google.com/g/rack-devel/c/T5YE-aFzSIQ/m/2HSEoK8lCGAJ talking about unicorn, and it mentioned Nginx would still buffer the stream before sending it onto unicorn. – Stephen ODonnell Nov 13 '20 at 10:58

1 Answers1

1

Is it Rack or Puma which does the buffering? If so, where does it buffer the data?

The server collects the whole stream before passing it onwards to Rack / Rails... so you won't be able to start handling the request before the body completely uploads.

There's a good reason for this design, as the server will use the available threads to poll the IO instead of blocking, allowing your application to process other requests while performing the IO operations in the background.

Otherwise the IO would block the thread (and, by extension, the server) when your application called read on an incomplete stream, causing concurrency to falter.

Is there anyway to start reading the stream as it comes in and or avoid buffering it?

You can either implement your own server (not recommended), or upload bits and pieces using AJAX / WebSockets (a more common approach).

Myst
  • 18,516
  • 2
  • 45
  • 67