Created
February 21, 2013 22:31
-
-
Save oggy/5008990 to your computer and use it in GitHub Desktop.
Demonstrates the superiority of Goliath over Unicorn for a single-process app handling file uploads. Also a fine excuse to use gems named Unicorn, Thor, and Goliath in a single ruby script.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# Demonstrates the superiority of Goliath over Unicorn for a single-process app | |
# handling file uploads. | |
# | |
# This program runs in 3 modes: | |
# | |
# ./goliath-vs-unicorn goliath - Runs the goliath server on the default port (9000) | |
# ./goliath-vs-unicorn unicorn - Runs the unicorn server on the default port (8080) | |
# ./goliath-vs-unicorn upload - Upload 5 files in parallel to the port given by -p | |
# | |
# The goliath and unicorn servers run an app that simply write the uploaded file | |
# to disk in the current directory. | |
# | |
# The upload mode will upload the 5 files in parallel threads, in 10 1MB chunks | |
# with 1 second sleeps between each chunk. This simulates multiple clients | |
# performing file uploads to a web server in parallel. To view progress on the | |
# client-side, a dot is printed after each chunk is written to the socket, and | |
# the HTTP response is printed when each request is complete. | |
# | |
# When running against Unicorn, the requests are seen to become serialized in | |
# the request queue, and the whole process takes around 5x longer. Goliath | |
# handles all 5 requests in parallel due to its evented architecture. | |
require 'net/http' | |
require 'goliath/api' | |
require 'goliath/runner' | |
require 'unicorn' | |
require 'thor' | |
class App < Thor | |
NUM_FILES = 5 | |
CHUNKS = 10 | |
desc 'goliath', 'Run the goliath server' | |
def goliath | |
api = Api.new | |
runner = Goliath::Runner.new([], api) | |
runner.app = Goliath::Rack::Builder.build(Api, api) | |
runner.run | |
end | |
desc 'unicorn', 'Run the unicorn server' | |
def unicorn | |
app = Api.new | |
Unicorn::HttpServer.new(app).start.join | |
end | |
desc 'upload PORT', 'Upload files' | |
def upload(port) | |
posters = (0...NUM_FILES).map do |i| | |
Thread.new { post_file(i, port) } | |
end | |
posters.each(&:join) | |
end | |
private | |
def post_file(i, port) | |
res = Net::HTTP.start('localhost', port) do |http| | |
path = self.class.output_path(i) | |
request = Net::HTTP::Post.new("/#{i}") | |
request['Content-length'] = 10*(2**20) | |
request.body_stream = SlowStream.new(CHUNKS) { print '.'; $>.flush } | |
http.request(request) | |
end | |
p res | |
end | |
def self.output_path(i) | |
'out.%d.txt' % i | |
end | |
end | |
class SlowStream | |
def initialize(chunks=chunks, delay=1, &callback) | |
@chunks = chunks | |
@delay = delay | |
@callback = callback | |
@chunks_remaining = chunks | |
end | |
def read(size) | |
return nil if @chunks_remaining == 0 | |
sleep @delay | |
@callback.call | |
@chunks_remaining -= 1 | |
'.'*(2**20) | |
end | |
end | |
class Api < Goliath::API | |
def response(env) | |
accept_file | |
[200, {}, 'hi'] | |
end | |
def call(env) | |
accept_file(env) | |
[200, {'Content-length' => '2', 'Content-type' => 'text/plain'}, ['hi']] | |
end | |
def accept_file(env) | |
i = env['PATH_INFO'][/\d+/].to_i | |
File.open(App.output_path(i), "w") do |file| | |
input = env['rack.input'] | |
input.rewind | |
while (chunk = input.read(100)) | |
file.write(chunk) | |
end | |
end | |
end | |
end | |
App.start |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment