Rack: Large file upload consumes 100% cpu for a very long time

Created on 24 May 2016  路  15Comments  路  Source: rack/rack

Rack 2.0.0.rc1 with Rails 5.0.0.rc1 and Puma 3.4.0

The problem is that there is a loop in Rack::Multipart::Parser.parse (https://github.com/rack/rack/blob/master/lib/rack/multipart/parser.rb#L70). On each iteration it reads at most Rack::Multipart::Parser::BUFSIZE bytes which is 16384. But for the 200Mb file this means 12000 iterations and this is very slow.

My dirty hack:

# config/initializers/rack.rb
Rack::Multipart::Parser.const_set('BUFSIZE', 10_000_000)

Maybe there should be a config option to override default Rack::Multipart::Parser::BUFSIZE. Or maybe a better solution?

Most helpful comment

You can create a middleware and just insert it before Rack, such as:

class MultipartBufferSetter

  def initialize(app)
    @app = app
  end

  def call(env)
    env.merge!(Rack::RACK_MULTIPART_BUFFER_SIZE => 100*1024*1024)
    @app.call(env)
  end

end

and then:

config.middleware.insert_before "Rack::Runtime", "MultipartBufferSetter"

All 15 comments

Since no one has commented, you can pass header 'rack.multipart.buffer_size' with your response in order to specify the size of buffer instead of doing such workarounds.

Thank you @miroslavLalev !

@miroslavLalev, @olegantonyan Can you please explain how to do it? Where to put it?

@vizo I didn't succeed to send the header, I just did the dirty hack of @olegantonyan and it's finally working fine.

Actually, i didn't work for me either, and I couldn't find why in rack's source code. I've just implemented clunked upload, so this option no longer required.

Any news with this issue?

@mahnunchik The "dirty" (but very useful) hack of @olegantonyan is working great. I just have a warning when starting the server.

Chunked upload is a good alternative (easy to do with https://github.com/tors/jquery-fileupload-rails ) but if you provide a public API, users will also have to implement the chunked upload. (lost my hairs trying to do it with cURL)

So that's why personnaly I go with the dirty hack.

You can create a middleware and just insert it before Rack, such as:

class MultipartBufferSetter

  def initialize(app)
    @app = app
  end

  def call(env)
    env.merge!(Rack::RACK_MULTIPART_BUFFER_SIZE => 100*1024*1024)
    @app.call(env)
  end

end

and then:

config.middleware.insert_before "Rack::Runtime", "MultipartBufferSetter"

@darfux Thanks, it works! ;)

You need to set rack.multipart.buffer_size to your Rack env before Rack::Request#params is called for the first time (or just #params in Rails), because that's when parsing the multipart body happens. I tested that the following script correctly sets the buffer size:

gem "rack-test_app"
require "rack/test_app"

app = proc do |env|
  request = Rack::Request.new(env)
  request.env["rack.multipart.buffer_size"] = 100
  request.params

  [200, {}, []]
end

test_app = Rack::TestApp.wrap(app)
test_app.post("/", multipart: {file: File.open(__FILE__)})

Yeah, finded it!!! Ty!

If anyone's interested in conclusively addressing this, a CPU+memory usage benchmark for different buffer sizes (16kB, 1MB, 4MB, 16MB, etc) and file sizes (1kB, 1MB, 10MB, 100MB) would clearly indicate a sweet-spot buffer size.

Silly to have everyone needing to figure out a workaround themselves or insert this awkward runtime middleware. Please do investigate 馃槉

It looks like #1179 fixes the performance regression introduced in Rack 2, but dialing in the buffer size is probably still a good idea.

@jeremy I've posted some benchmarks to #1192 if you get some time to take a look.

Fixed by #1179

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ziggythehamster picture ziggythehamster  路  9Comments

mwpastore picture mwpastore  路  3Comments

leahneukirchen picture leahneukirchen  路  14Comments

ioquatix picture ioquatix  路  5Comments

dmitry picture dmitry  路  6Comments