diff --git a/lib/thin/controllers/cluster.rb b/lib/thin/controllers/cluster.rb index 33692c11..30787014 100644 --- a/lib/thin/controllers/cluster.rb +++ b/lib/thin/controllers/cluster.rb @@ -3,7 +3,7 @@ module Thin # An exception class to handle the event that server didn't start on time class RestartTimeout < RuntimeError; end - + module Controllers # Control a set of servers. # * Generate start and stop commands and run them. @@ -12,18 +12,18 @@ module Controllers class Cluster < Controller # Cluster only options that should not be passed in the command sent # to the indiviual servers. - CLUSTER_OPTIONS = [:servers, :only, :onebyone, :wait] - + CLUSTER_OPTIONS = [:servers, :only, :onebyone] + # Maximum wait time for the server to be restarted DEFAULT_WAIT_TIME = 30 # seconds - + # Create a new cluster of servers launched using +options+. def initialize(options) super # Cluster can only contain daemonized servers @options.merge!(:daemonize => true) end - + def first_port; @options[:port] end def address; @options[:address] end def socket; @options[:socket] end @@ -33,35 +33,35 @@ def size; @options[:servers] end def only; @options[:only] end def onebyone; @options[:onebyone] end def wait; @options[:wait] end - + def swiftiply? @options.has_key?(:swiftiply) end - + # Start the servers def start with_each_server { |n| start_server n } end - + # Start a single server def start_server(number) log_info "Starting server on #{server_id(number)} ... " - + run :start, number end - + # Stop the servers def stop with_each_server { |n| stop_server n } end - + # Stop a single server def stop_server(number) log_info "Stopping server on #{server_id(number)} ... " - + run :stop, number end - + # Stop and start the servers. def restart unless onebyone @@ -70,7 +70,7 @@ def restart sleep 0.1 # Let's breath a bit shall we ? start else - with_each_server do |n| + with_each_server do |n| stop_server(n) sleep 0.1 # Let's breath a bit shall we ? start_server(n) @@ -78,7 +78,7 @@ def restart end end end - + def test_socket(number) if socket UNIXSocket.new(socket_for(number)) @@ -88,12 +88,12 @@ def test_socket(number) rescue nil end - + # Make sure the server is running before moving on to the next one. def wait_until_server_started(number) log_info "Waiting for server to start ..." STDOUT.flush # Need this to make sure user got the message - + tries = 0 loop do if test_socket = test_socket(number) @@ -109,7 +109,7 @@ def wait_until_server_started(number) end end end - + def server_id(number) if socket socket_for(number) @@ -119,23 +119,23 @@ def server_id(number) [address, number].join(':') end end - + def log_file_for(number) include_server_number log_file, number end - + def pid_file_for(number) include_server_number pid_file, number end - + def socket_for(number) include_server_number socket, number end - + def pid_for(number) File.read(pid_file_for(number)).chomp.to_i end - + private # Send the command to the +thin+ script def run(cmd, number) @@ -150,7 +150,7 @@ def run(cmd, number) end Command.run(cmd, cmd_options) end - + def with_each_server if only if first_port && only < 80 @@ -166,7 +166,7 @@ def with_each_server size.times { |n| yield first_port + n } end end - + # Add the server port or number in the filename # so each instance get its own file def include_server_number(path, number) diff --git a/lib/thin/controllers/controller.rb b/lib/thin/controllers/controller.rb index bb590265..df27d59a 100644 --- a/lib/thin/controllers/controller.rb +++ b/lib/thin/controllers/controller.rb @@ -3,37 +3,37 @@ module Thin # Error raised that will abort the process and print not backtrace. class RunnerError < RuntimeError; end - + # Raised when a mandatory option is missing to run a command. class OptionRequired < RunnerError def initialize(option) super("#{option} option required") end end - + # Raised when an option is not valid. class InvalidOption < RunnerError; end - + # Build and control Thin servers. # Hey Controller pattern is not only for web apps yo! - module Controllers + module Controllers # Controls one Thin server. # Allow to start, stop, restart and configure a single thin server. class Controller include Logging - + # Command line options passed to the thin script attr_accessor :options - + def initialize(options) @options = options - + if @options[:socket] @options.delete(:address) @options.delete(:port) end end - + def start # Constantize backend class @options[:backend] = eval(@options[:backend], TOPLEVEL_BINDING) if @options[:backend] @@ -41,7 +41,7 @@ def start server = Server.new(@options[:socket] || @options[:address], # Server detects kind of socket @options[:port], # Port ignored on UNIX socket @options) - + # Set options server.pid_file = @options[:pid] server.log_file = @options[:log] @@ -63,7 +63,7 @@ def start # +config+ must be called before changing privileges since it might require superuser power. server.config - + server.change_privilege @options[:user], @options[:group] if @options[:user] && @options[:group] # If a Rack config file is specified we eval it inside a Rack::Builder block to create @@ -86,27 +86,27 @@ def start server.start end - + def stop raise OptionRequired, :pid unless @options[:pid] - + tail_log(@options[:log]) do - if Server.kill(@options[:pid], @options[:force] ? 0 : (@options[:timeout] || 60)) + if Server.kill(@options[:pid], @options[:force] ? 0 : (@options[:wait] || 60)) wait_for_file :deletion, @options[:pid] end end end - + def restart raise OptionRequired, :pid unless @options[:pid] - + tail_log(@options[:log]) do if Server.restart(@options[:pid]) wait_for_file :creation, @options[:pid] end end end - + def config config_file = @options.delete(:config) || raise(OptionRequired, :config) @@ -116,19 +116,19 @@ def config File.open(config_file, 'w') { |f| f << @options.to_yaml } log_info "Wrote configuration to #{config_file}" end - + protected # Wait for a pid file to either be created or deleted. def wait_for_file(state, file) - Timeout.timeout(@options[:timeout] || 30) do + Timeout.timeout(@options[:wait] || 30) do case state when :creation then sleep 0.1 until File.exist?(file) when :deletion then sleep 0.1 while File.exist?(file) end end end - - # Tail the log file of server +number+ during the execution of the block. + + # Tail the log file of server +number+ during the execution of the block. def tail_log(log_file) if log_file tail_thread = tail(log_file) @@ -138,7 +138,7 @@ def tail_log(log_file) yield end end - + # Acts like GNU tail command. Taken from Rails. def tail(file) cursor = File.exist?(file) ? File.size(file) : 0 @@ -171,7 +171,7 @@ def load_adapter rescue Rack::AdapterNotFound => e raise InvalidOption, e.message end - + def load_rackup_config ENV['RACK_ENV'] = @options[:environment] case @options[:rackup] diff --git a/spec/controllers/cluster_spec.rb b/spec/controllers/cluster_spec.rb index fa35f7a8..b09c04dd 100644 --- a/spec/controllers/cluster_spec.rb +++ b/spec/controllers/cluster_spec.rb @@ -5,19 +5,19 @@ before do @cluster = Cluster.new(:chdir => '/rails_app', :address => '0.0.0.0', - :port => 3000, + :port => 3000, :servers => 3, :timeout => 10, :log => 'thin.log', :pid => 'thin.pid' ) end - + it 'should include port number in file names' do expect(@cluster.send(:include_server_number, 'thin.log', 3000)).to eq('thin.3000.log') expect(@cluster.send(:include_server_number, 'thin.pid', 3000)).to eq('thin.3000.pid') end - + it 'should call each server' do calls = [] @cluster.send(:with_each_server) do |port| @@ -25,7 +25,7 @@ end expect(calls).to eq([3000, 3001, 3002]) end - + it 'should start on each port' do expect(Command).to receive(:run).with(:start, options_for_port(3000)) expect(Command).to receive(:run).with(:start, options_for_port(3001)) @@ -41,7 +41,7 @@ @cluster.stop end - + private def options_for_port(port) { :daemonize => true, :log => "thin.#{port}.log", :timeout => 10, :address => "0.0.0.0", :port => port, :pid => "thin.#{port}.pid", :chdir => "/rails_app" } @@ -60,17 +60,17 @@ def options_for_port(port) :pid => 'thin.pid' ) end - + it 'should include socket number in file names' do expect(@cluster.send(:include_server_number, 'thin.sock', 0)).to eq('thin.0.sock') expect(@cluster.send(:include_server_number, 'thin', 0)).to eq('thin.0') end - + it "should exclude :address and :port options" do expect(@cluster.options).not_to have_key(:address) expect(@cluster.options).not_to have_key(:port) end - + it 'should call each server' do calls = [] @cluster.send(:with_each_server) do |n| @@ -78,7 +78,7 @@ def options_for_port(port) end expect(calls).to eq([0, 1, 2]) end - + it 'should start each server' do expect(Command).to receive(:run).with(:start, options_for_socket(0)) expect(Command).to receive(:run).with(:start, options_for_socket(1)) @@ -94,8 +94,8 @@ def options_for_port(port) @cluster.stop end - - + + private def options_for_socket(number) { :daemonize => true, :log => "thin.#{number}.log", :timeout => 10, :socket => "/tmp/thin.#{number}.sock", :pid => "thin.#{number}.pid", :chdir => "/rails_app" } @@ -106,7 +106,7 @@ def options_for_socket(number) before do @cluster = Cluster.new(:chdir => '/rails_app', :address => '0.0.0.0', - :port => 3000, + :port => 3000, :servers => 3, :timeout => 10, :log => 'thin.log', @@ -114,7 +114,7 @@ def options_for_socket(number) :only => 3001 ) end - + it 'should call only specified server' do calls = [] @cluster.send(:with_each_server) do |n| @@ -122,13 +122,13 @@ def options_for_socket(number) end expect(calls).to eq([3001]) end - + it "should start only specified server" do expect(Command).to receive(:run).with(:start, options_for_port(3001)) @cluster.start end - + private def options_for_port(port) { :daemonize => true, :log => "thin.#{port}.log", :timeout => 10, :address => "0.0.0.0", :port => port, :pid => "thin.#{port}.pid", :chdir => "/rails_app" } @@ -148,7 +148,7 @@ def options_for_port(port) :only => 1 ) end - + it 'should call only specified server' do calls = [] @cluster.send(:with_each_server) do |n| @@ -162,7 +162,7 @@ def options_for_port(port) before do @cluster = Cluster.new(:chdir => '/rails_app', :address => '0.0.0.0', - :port => 3000, + :port => 3000, :servers => 3, :timeout => 10, :log => 'thin.log', @@ -170,7 +170,7 @@ def options_for_port(port) :only => 1 ) end - + it 'should call only specified server' do calls = [] @cluster.send(:with_each_server) do |n| @@ -178,13 +178,13 @@ def options_for_port(port) end expect(calls).to eq([3001]) end - + it "should start only specified server" do expect(Command).to receive(:run).with(:start, options_for_port(3001)) @cluster.start end - + private def options_for_port(port) { :daemonize => true, :log => "thin.#{port}.log", :timeout => 10, :address => "0.0.0.0", :port => port, :pid => "thin.#{port}.pid", :chdir => "/rails_app" } @@ -195,7 +195,7 @@ def options_for_port(port) before do @cluster = Cluster.new(:chdir => '/rails_app', :address => '0.0.0.0', - :port => 3000, + :port => 3000, :servers => 3, :timeout => 10, :log => 'thin.log', @@ -203,7 +203,7 @@ def options_for_port(port) :swiftiply => true ) end - + it 'should call each server' do calls = [] @cluster.send(:with_each_server) do |n| @@ -211,7 +211,7 @@ def options_for_port(port) end expect(calls).to eq([0, 1, 2]) end - + it 'should start each server' do expect(Command).to receive(:run).with(:start, options_for_swiftiply(0)) expect(Command).to receive(:run).with(:start, options_for_swiftiply(1)) @@ -227,7 +227,7 @@ def options_for_port(port) @cluster.stop end - + private def options_for_swiftiply(number) { :address => '0.0.0.0', :port => 3000, :daemonize => true, :log => "thin.#{number}.log", :timeout => 10, :pid => "thin.#{number}.pid", :chdir => "/rails_app", :swiftiply => true } @@ -238,7 +238,7 @@ def options_for_swiftiply(number) before do @cluster = Cluster.new(:chdir => '/rails_app', :address => '0.0.0.0', - :port => 3000, + :port => 3000, :servers => 2, :timeout => 10, :log => 'thin.log', @@ -247,7 +247,7 @@ def options_for_swiftiply(number) :wait => 30 ) end - + it "should restart servers one by one" do expect(Command).to receive(:run).with(:stop, options_for_port(3000)) expect(Command).to receive(:run).with(:start, options_for_port(3000)) @@ -256,12 +256,12 @@ def options_for_swiftiply(number) expect(Command).to receive(:run).with(:stop, options_for_port(3001)) expect(Command).to receive(:run).with(:start, options_for_port(3001)) expect(@cluster).to receive(:wait_until_server_started).with(3001) - + @cluster.restart end - + private def options_for_port(port) - { :daemonize => true, :log => "thin.#{port}.log", :timeout => 10, :address => "0.0.0.0", :port => port, :pid => "thin.#{port}.pid", :chdir => "/rails_app" } + { :daemonize => true, :log => "thin.#{port}.log", :timeout => 10, :address => "0.0.0.0", :port => port, :pid => "thin.#{port}.pid", :chdir => "/rails_app", :wait => 30 } end -end \ No newline at end of file +end diff --git a/spec/controllers/controller_spec.rb b/spec/controllers/controller_spec.rb index a500aaaa..709f1e2c 100644 --- a/spec/controllers/controller_spec.rb +++ b/spec/controllers/controller_spec.rb @@ -12,7 +12,7 @@ :max_conns => 2000, :max_persistent_conns => 1000, :adapter => 'rails') - + @server = OpenStruct.new @adapter = OpenStruct.new @@ -20,7 +20,7 @@ expect(@server).to receive(:config) allow(Rack::Adapter::Rails).to receive(:new) { @adapter } end - + it "should configure server" do @controller.start @@ -30,7 +30,7 @@ expect(@server.maximum_connections).to eq(2000) expect(@server.maximum_persistent_connections).to eq(1000) end - + it "should start as daemon" do @controller.options[:daemonize] = true @controller.options[:user] = true @@ -41,31 +41,31 @@ @controller.start end - + it "should configure Rails adapter" do expect(Rack::Adapter::Rails).to receive(:new).with(@controller.options.merge(:root => nil)) @controller.start end - + it "should mount app under :prefix" do @controller.options[:prefix] = '/app' @controller.start - + expect(@server.app.class).to eq(Rack::URLMap) end it "should mount Stats adapter under :stats" do @controller.options[:stats] = '/stats' @controller.start - + expect(@server.app.class).to eq(Stats::Adapter) end - + it "should load app from Rack config" do @controller.options[:rackup] = File.dirname(__FILE__) + '/../../example/config.ru' @controller.start - + expect(@server.app.class).to eq(Proc) end @@ -82,14 +82,14 @@ @controller.start end.to raise_error(RuntimeError, /please/) end - + it "should set server as threaded" do @controller.options[:threaded] = true @controller.start expect(@server.threaded).to be_truthy end - + it "should set RACK_ENV" do @controller.options[:rackup] = File.dirname(__FILE__) + '/../../example/config.ru' @controller.options[:environment] = "lolcat" @@ -97,7 +97,7 @@ expect(ENV['RACK_ENV']).to eq("lolcat") end - + end describe Controller do @@ -105,17 +105,17 @@ @controller = Controller.new(:pid => 'thin.pid', :timeout => 10) allow(@controller).to receive(:wait_for_file) end - + it "should stop" do expect(Server).to receive(:kill).with('thin.pid', 10) @controller.stop end - + it "should restart" do expect(Server).to receive(:restart).with('thin.pid') @controller.restart end - + it "should write configuration file" do silence_stream(STDOUT) do Controller.new(:config => 'test.yml', :port => 5000, :address => '127.0.0.1').config diff --git a/spec/server/robustness_spec.rb b/spec/server/robustness_spec.rb index 7f18a109..35bbf230 100644 --- a/spec/server/robustness_spec.rb +++ b/spec/server/robustness_spec.rb @@ -7,7 +7,7 @@ [200, { 'Content-Type' => 'text/html' }, body] end end - + it "should not crash when header too large" do 100.times do begin @@ -16,18 +16,18 @@ socket.write("Host: localhost\r\n") socket.write("Connection: close\r\n") 10000.times do - socket.write("X-Foo: #{'x' * 100}\r\n") - socket.flush + socket.write("X-Foo: #{'x' * 100}\r\n") + socket.flush end socket.write("\r\n") socket.read socket.close rescue Errno::EPIPE, Errno::ECONNRESET - # Ignore. - end + # Ignore. + end end end - + after do stop_server end