Skip to content

Commit

Permalink
Support running in a single process on platforms where fork is not …
Browse files Browse the repository at this point in the history
…available.

Can be forced by setting the number of workers to 0.
  • Loading branch information
macournoyer committed Nov 29, 2011
1 parent ee9d987 commit 9c29f33
Show file tree
Hide file tree
Showing 10 changed files with 260 additions and 48 deletions.
50 changes: 50 additions & 0 deletions lib/thin/backends/prefork.rb
@@ -0,0 +1,50 @@
require "preforker"

module Thin
# Raised when the pid file already exist starting as a daemon.
class PidFileExist < RuntimeError; end

module Backends
class Prefork
def initialize(server)
@server = server
end

def start(daemonize)
if File.file?(@server.pid_path)
raise PidFileExist, "#{@server.pid_path} already exists. Thin is already running or the file is stale. " +
"Stop the process or delete #{@server.pid_path}."
end

@prefork = Preforker.new(
:app_name => @server.to_s,
:workers => @server.workers,
:timeout => @server.timeout,
:pid_path => @server.pid_path,
:stderr_path => @server.log_path,
:stdout_path => @server.log_path,
:logger => Logger.new(@server.log_path || $stdout)
) do |master|

EM.run do
EM.add_periodic_timer(4) do
EM.stop_event_loop unless master.wants_me_alive?
end

yield
end
end

if daemonize
@prefork.start
else
@prefork.run
end
end

def stop
@prefork.quit if @prefork
end
end
end
end
26 changes: 26 additions & 0 deletions lib/thin/backends/single_process.rb
@@ -0,0 +1,26 @@
module Thin
module Backends
class SingleProcess
def initialize(server)
@server = server
end

def start(daemonize)
raise NotImplementedError, "Daemonization not supported in single process mode" if daemonize

$0 = @server.to_s

# Install signals
trap("INT", "EXIT")

EM.run do
yield
end
end

def stop
EM.stop_event_loop
end
end
end
end
3 changes: 2 additions & 1 deletion lib/thin/runner.rb
Expand Up @@ -62,7 +62,8 @@ def parse!(args)
options[:log] = ::File.expand_path(f)
}

opts.on("-W", "--workers NUMBER", "starts NUMBER of workers (default: number of processors)") { |n|
opts.on("-W", "--workers NUMBER", "starts NUMBER of workers (default: number of processors)",
"0 to run in a single process") { |n|
options[:workers] = n.to_i
}

Expand Down
120 changes: 83 additions & 37 deletions lib/thin/server.rb
@@ -1,76 +1,122 @@
require "preforker"
require "eventmachine"
require "socket"

require "thin/system"
require "thin/connection"
require "thin/backends/prefork"
require "thin/backends/single_process"

module Thin
class Server
attr_accessor :app, :address, :port, :backlog, :workers, :timeout, :pid_path, :log_path, :use_epoll, :maximum_connections
# Application (Rack adapter) called with the request that produces the response.
attr_accessor :app

# A tag that will show in the process listing
attr_accessor :tag

# Address on which the server is listening for connections.
attr_accessor :address
alias :host :address
alias :host= :address=

# Port on which the server is listening for connections.
attr_accessor :port

attr_accessor :backlog

attr_accessor :workers

# Workers are killer if they don't checked in under `timeout` seconds.
attr_accessor :timeout

attr_accessor :pid_path

attr_accessor :log_path

attr_accessor :use_epoll

# Maximum number of file or socket descriptors that the server may open.
attr_accessor :maximum_connections

# Backend handling the connections to the clients.
attr_writer :backend

def initialize(app, address="0.0.0.0", port=3000)
@app = app
@address = address
@port = port
@backlog = 1024
@workers = nil
@timeout = 30
@pid_path = "./thin.pid"
@log_path = nil
@use_epoll = true
@maximum_connections = 1024

if System.supports_fork?
# One worker per processor
@workers = System.processor_count
else
@workers = 0
end
end

def backend
@backend ||= begin
if prefork?
Backends::Prefork.new(self)
else
Backends::SingleProcess.new(self)
end
end
end

def start(daemonize=false)
# One worker per processor
@workers = System.processor_count unless @workers

# Configure EventMachine
EM.epoll if @use_epoll
@maximum_connections = EM.set_descriptor_table_size(@maximum_connections)
puts "Maximum connections set to #{@maximum_connections} per worker"

# Starts and configure the server socket.
socket = TCPServer.new(@address, @port)
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
socket.listen(@backlog)

trap("EXIT") { socket.close }
@socket = TCPServer.new(@address, @port)
@socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
@socket.listen(@backlog)

# Prefork!
puts "Starting #{@workers} worker(s) ..."
@prefork = Preforker.new(
:workers => @workers,
:app_name => "Thin",
:timeout => @timeout,
:pid_path => pid_path,
:stderr_path => @log_path,
:stdout_path => @log_path,
:logger => Logger.new(@log_path || $stdout)
) do |master|

EM.run do
EM.add_periodic_timer(4) do
EM.stop_event_loop unless master.wants_me_alive?
end

EM.attach_server(socket, Connection) { |c| c.server = self }
end
end
trap("EXIT") { stop }

puts "Using #{@workers} worker(s) ..." if @workers > 0
puts "Listening on #{@address}:#{@port}, CTRL+C to stop"
if daemonize
@prefork.start
else
@prefork.run

backend.start(daemonize) do
EM.attach_server(@socket, Connection) { |c| c.server = self }
@started = true
end
rescue
@socket.close if @socket
raise
end

def started?
@started
end

def stop
@prefork.quit if @prefork
if started?
puts "Stopping ..."
backend.stop
@socket.close
@socket = nil
@started = false
end
end
alias :shutdown :stop

def prefork?
@workers > 0
end

def to_s
"Thin" + (@tag ? " [#{@tag}]" : "")
end
end
end
11 changes: 11 additions & 0 deletions lib/thin/system.rb
Expand Up @@ -4,6 +4,10 @@ def self.win?
RUBY_PLATFORM =~ /mswin|mingw/
end

def self.java?
RUBY_PLATFORM =~ /java/
end

def self.linux?
RUBY_PLATFORM =~ /linux/
end
Expand Down Expand Up @@ -34,5 +38,12 @@ def self.processor_count
1
end
end

def self.supports_fork?
fork { exit }
true
rescue NotImplementedError
false
end
end
end
15 changes: 6 additions & 9 deletions test/integration/error_test.rb
Expand Up @@ -18,15 +18,12 @@ def test_raise_without_middlewares
end

def test_logs_errors
log_file = "test.log"
File.delete log_file if File.exist?(log_file)
thin :env => "none", :log => log_file

get "/raise"
assert_match "Error processing request: ouch", File.read(log_file)

ensure
File.delete log_file
with_log_file do |log_file|
thin :env => "none", :log => log_file

get "/raise"
assert_match "Error processing request: ouch", File.read(log_file)
end
end

def test_parse_error
Expand Down
12 changes: 12 additions & 0 deletions test/integration/single_process_test.rb
@@ -0,0 +1,12 @@
require 'test_helper'

class SingleProcessTest < IntegrationTestCase
def test_stop_with_int_signal
@pid = thin :workers => 0

Process.kill "INT", @pid

Process.wait @pid
@pid = nil
end
end
29 changes: 28 additions & 1 deletion test/test_helper.rb
Expand Up @@ -28,6 +28,23 @@ def silence_stream(stream)
ensure
stream.reopen(old_stream)
end

def silence_streams
silence_stream($stdout) { silence_stream($stderr) { yield } }
end

def capture_streams
out = StringIO.new
silence_streams do
$stdout = out
$stderr = out
yield
out.read
end
ensure
$stdout = STDOUT
$stderr = STDERR
end
end

class IntegrationTestCase < Test::Unit::TestCase
Expand All @@ -52,7 +69,7 @@ def thin(options={})
raise "Failed to start server" if tries > 20
end

@pid = File.read(pid_file).to_i
@pid = File.read(pid_file).to_i if File.exist?(pid_file)

launcher_pid
end
Expand Down Expand Up @@ -90,6 +107,16 @@ def socket
socket.close rescue nil
end

def with_log_file
log_file = "test.log"
File.delete log_file if File.exist?(log_file)

yield log_file

ensure
File.delete log_file if File.exist?(log_file)
end

def assert_status(status)
assert_equal status, @response.code.to_i
end
Expand Down
25 changes: 25 additions & 0 deletions test/unit/server_test.rb
@@ -0,0 +1,25 @@
require 'test_helper'

class ServerTest < Test::Unit::TestCase
def setup
app = proc { |env| [200, {}, ["ok"]] }
@server = Thin::Server.new(app)
end

def test_pick_prefork_backend_if_any_workers
@server.workers = 1
assert_kind_of Thin::Backends::Prefork, @server.backend
end

def test_pick_single_process_backend_if_no_workers
@server.workers = 0
assert_kind_of Thin::Backends::SingleProcess, @server.backend
end

def test_cant_daemonize_single_process
@server.workers = 0
assert_raise(NotImplementedError) do
silence_streams { @server.start(true) }
end
end
end

0 comments on commit 9c29f33

Please sign in to comment.