Tuesday, February 22, 2011

Rails requests not timing out in time?

The web server CPU was mostly idle, the 5 thin instances didn't seem to be doing much, but the requests were still taking forever to return anything. So what the flimmin' flip was taking so long?

Nginx was configured to timeout requests after 60s, but it didn't seem to be working. In fact, the key to figuring out what was going on, was that while tailing the logs, I saw an occasional query come through with a ridiculously long execution time - 400 seconds or more... Yowch!

So, if requests were supposed to time out after 60 seconds, how come a query was allowed to stay executing for 400 or more?

It all comes down to threads. We're still on CRuby 1.8.7 (MRI) in production - we'll move to 1.9 and JRuby at some point, but previous experience has made me careful to test thoroughly, test some more, then test again and again and again before making the move, and we're not quite ready to shift yet.

Now, the default (MRI) CRuby interpreter uses green threads, and the eventmachine gem underlying Thin server uses - like most Ruby libraries - timeout.rb for its timeout mechanism. This article gives the full details, but it boils down to the fact that:
it is a well-known limitations of green threads that when a green thread performs a blocking system call to the underlying operating systems, none of the green threads in the virtual machine will run until the system call returns ... From the operating system perspective, there is only a single thread in the Ruby interpreter (the native one)

So essentially, the monitor thread which is responsible for timing out the request has to wait for the blocking IO call to return, before it can resume. This means that when you have a long-running, block IO call (say, for instance, an ActiveRecord query that takes 400 seconds to run) running under MRI, the default timeout mechanism is useless.

The solution? If you can't just switch to JRuby, then here's a simple 5-minute fix that did the trick for us:

in environment.rb :


config.gem 'SystemTimer', :lib => 'system_timer'


in application_controller.rb :

  around_filter :force_timeout

  def force_timeout( timeout=nil, &block )
    timeout ||= 60 # <= or whatever value is appropriate
    if defined?(SystemTimer) && timeout.to_i > 0
      begin
        SystemTimer.timeout_after(timeout) do
          yield
        end
      rescue Timeout::Error => e
        logger.error( "Timed out request after #{timeout}s!")
        raise "RequestTimeout" 
      end
    else
      yield
    end
  end

Friday, July 02, 2010

"cannot redeclare exchange" error on amqp and RabbitMQ 1.8 - fixed! (at last)

I've been having some trouble with an inherited MacBook Pro, on Leopard (OS X 10.5), trying to get RabbitMQ and the tmm1-amqp gem up and running. The annoying thing is that it was all working fine a few weeks ago, but I ended up having to completely remove MacPorts and do several sudo rm -rf wipeouts and pretty much rebuild the dev environment from scratch. Since then, I re-installed MacPorts and did:
sudo port install rabbitmq-server

After a bit of fiddling about, I got this error (using exactly the same code that worked a few weeks ago):

vendor/gems/tmm1-amqp-0.6.4/lib/mq.rb:225:in `process_frame': PRECONDITION_FAILED - cannot redeclare exchange 'jobs' in vhost 'trigganews.com' with different type, durable or autodelete value in AMQP::Protocol::Exchange::Declare on 1 (MQ::Error)


....several hours of frustrating googling later.....

It turns out that I'd innocently upgraded my version of RabbitMQ 1.8, whereas previously I'd had v1.7.2. This is significant because RMQ 1.8 uses AMQP 0.9, whereas v1.7.2 uses AMQP 0.8 - and this matters because I'm using the tmm1-amqp gem to connect to it, and tmm1-amqp v0.6.4 only supports AMQP 0.8...

(HINT - check the files under the protocol directory to see what version your gem supports)

So - time to downgrade RabbitMQ to 1.7.2 then. Should be straightforward, no? Well, no.

It also turns out that the version of Erlang in MacPorts is now R14A - and guess what? RabbitMQ 1.7.2 doesn't build in Erlang R14A


So I had to create a local MacPorts repo (using the very helpful instructions HERE) from the previous checkin of the Erlang R13B4 portfile and point ports at that.


Then, with a sudo port uninstall rabbitmq-server; sudo port install rabbitmq-server @1.7.2, followed by a blat of the mnesia database files:
sudo rm -rf /opt/local/var/lib/rabbitmq/mnesia/rabbit

- and it all works again. YAY!

(PS - I had to port uninstall and re-install rabbitmq-server twice for it to work properly. So that's always worth a try too - I swear MacOS gets more and more like Windows every day.....:)