Skip to content

Commit

Permalink
Add cluster support through the -s option in the thin script, start 3…
Browse files Browse the repository at this point in the history
… thins like this:

  thin start -s3 -p3000

3 thin servers will be started on port 3000, 3001, 3002, also the port number will be
injected in the pid and log filenames.
  • Loading branch information
macournoyer committed Jan 12, 2008
1 parent 31d7fe1 commit 6a77256
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 52 deletions.
15 changes: 9 additions & 6 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
== 0.5.2 Cheezburger release
* Add cluster support through the -s option in the thin script, start 3 thins like this:
thin start -s3 -p3000
3 thin servers will be started on port 3000, 3001, 3002, also the port number will be
injected in the pid and log filenames.
* Fix IOError when writing to logger when starting server as a daemon.
* Really change directory when the -c option is specified.
* Add restart command to thin script.
* Fix typo in thin script usage message and expand chdir path.
* Rename thin script options to be the same as mongrel_rails script:
-o --host => -a --address
--log-file => --log
--pid-file => --pid
--env => --environment
[thronedrk]
* Rename thin script options to be the same as mongrel_rails script [thronedrk]:
-o --host => -a --address
--log-file => --log
--pid-file => --pid
--env => --environment

== 0.5.1 LOLCAT release
* Add URL rewriting to Rails adapter so that page caching works and / fetches index.html if present.
Expand Down
94 changes: 53 additions & 41 deletions bin/thin
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ require 'optparse'
COMMANDS = %w(start stop restart)

options = {
:root => Dir.pwd,
:env => 'development',
:host => '0.0.0.0',
:port => 3000,
:timeout => 60,
:log_file => 'log/thin.log',
:pid_file => 'tmp/pids/thin.pid'
:chdir => Dir.pwd,
:environment => 'development',
:address => '0.0.0.0',
:port => 3000,
:timeout => 60,
:log => 'log/thin.log',
:pid => 'tmp/pids/thin.pid',
:servers => 1
}

opts = OptionParser.new do |opts|
Expand All @@ -22,74 +23,85 @@ opts = OptionParser.new do |opts|
opts.separator ""
opts.separator "Server options:"

opts.on("-a", "--address HOST", "bind to HOST address (default: 0.0.0.0)") { |host| options[:host] = host }
opts.on("-p", "--port PORT", "use PORT (default: 3000)") { |port| options[:port] = port }
opts.on("-e", "--environment ENV", "Rails environment (default: development)") { |env| options[:env] = env }
opts.on("-c", "--chdir PATH", "Change to dir before starting") { |dir| options[:root] = File.expand_path(dir) }
opts.on("-a", "--address HOST", "bind to HOST address (default: 0.0.0.0)") { |host| options[:address] = host }
opts.on("-p", "--port PORT", "use PORT (default: 3000)") { |port| options[:port] = port.to_i }
opts.on("-e", "--environment ENV", "Rails environment (default: development)") { |env| options[:environment] = env }
opts.on("-c", "--chdir PATH", "Change to dir before starting") { |dir| options[:chdir] = File.expand_path(dir) }
opts.on("-s", "--servers NUM", "Number of servers to start") { |num| options[:servers] = num.to_i }
opts.on("-d", "--daemonize", "Run daemonized in the background") { options[:daemonize] = true }
opts.on("-l", "--log FILE", "File to redirect output",
"(default: #{options[:log_file]})") { |file| options[:log_file] = file }
"(default: #{options[:log]})") { |file| options[:log] = file }
opts.on("-P", "--pid FILE", "File to store PID",
"(default: #{options[:pid_file]})") { |file| options[:pid_file] = file }
"(default: #{options[:pid]})") { |file| options[:pid] = file }
opts.on("-t", "--timeout SEC", "Request or command timeout in sec",
"(default: #{options[:timeout]})") { |sec| options[:timeout] = sec }
"(default: #{options[:timeout]})") { |sec| options[:timeout] = sec.to_i }
opts.on("-u", "--user NAME", "User to run daemon as (use with -g)") { |user| options[:user] = user }
opts.on("-g", "--group NAME", "Group to run daemon as (use with -u)") { |group| options[:group] = group }

opts.separator ""
opts.separator "Common options:"

opts.on_tail("-D", "--debug", "Set debbuging on") { $DEBUG = true }

opts.on_tail("-h", "--help", "Show this message") do
puts opts
exit
end

opts.on_tail('-v', '--version', "Show version") do
puts Thin::SERVER
exit
end
opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
opts.on_tail('-v', '--version', "Show version") { puts Thin::SERVER; exit }

opts.parse! ARGV
end


# Commands definitions

def cluster?(options)
options[:servers] && options[:servers] > 1
end

def start(options)
server = Thin::Server.new(options[:host], options[:port])
if cluster?(options)
server = Thin::Cluster.new(options)
server.start
else
server = Thin::Server.new(options[:address], options[:port])

server.pid_file = options[:pid_file]
server.log_file = options[:log_file]
server.timeout = options[:timeout]
server.pid_file = options[:pid]
server.log_file = options[:log]
server.timeout = options[:timeout]

if options[:daemonize]
server.change_privilege options[:user], options[:group] if options[:user] && options[:group]
server.daemonize
end
if options[:daemonize]
server.change_privilege options[:user], options[:group] if options[:user] && options[:group]
server.daemonize
end

server.app = Rack::Adapter::Rails.new(options)

server.start!
server.app = Rack::Adapter::Rails.new(options.merge(:root => options[:chdir]))
server.start!
end
end

def stop(options)
Thin::Server.kill options[:pid_file], options[:timeout]
if cluster?(options)
server = Thin::Cluster.new(options)
server.stop
else
Thin::Server.kill options[:pid], options[:timeout]
end
end

def restart(options)
# Restart only make sense when running as a daemon
options.update :daemonize => true
if cluster?(options)
server = Thin::Cluster.new(options)
server.restart
else
# Restart only make sense when running as a daemon
options.update :daemonize => true

stop(options)
start(options)
stop(options)
start(options)
end
end


# Runs the command

Dir.chdir(options[:root])
Dir.chdir(options[:chdir])
command = ARGV[0]

if COMMANDS.include?(command)
Expand Down
4 changes: 2 additions & 2 deletions lib/rack/adapter/rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ module Rack
module Adapter
class Rails
def initialize(options={})
@root = options[:root] || Dir.pwd
@env = options[:env] || 'development'
@root = options[:root] || Dir.pwd
@env = options[:environment] || 'development'

load_application

Expand Down
1 change: 1 addition & 0 deletions lib/thin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module Thin
NAME = 'thin'.freeze
SERVER = "#{NAME} #{VERSION::STRING} codename #{VERSION::CODENAME}".freeze

autoload :Cluster, 'thin/cluster'
autoload :Connection, 'thin/connection'
autoload :Daemonizable, 'thin/daemonizing'
autoload :Logging, 'thin/logging'
Expand Down
103 changes: 103 additions & 0 deletions lib/thin/cluster.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
module Thin
# Control a set of servers. Generate start and stop commands and run them.
class Cluster
include Logging

class << self
# Script to run
attr_accessor :thin_script
end
self.thin_script = 'thin'

# Number of servers in the cluster.
attr_accessor :size

# Command line options passed to the thin script
attr_accessor :options

# Create a new cluster of servers launched using +options+.
def initialize(options)
@options = options.merge(:daemonize => true)
@size = @options.delete(:servers)
end

def first_port; @options[:port] end
def address; @options[:address] end
def pid_file; File.expand_path File.join(@options[:chdir], @options[:pid]) end
def log_file; File.expand_path File.join(@options[:chdir], @options[:log]) end

# Start the servers
def start
with_each_server { |port| start_on_port port }
end

# Start the server on a single port
def start_on_port(port)
log "Starting #{address}:#{port} ... "

run :start, @options, port
end

# Stop the servers
def stop
with_each_server { |port| stop_on_port port }
end

# Stop the server running on +port+
def stop_on_port(port)
log "Stopping #{address}:#{port} ... "

run :stop, @options, port
end

# Stop and start the servers.
def restart
stop
sleep 0.1 # Let's breath a bit shall we ?
start
end

def log_file_for(port)
include_port_number log_file, port
end

def pid_file_for(port)
include_port_number pid_file, port
end

def pid_for(port)
File.read(pid_file_for(port)).chomp.to_i
end

private
# Send the command to the +thin+ script
def run(cmd, options, port)
shell_cmd = shellify(cmd, options.merge(:pid => pid_file_for(port), :log => log_file_for(port)))
trace shell_cmd
log `#{shell_cmd}`
end

# Turn into a runnable shell command
def shellify(cmd, options)
shellified_options = options.inject([]) do |args, (name, value)|
args << case value
when NilClass
when TrueClass then "--#{name}"
else "--#{name.to_s.tr('_', '-')}=#{value.inspect}"
end
end
"#{self.class.thin_script} #{cmd} #{shellified_options.compact.join(' ')}"
end

def with_each_server
@size.times { |n| yield first_port + n }
end

# Add the port numbers in the filename
# so each instance get its own file
def include_port_number(path, port)
raise ArgumentError, "filename '#{path}' must include an extension" unless path =~ /\./
path.gsub(/\.(.+)$/) { ".#{port}.#{$1}" }
end
end
end
2 changes: 1 addition & 1 deletion lib/thin/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def parse(data)

if @parser.finished? # Header finished, can only be some more body
body << data
elsif @data.size > MAX_HEADER # Oho! very big header, must be a bad person
elsif @data.size > MAX_HEADER # Oho! very big header, must be a mean person
raise InvalidRequest, MAX_HEADER_MSG
else # Parse more header
@nparsed = @parser.execute(@env, @data, @nparsed)
Expand Down
62 changes: 62 additions & 0 deletions spec/cluster_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
require File.dirname(__FILE__) + '/spec_helper'

describe Cluster do
before do
Thin::Cluster.thin_script = File.dirname(__FILE__) + '/../bin/thin'
@cluster = Thin::Cluster.new(:chdir => File.dirname(__FILE__) + '/rails_app',
:address => '0.0.0.0',
:port => 3000,
:servers => 3,
:timeout => 10,
:log => 'thin.log',
:pid => 'thin.pid'
)
@cluster.silent = true
end

it 'should include port number in file names' do
@cluster.send(:include_port_number, 'thin.log', 3000).should == 'thin.3000.log'
@cluster.send(:include_port_number, 'thin.pid', 3000).should == 'thin.3000.pid'
proc { @cluster.send(:include_port_number, 'thin', 3000) }.should raise_error(ArgumentError)
end

it 'should call each server' do
calls = []
@cluster.send(:with_each_server) do |port|
calls << port
end
calls.should == [3000, 3001, 3002]
end

it 'should shellify command' do
out = @cluster.send(:shellify, :start, :port => 3000, :daemonize => true, :log => 'hi.log', :pid => nil)
out.should include('--port=3000', '--daemonize', '--log="hi.log"', 'thin start --')
out.should_not include('--pid=')
end

it 'should absolutize file path' do
@cluster.pid_file_for(3000).should == File.expand_path(File.dirname(__FILE__) + "/rails_app/thin.3000.pid")
end

it 'should start on specified port' do
@cluster.start_on_port 3000

File.exist?(@cluster.pid_file_for(3000)).should be_true
File.exist?(@cluster.log_file_for(3000)).should be_true
end

it 'should stop on specified port' do
@cluster.start_on_port 3000
@cluster.stop_on_port 3000

File.exist?(@cluster.pid_file_for(3000)).should be_false
end

after do
3000.upto(3003) do |port|
Process.kill 9, @cluster.pid_for(port) rescue nil
File.delete @cluster.pid_file_for(port) rescue nil
File.delete @cluster.log_file_for(port) rescue nil
end
end
end
12 changes: 10 additions & 2 deletions tasks/gem.rake
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require 'rake/gempackagetask'

CLEAN.include %w(pkg *.gem)
task :clean => :clobber_package

spec = Gem::Specification.new do |s|
s.name = Thin::NAME
Expand Down Expand Up @@ -32,6 +32,14 @@ Rake::GemPackageTask.new(spec) do |p|
p.gem_spec = spec
end

task :tag_warn do
puts "*" * 40
puts "Don't forget to tag the release:"
puts " git tag -a v#{Thin::VERSION::STRING}"
puts "*" * 40
end
task :gem => :tag_warn

namespace :gem do
desc 'Upload gem to code.macournoyer.com'
task :upload => :gem do
Expand All @@ -43,7 +51,7 @@ namespace :gem do
task :upload_rubyforge => :gem do
sh 'rubyforge login'
sh "rubyforge add_release thin thin #{Thin::VERSION::STRING} pkg/thin-#{Thin::VERSION::STRING}.gem"
sh "rubyforge add_file thin thin #{Thin::VERSION::STRING} pkg/thin-#{Thin::VERSION::STRING}.gem"
sh "rubyforge add_file thin thin #{Thin::VERSION::STRING} pkg/thin-#{Thin::VERSION::STRING}.gem"
end
end

Expand Down

0 comments on commit 6a77256

Please sign in to comment.