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?
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
Most helpful comment
You can create a middleware and just insert it before Rack, such as:
and then: