Monday, February 11, 2008

Script for starting and controlling Rails Mongrel clusters automatically

This script is designed to be a simple way of launching all rails apps installed in a root path (default:/var/www) and starting/stopping them as required.



#!/usr/bin/ruby
#
# mongrels by Gary McGhee
#
# This is a startup script for use in /etc/init.d
# and for starting/stopping etc all packs of mongrels at once
#
# based on code from http://www.simplisticcomplexity.com/2006/9/26/start-and-stop-all-your-mongrel_cluster-processes/
#
# features :
# + finds and runs your rails apps assuming they are under
# APP_DIR and have a mongrel_cluster.yml file in their
# config folder
# + uses mongrel_cluster clean feature to clean up processes
# without pid files. This is important ! Otherwise processes
# can survive a deployment and then your old version will
# keep running despite the deployment.
# + status feature gives details on procs and pid files for all
# apps
# + by default, commands apply to all found apps
# + stop will do nothing and won't give an error if the app is
# not running
# + start and restart will do nothing and won't give an error if
# the app is already running
# + restart will just start if it is currently stopped
#
# Installation
# 1) put this in /etc/init.d/
# 2) chmod 755 /etc/init.d/mongrels
# 3) update-rc.d -f mongrels defaults
#
# Sample mongrel_cluster.yml :
#
# ---
# cwd: /var/www/defaultdomain/current/
# port: "8000"
# environment: production
# #address: 127.0.0.1
# pid_file: /var/run/mongrel_cluster/my_app.pid
# servers: 8
# #log_file: /var/www/defaultdomain/current/log/mongrel.log
#

require 'fileutils'
require 'yaml'

SCRIPT_NAME = 'mongrels'
APP_DIR = '/var/www'
SCRIPT_VERSION = '1.0'

DEFAULT_PID_FILE='/var/run/mongrel_cluster/mongrel.pid'
DEFAULT_USER='root'

def cluster_config_file(app)
File.join(APP_DIR, app, "current/config/mongrel_cluster.yml")
end

def load_cluster_config(aFile)
(YAML::load(File.open(aFile)) rescue nil)
end

def is_cluster?(app)
File.exists?(cluster_config_file(app))
end

# not currently used, but left for potential future use
def is_started?(app,aConfig=nil)
pid_file = (aConfig && aConfig['pid_file']) || DEFAULT_PID_FILE
pid_path = File.dirname(pid_file)
pid_pattern = File.basename(pid_file).sub(/\.([^.]*)$/,'.*.\1')
return !Dir[File.join(pid_path,pid_pattern)].empty?
end

def cluster_command(aApp,aCommand)
config_file = cluster_config_file(aApp)
config ||= load_cluster_config(config_file)
pid_file = (config && config['pid_file']) || DEFAULT_PID_FILE
user = (config && config['user']) || DEFAULT_USER
pid_path = File.dirname(pid_file)
pid_pattern = File.basename(pid_file).sub(/\.([^.]*)$/,'.*.\1')

if %w(start restart).include? aCommand
`mkdir -p #{pid_path}`
`chown #{user}:#{user} #{pid_path}`
end
options = (%w(start stop restart).include? aCommand) ? '--clean' : ''


`mongrel_rails cluster::#{aCommand} #{options} -C #{config_file}`
end

puts

cluster_apps = Dir.open(APP_DIR).to_a.delete_if { |aApp| !is_cluster?(aApp) }

VERBS = {
'start' => 'starting',
'stop' => 'stopping',
'restart' => 'restarting',
'status' => 'getting status for'
}

case command = ARGV.first
when 'start','stop','restart','status'
cluster_apps.each do |aApp|
puts VERBS[command]+' '+aApp
puts cluster_command(aApp,command)
end
when 'version'
puts "#{SCRIPT_NAME} version #{SCRIPT_VERSION}"
exit
else
puts "Usage: #{SCRIPT_NAME} {start|stop|restart|version|status}"
exit
end

Wednesday, February 06, 2008

Single common root path for team development with Linux, Windows and Eclipse

For a long time I've adopted the philosophy of having a single, global, path root for all development that is consistent across the whole software team's machines. On Windows I've used SUBST to create a virtual drive letter (eg. R: for 'Repository). I point R to the root of the local working copy of the repositiory I'm currently using. If I have 2 working copies (eg. the trunk and my current branch) then I'll update R: to my current one.

This means that in configuration files eg. for my editor or IDE, they always get the correct files even when I've switched branches. If the whole team does it, we can even share configuration files, build scripts etc via the repository and they will work, regardless of where the current working copy on each machine is actually located.

Well now I'm using Adobe Flex Builder 3 Beta 2 on Linux, which is based on Eclipse.

For Linux I've arrived at the following as my best practice :

ln -s /path/to/working_copy /mnt/root_name

I'm actually running Linux in a virtual machine, and my working copy is shared in via VMWare's shared folders. On Windows I use drive V: via SUBST, and so my main link for general repository use under Linux is now /mnt/v/.

Thats all fine, but not under Eclipse. Using /mnt/v under Eclipse works fine, but in the .project file it expands the symbolic link to something long and ugly which won't mach other developers machines. Damn!

Thankfully there is a simple solution. In Eclipse go to the menu Window->Preferences/General/Workspace/Linked Resources.

Here you can add variables for use elsewhere in the workspace. The DOCUMENTS variable should already be defined here.
So click "New..." and then enter :
Name: V (I'm using capitals to follow the existing convention)
Location: /mnt/v
and click OK and OK again.
Now go to "Project->Properties/Flex Build Path" and for any source folders change them to something like "${V}/this/that/etc". If you add new folders by navigating to /mnt/v first, they will automatically get the ${V} treatment.

Also note that in .project you will get :



<link>
<name>[source path] etc</name>
<type>2</type>
<locationURI>V/this/that/etc</locationURI>
</link>

instead of :



<link>
<name>[source path] etc</name>
<type>2</type>
<location>/mnt/hgfs/something/this/that/etc</location>
</link>


(Note the lack of curly brackets in the first example)

This .project can now be version controlled and used team-wide. However I think the V variable definition will have to be done per machine. Of course, the /mnt/v will have to be done per machine, but that should be maintaned by each developer anyway.