runit and delayed_job

January 16th, 2010 by Michael Guterl

Back story
We’ve been using Collective Idea’s fork of Delayed Job at RecruitMilitary for quite some time now. We’ve processed over 2 million jobs and we’re extremely reliant on it for the day-to-day operation of our site.

For a long time the compelling reason behind us using Collective Idea’s fork of delayed_job was the built in support for daemonization. This functionality is added via the daemons gem, however, it is not without problems. We use monit to kill memory hungry workers and have frequently experienced issues with workers not stopping, deleting their pid file, and another duplicate worker ends up starting. Before you know it, your server is freaking out because there are 4 times the number of workers running than you want.

Everyone has their own solutions too. One of which was working for us for a while, but suddenly started experiencing the same issues. At this point I’m completely irritated, I have to monitor the server and kill off stray workers a few times per day. We considered moving to Resque, but we weren’t ready to change such a critical piece of our infrastructure.

And then I came across runit. A few weeks back I had asked Tobias Lütke about daemonizing clarity and he said they use runit. Tobi is also the original author of Delayed Job, so when I started having all of these problems I asked how he managed daemonizing workers. “we use runit for everything. It’s so much better.”

runit is a cross-platform Unix init scheme with service supervision, a replacement for sysvinit, and other init schemes. It runs on GNU/Linux, *BSD, MacOSX, Solaris, and can easily be adapted to other Unix operating systems.

This intrigued me, however, I was not interested in replacing my init scheme. Luckily ubuntu provides two separate packages runit (doesn’t replace init) and runit-run (replaces init)/ runit-services. We’ll be just using runit.

Configuring runit and delayed_job
sudo apt-get install runit

With the help of some runit configuration files from Rick Olson we moved our worker infrastructure to runit. Documentation for getting all of this setup is sparse, so I hope this can help someone else.

Services are configured in /etc/sv so we create a directory for each worker that we want to run /etc/sv/rm-dj-1 .. rm-dj-n. Inside of each directory create a file named “run” that resembles this:

#!/bin/sh
set -e
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 
APP_ROOT=/home/deploy/public_html/rm/current
cd $APP_ROOT
 
exec 2>&1
exec chpst -u deploy:deploy -e /etc/service/rm-dj-1/env rake jobs:work

Notice that we are just running a rake task, that does not go into the background. When you build services for runit, you have to make sure that it does not background itself, runit handles all of that for you. If the process dies for any reason runit will bring it back up almost instantly. It also handles all of the process id (pid) management for you too.

chpst is a useful tool included with runit, in this case it changes the user from root to deploy and evaluates the files in /etc/service/rm-dj-1/env as environment variables. I just need to set MIN_PRIORITY and RAILS_ENV with some simple below:

echo '0' >/etc/sv/rm-dj-1/env/MIN_PRIORITY
echo 'production' > /etc/sv/rm-dj-1/RAILS_ENV

Currently the workers are just logging to stdout, I need to configure them to log to RAILS_ROOT/log/delayed_job.log again, but it’s not extremely important at the moment. Anyways, runit provides svlogd for handling streams of output. I really don’t know much about what is going on here, but I’ll figure it out some day.

In /etc/sv/rm-dj-n/log create a file named “run” that resembles this:

#!/bin/sh
set -e
exec svlogd ./main

Make sure you create the /etc/sv/rm-dj-n/main directory or it won’t be able to write the data there. Also, be sure to start runsvdir if it is not started already. At this point your first worker should be fully configured and you can make runit aware by symlinking it to the service directory.

sudo ln -s /etc/sv/rm-dj-1 /etc/service/

As soon as runsvdir picks up the new directory it will fire up your worker and its manager process “runsv rm-dj-1.”

ps logging
runit has a really useful feature for debugging. you can simply run:

ps -ef | grep runsvdir

and see any errors. If there are no errors you will just see a series of dots.

runit and monit
We use monit to handle workers with out of control memory usage. Monit used to be in charge of restarting dead workers too, but runit is much faster at detecting this and typically restarts the process before monit even notices. Monit will notice and report that the pid for that entry changed.

check process rm_dj_worker_1
  with pidfile /etc/sv/rm-dj-1/supervise/pid
  start program = "/usr/bin/sv up rm-dj-1" as uid root and gid root with timeout 3000 seconds
  stop program = "/usr/bin/sv down rm-dj-1" as uid root and gid root with timeout 3000 seconds
  group delayed_job
  if totalmem > 175 Mb then restart
  if changed pid then restart
  if 3 restarts within 5 cycles then timeout

The Future
There is talk on the mailing list about a Unicorn style pre-forking worker model. I will be extremely happy once this is merged in. We’ve been using Unicorn for the last couple months and it has been amazing!

Full Configuration

require ’spec/spec_helper’

December 5th, 2009 by Michael Guterl

This has been ranted about by others many times and I never really understood the big deal. Now I do, especially after it shaved 5 seconds off our suite of specs.

After some research I found out we were requiring our spec helpers in a “bad way.” Most of this is because of my own doing, but I thought I’d share with you guys.

There were probably 4 or 5 variations of the syntax used to require spec_helper.

require File.dirname(__FILE__) + '/../spec_helper'
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
require File.expand_path(File.dirname(__FILE__), + '/../spec_helper')
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))

Each of these variations existed for the different possible depths below spec, for example, spec/models, spec/models/lead, etc.

This causes ruby to attempt to load the file each time, which we really don’t want to do.

The solution is simple.

require 'spec/spec_helper'

This works with autospec and rake just fine and looks much better.

Writing Good Factories

December 5th, 2009 by Michael Guterl

I really wanted to remember what Pratik had to say about writing good factories in his Rails Summit talk so I’m putting it up here as a reference.

  1. Should be able to loop
    10.times { Factory(:user) }
  2. No associations in the base Factory
    Factory(:user) and Factory(:user_with_items)
  3. Should pass validations

I am thinking that rule #2 can be broken in order to achieve #3. Sometimes an object has to have a parent. I think it may be more accurate to say “No has many associations in the base Factory”, but I’m still giving it some thought.

Problems, difficulties and frustrations

June 8th, 2009 by Michael Guterl

After reading The Simplest Thing that Could Possibly Work I couldn’t stop thinking about how Ward Cunningham describes the difference between problems and difficulties.

A friend of mine once said that there are problems and there are difficulties. A problem is something you savor. You say, “Well that’s an interesting problem. Let me think about that problem a while.” You enjoy thinking about it, because when you find the solution to the problem, it’s enlightening.

And then there are difficulties. Computers are famous for difficulties. A difficulty is just a blockage from progress. You have to try a lot of things. When you finally find what works, it doesn’t tell you a thing. It won’t be the same tomorrow. Getting the computer to work is so often dealing with difficulties.

As a software developer I love to solve problems, yet I become frustrated when dealing with difficulties. The more I think about it, I feel like frustration is a better word than difficulty.  In fact, one of the definitions for frustration from Apple’s Dictionary is:

frustration |frəˈstrā sh ən|

the prevention of the progress, success, or fulfillment of something

I don’t mind slowing down, but I hate stopping progress.  As I work through practicing TDD/BDD, I find that being able to slow down and take baby steps allows me to reduce the number of frustrations in my day.  I shudder any time I have to touch code without tests/specs because I am afraid I’m going to break something and not know it.  I’m not sure how I lived without tests, I know I spent a lot of time clicking around doing testing in the browser.

Saving time [and sanity] with AppleScript

May 31st, 2009 by Michael Guterl

Depending where I am at I have three possible monitor configurations for my MacBook.

  1. External display only
  2. Laptop display only
  3. External and laptop display

Each time I change between these three workspaces I have to manually move the windows to the correct position for that particular configuration.  This can become extremely annoying, especially when you’re as OCD as I am.  I can’t even contemplate how much time I’ve wasted putting each window in its “perfect” position…

After many searches I came across exactly what I was looking for: http://www.jonathanlaliberte.com/2009/02/04/restore-previous-display-window-positions-applescript

Usage

  1. Download the script here
  2. Open it with Script Editor
  3. Remove references to applications you aren’t using
  4. Save as an application with the name of the layout in the Applications fold
  5. Use Spotlight to run the app

I use the same script for each layout, I just save it under a different name. Problems

  1. Certain applications don’t seem to work (TweetDeck)
  2. Applications with child windows require slightly more work (see Firefox example code)
  3. The application has to be running or it errors

Todo

  1. Start the application if it is not running
  2. Loop through all open applications eliminating manual configuration

Source

property numFFWindows : 0
property FFPos : {}
property FFSize : {}
property numTermWindows : 0
property TermPos : {}
property TermSize : {}
property iTunesPos : {}
property iTunesSize : {}
property EmacsPos : {}
property EmacsSize : {}
property TweetDeckPos : {}
property TweetDeckSize : {}
property iCalPos : 0
property iCalSize : 0
property AdiumContactsPos : 0
property AdiumContactsSize : 0
property AdiumIMSize : 0
property AdiumIMPos : 0
property OFPos : 0
property OFSize : 0
 
display dialog "Set Window Position or Save Window Position?" buttons {"Restore", "Save"} default button "Restore"
set theResult to result
 
tell application "System Events"
	if (button returned of theResult is "Restore") then
		-- Restore Settings
		if (numFFWindows > 0) then
			tell process "Firefox"
				repeat with i from 1 to numFFWindows
					set position of window i to (item i of FFPos)
					set size of window i to (item i of FFSize)
				end repeat
			end tell
		end if
		if (numTermWindows > 0) then
			tell process "Terminal"
				repeat with i from 1 to numTermWindows
					set position of window i to (item i of TermPos)
					set size of window i to (item i of TermSize)
				end repeat
			end tell
		end if
		if (iTunesPos is not {0, 0}) then
			tell process "iTunes"
				set position of window 1 to iTunesPos
				set size of window 1 to iTunesSize
			end tell
		end if
		if (EmacsPos is not {0, 0}) then
			tell process "Emacs"
				set position of window 1 to EmacsPos
				set size of window 1 to EmacsSize
			end tell
		end if
		if (iCalPos is not {0, 0}) then
			tell process "iCal"
				set position of window 1 to iCalPos
				set size of window 1 to iCalSize
			end tell
		end if
		if (OFPos is not {0, 0}) then
			tell process "OmniFocus"
				set position of window 1 to OFPos
				set size of window 1 to OFSize
			end tell
		end if
		if (AdiumContactsPos is not {0, 0}) then
			tell process "Adium"
				set position of window "Contacts" to AdiumContactsPos
				set size of window "Contacts" to AdiumContactsSize
				repeat with i from 1 to (count windows)
					if ((window i) is not (window "Contacts")) then
						set position of window i to AdiumIMPos
						set size of window i to AdiumIMSize
					end if
				end repeat
 
			end tell
		end if
 
	else
		-- Save Settings
		tell process "Firefox"
			set numFFWindows to count windows
			set FFPos to {}
			set FFSize to {}
			repeat with i from 1 to numFFWindows
				set end of FFPos to (position of window i)
				set end of FFSize to (size of window i)
			end repeat
		end tell
		tell process "Terminal"
			set numTermWindows to count windows
			set TermPos to {}
			set TermSize to {}
			repeat with i from 1 to numTermWindows
				set end of TermPos to (position of window i)
				set end of TermSize to (size of window i)
			end repeat
		end tell
		tell process "iTunes"
			set iTunesPos to position of window 1
			set iTunesSize to size of window 1
		end tell
		tell process "Emacs"
			set EmacsPos to position of window 1
			set EmacsSize to size of window 1
		end tell
		tell process "iCal"
			set iCalPos to position of window 1
			set iCalSize to size of window 1
		end tell
		tell process "OmniFocus"
			set OFPos to position of window 1
			set OFSize to size of window 1
		end tell
		tell process "Adium"
			set AdiumContactsPos to position of window "Contacts"
			set AdiumContactsSize to size of window "Contacts"
			set AdiumIMPos to {}
			set AdiumIMSize to {}
			repeat with i from 1 to (count windows)
				if ((window i) is not (window "Contacts")) then
					set AdiumIMPos to (position of window i)
					set AdiumIMSize to (size of window i)
				end if
			end repeat
		end tell
	end if
end tell

Ubuntu 8.10 Server Setup (Rails)

April 19th, 2009 by Michael Guterl

I spent the first half of today moving my server from Debian 4.0 to Ubuntu 8.10.  It was a fairly painless process and I decided to record the route I took for configuring everything.  There are many options out there for quickly getting a Rails environment up on Ubuntu and this is a conglomeration of many ideas.

You can either continue reading or view the gist directly.

Read the rest of this entry »

Revamped blog

April 19th, 2009 by Michael Guterl

I’ve changed my blog from mephisto to wordpress.  I decided to be really lazy and not migrate any of the content from the mephisto blog.  Oh well, there wasn’t that much useful stuff there anyway…