thin 0.6.4 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of thin might be problematic. Click here for more details.

Files changed (45) hide show
  1. data/CHANGELOG +20 -0
  2. data/README +11 -12
  3. data/benchmark/abc +51 -0
  4. data/benchmark/benchmarker.rb +80 -0
  5. data/benchmark/runner +79 -0
  6. data/example/adapter.rb +3 -3
  7. data/example/thin.god +11 -7
  8. data/lib/thin.rb +17 -16
  9. data/lib/thin/command.rb +10 -4
  10. data/lib/thin/connection.rb +46 -13
  11. data/lib/thin/connectors/connector.rb +22 -10
  12. data/lib/thin/connectors/swiftiply_client.rb +55 -0
  13. data/lib/thin/controllers/cluster.rb +28 -22
  14. data/lib/thin/controllers/controller.rb +74 -14
  15. data/lib/thin/controllers/service.rb +1 -1
  16. data/lib/thin/daemonizing.rb +6 -4
  17. data/lib/thin/headers.rb +4 -0
  18. data/lib/thin/logging.rb +34 -9
  19. data/lib/thin/request.rb +31 -2
  20. data/lib/thin/response.rb +22 -7
  21. data/lib/thin/runner.rb +27 -14
  22. data/lib/thin/server.rb +55 -7
  23. data/lib/thin/version.rb +3 -3
  24. data/spec/command_spec.rb +2 -3
  25. data/spec/connection_spec.rb +15 -1
  26. data/spec/connectors/swiftiply_client_spec.rb +66 -0
  27. data/spec/controllers/cluster_spec.rb +43 -12
  28. data/spec/controllers/controller_spec.rb +16 -4
  29. data/spec/controllers/service_spec.rb +0 -1
  30. data/spec/logging_spec.rb +42 -0
  31. data/spec/request/persistent_spec.rb +35 -0
  32. data/spec/response_spec.rb +18 -0
  33. data/spec/server/pipelining_spec.rb +108 -0
  34. data/spec/server/swiftiply.yml +6 -0
  35. data/spec/server/swiftiply_spec.rb +27 -0
  36. data/spec/server/tcp_spec.rb +3 -3
  37. data/spec/server_spec.rb +22 -0
  38. data/spec/spec_helper.rb +3 -3
  39. data/tasks/gem.rake +1 -1
  40. data/tasks/spec.rake +9 -0
  41. metadata +13 -6
  42. data/benchmark/previous.rb +0 -14
  43. data/benchmark/simple.rb +0 -15
  44. data/benchmark/utils.rb +0 -75
  45. data/lib/thin_parser.bundle +0 -0
data/CHANGELOG CHANGED
@@ -1,3 +1,23 @@
1
+ == 0.7.0 Spherical Cow release
2
+ * Add --max-persistent-conns option to sets the maximum number of persistent connections.
3
+ Set to 0 to disable Keep-Alive.
4
+ * INT signal now force stop and QUIT signal gracefully stops.
5
+ * Warn when descriptors table size can't be set as high as expected.
6
+ * Eval Rackup config file using top level bindings.
7
+ * Remove daemons gem dependency on Windows plateform, fixes #45.
8
+ * Change default timeout from 60 to 30 seconds.
9
+ * Add --max-conns option to sets the maximum number of file or socket descriptors that
10
+ your process may open, defaults to 1024.
11
+ * Tail logfile when stopping and restarting a demonized server, fixes #26.
12
+ * Wrap application in a Rack::CommonLogger adapter in debug mode.
13
+ * --debug (-D) option no longer set $DEBUG so logging will be less verbose
14
+ and Ruby won't be too strict, fixes #36.
15
+ * Deprecate Server#silent in favour of Logging.silent.
16
+ * Persistent connection (keep-alive) & HTTP pipelining support.
17
+ * Fix -s option not being included in generated config file, fixes #37.
18
+ * Add Swiftiply support. Use w/ the --swiftiply (-y) option in the thin script,
19
+ closes #28 [Alex MacCaw]
20
+
1
21
  == 0.6.4 Sexy Lobster release
2
22
  * Fix error when stopping server on UNIX domain socket, fixes #42
3
23
  * Rescue errors in Connection#get_peername more gracefully, setting REMOTE_ADDR to nil, fixes #43
data/README CHANGED
@@ -2,9 +2,10 @@
2
2
  Tiny, fast & funny HTTP server
3
3
 
4
4
  Thin is a Ruby web server that glues together 3 of the best Ruby libraries in web history:
5
- * the Mongrel parser: the root of Mongrel speed and security
6
- * Event Machine: a network I/O library with extremely high scalability, performance and stability
7
- * Rack: a minimal interface between webservers and Ruby frameworks
5
+ * the Mongrel parser: the root of Mongrel speed and security
6
+ * Event Machine: a network I/O library with extremely high scalability, performance and stability
7
+ * Rack: a minimal interface between webservers and Ruby frameworks
8
+
8
9
  Which makes it, with all humility, the most secure, stable, fast and extensible Ruby web server
9
10
  bundled in an easy to use gem for your own pleasure.
10
11
 
@@ -24,17 +25,15 @@ Or from source:
24
25
  rake install
25
26
 
26
27
  === Usage
27
- A +thin+ script is offered to easily start your Rails application:
28
+ A +thin+ script offers an easy way to start your Rails application:
28
29
 
29
30
  cd to/your/rails/app
30
31
  thin start
31
32
 
32
- But Thin is also usable through Rack +rackup+ command.
33
- You need to setup a config.ru file and require thin in it:
33
+ But Thin is also usable with a Rack config file.
34
+ You need to setup a config.ru file and pass it to the thin script:
34
35
 
35
- cat <<EOS
36
- require 'thin'
37
-
36
+ cat config.ru
38
37
  app = proc do |env|
39
38
  [
40
39
  200,
@@ -47,10 +46,10 @@ You need to setup a config.ru file and require thin in it:
47
46
  end
48
47
 
49
48
  run app
50
- EOS
51
- rackup -s thin
49
+
50
+ thin start -r config.ru
52
51
 
53
- See example/config.ru for details and rackup -h
52
+ See example directory for more samples and run 'thin -h' for usage.
54
53
 
55
54
  === License
56
55
  Ruby License, http://www.ruby-lang.org/en/LICENSE.txt.
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+ # Automate benchmarking with ab with various concurrency levels.
3
+ require 'optparse'
4
+
5
+ options = {
6
+ :address => '0.0.0.0',
7
+ :port => 3000,
8
+ :requests => 1000,
9
+ :start => 1,
10
+ :end => 100,
11
+ :step => 10
12
+ }
13
+
14
+ OptionParser.new do |opts|
15
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
16
+
17
+ opts.on("-n", "--requests NUM", "Number of requests") { |num| options[:requests] = num }
18
+ opts.on("-a", "--address HOST", "Address (default: 0.0.0.0)") { |host| options[:address] = host }
19
+ opts.on("-p", "--port PORT", "use PORT (default: 3000)") { |port| options[:port] = port.to_i }
20
+ opts.on("-s", "--start N", "First concurrency level") { |n| options[:start] = n.to_i }
21
+ opts.on("-e", "--end N", "Last concurrency level") { |n| options[:end] = n.to_i }
22
+ opts.on("-S", "--step N", "Concurrency level step") { |n| options[:step] = n.to_i }
23
+ opts.on("-u", "--uri PATH", "Path to send to") { |u| options[:uri] = u }
24
+ opts.on("-k", "--keep-alive", "Use Keep-Alive") { options[:keep_alive] = true }
25
+
26
+ opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
27
+ end.parse!(ARGV)
28
+
29
+ puts 'request concurrency req/s failures'
30
+ puts '=' * 42
31
+
32
+ c = options[:start]
33
+ until c >= options[:end]
34
+ sleep 0.5
35
+ out = `nice -n20 ab #{'-k' if options[:keep_alive]} -c #{c} -n #{options[:requests]} #{options[:address]}:#{options[:port]}/#{options[:uri]} 2> /dev/null`
36
+
37
+ r = if requests = out.match(/^Requests.+?(\d+\.\d+)/)
38
+ requests[1].to_i
39
+ else
40
+ 0
41
+ end
42
+ f = if requests = out.match(/^Failed requests.+?(\d+)/)
43
+ requests[1].to_i
44
+ else
45
+ 0
46
+ end
47
+
48
+ puts "#{options[:requests].to_s.ljust(9)} #{c.to_s.ljust(13)} #{r.to_s.ljust(8)} #{f}"
49
+
50
+ c += options[:step]
51
+ end
@@ -0,0 +1,80 @@
1
+ require 'rack/lobster'
2
+
3
+ class Benchmarker
4
+ PORT = 7000
5
+ ADDRESS = '0.0.0.0'
6
+
7
+ attr_accessor :requests, :concurrencies, :servers, :keep_alive
8
+
9
+ def initialize
10
+ @servers = %w(Mongrel EMongrel Thin)
11
+ @requests = 1000
12
+ @concurrencies = [1, 10, 100]
13
+ end
14
+
15
+ def writer(&block)
16
+ @writer = block
17
+ end
18
+
19
+ def run!
20
+ @concurrencies.each do |concurrency|
21
+ @servers.each do |server|
22
+ req_sec, failed = run_one(server, concurrency)
23
+ @writer.call(server, @requests, concurrency, req_sec, failed)
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+ def start_server(handler_name)
30
+ @server = fork do
31
+ [STDOUT, STDERR].each { |o| o.reopen "/dev/null" }
32
+
33
+ case handler_name
34
+ when 'EMongrel'
35
+ require 'swiftcore/evented_mongrel'
36
+ handler_name = 'Mongrel'
37
+ end
38
+
39
+ app = proc do |env|
40
+ [200, {'Content-Type' => 'text/html', 'Content-Length' => '11'}, ['hello world']]
41
+ end
42
+
43
+ handler = Rack::Handler.const_get(handler_name)
44
+ handler.run app, :Host => ADDRESS, :Port => PORT
45
+ end
46
+
47
+ sleep 2
48
+ end
49
+
50
+ def stop_server
51
+ Process.kill('SIGKILL', @server)
52
+ Process.wait
53
+ end
54
+
55
+ def run_ab(concurrency)
56
+ `nice -n20 ab -c #{concurrency} -n #{@requests} #{@keep_alive ? '-k' : ''} #{ADDRESS}:#{PORT}/ 2> /dev/null`
57
+ end
58
+
59
+ def run_one(handler_name, concurrency)
60
+ start_server(handler_name)
61
+
62
+ out = run_ab(concurrency)
63
+
64
+ stop_server
65
+
66
+ req_sec = if matches = out.match(/^Requests.+?(\d+\.\d+)/)
67
+ matches[1].to_i
68
+ else
69
+ 0
70
+ end
71
+
72
+ failed = if matches = out.match(/^Failed requests.+?(\d+)/)
73
+ matches[1].to_i
74
+ else
75
+ 0
76
+ end
77
+
78
+ [req_sec, failed]
79
+ end
80
+ end
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env ruby
2
+ # Simple benchmark to compare Thin performance against
3
+ # other webservers supported by Rack.
4
+ #
5
+ # Run with:
6
+ #
7
+ # ruby simple.rb [num of request] [print|graph] [concurrency levels]
8
+ #
9
+ require File.dirname(__FILE__) + '/../lib/thin'
10
+ require File.dirname(__FILE__) + '/benchmarker'
11
+ require 'optparse'
12
+
13
+ options = {
14
+ :requests => 1000,
15
+ :concurrencies => [1, 10, 100],
16
+ :keep_alive => false,
17
+ :output => :table
18
+ }
19
+
20
+ OptionParser.new do |opts|
21
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
22
+
23
+ opts.on("-n", "--requests NUM", "Number of requests") { |num| options[:requests] = num.to_i }
24
+ opts.on("-c", "--concurrencies EXP", "Concurrency levels") { |exp| options[:concurrencies] = eval(exp).to_a }
25
+ opts.on("-k", "--keep-alive", "Use persistent connections") { options[:keep_alive] = true }
26
+ opts.on("-t", "--table", "Output as text table") { options[:output] = :table }
27
+ opts.on("-g", "--graph", "Output as graph") { options[:output] = :graph }
28
+
29
+ opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
30
+ end.parse!(ARGV)
31
+
32
+ # benchmark output_type, %w(WEBrick Mongrel EMongrel Thin), request, levels
33
+ b = Benchmarker.new
34
+ b.requests = options[:requests]
35
+ b.concurrencies = options[:concurrencies]
36
+ b.keep_alive = options[:keep_alive]
37
+
38
+ case options[:output]
39
+ when :table
40
+ puts 'server request concurrency req/s failures'
41
+ puts '=' * 52
42
+
43
+ b.writer do |server, requests, concurrency, req_sec, failed|
44
+ puts "#{server.ljust(8)} #{requests} #{concurrency.to_s.ljust(4)} #{req_sec.to_s.ljust(8)} #{failed}"
45
+ end
46
+
47
+ b.run!
48
+
49
+ when :graph
50
+ require '/usr/local/lib/ruby/gems/1.8/gems/gruff-0.2.9/lib/gruff'
51
+ g = Gruff::Area.new
52
+ g.title = "#{options[:requests]} requests"
53
+ g.title << ' w/ Keep-Alive' if options[:keep_alive]
54
+
55
+ g.x_axis_label = 'Concurrency'
56
+ g.y_axis_label = 'Requests / sec'
57
+ g.maximum_value = 0
58
+ g.minimum_value = 0
59
+ g.labels = {}
60
+ b.concurrencies.each_with_index { |c, i| g.labels[i] = c.to_s }
61
+
62
+ results = {}
63
+
64
+ b.writer do |server, requests, concurrency, req_sec, failed|
65
+ print '.'
66
+ results[server] ||= []
67
+ results[server] << req_sec
68
+ end
69
+
70
+ b.run!
71
+ puts
72
+
73
+ results.each do |server, concurrencies|
74
+ g.data(server, concurrencies)
75
+ end
76
+
77
+ g.write('bench.png')
78
+ `open bench.png`
79
+ end
@@ -9,8 +9,8 @@ class SimpleAdapter
9
9
  [
10
10
  200,
11
11
  {
12
- 'Content-Type' => 'text/plain',
13
- 'Content-Type' => body.join.size.to_s
12
+ 'Content-Type' => 'text/plain',
13
+ 'Content-Length' => body.join.size.to_s,
14
14
  },
15
15
  body
16
16
  ]
@@ -32,4 +32,4 @@ end
32
32
  # app = Rack::URLMap.new('/test' => SimpleAdapter.new,
33
33
  # '/files' => Rack::File.new('.'))
34
34
  # Thin::Server.new('0.0.0.0', 3000, app).start
35
- #
35
+ #
@@ -1,6 +1,6 @@
1
1
  # == God config file
2
2
  # http://god.rubyforge.org/
3
- # Author: Gump
3
+ # Authors: Gump and michael@glauche.de
4
4
  #
5
5
  # Config file for god that configures watches for each instance of a thin server for
6
6
  # each thin configuration file found in /etc/thin.
@@ -16,28 +16,32 @@ Dir[config_path + "/*.yml"].each do |file|
16
16
  config = YAML.load_file(file)
17
17
  num_servers = config["servers"] ||= 1
18
18
 
19
- for i in 0...num_servers
19
+ (0...num_servers).each do |i|
20
+ # UNIX socket cluster use number 0 to 2 (for 3 servers)
21
+ # and tcp cluster use port number 3000 to 3002.
22
+ number = config['socket'] ? i : (config['port'] + i)
23
+
20
24
  God.watch do |w|
21
25
  w.group = "thin-" + File.basename(file, ".yml")
22
- w.name = w.group + "-#{i}"
26
+ w.name = w.group + "-#{number}"
23
27
 
24
28
  w.interval = 30.seconds
25
29
 
26
30
  w.uid = config["user"]
27
31
  w.gid = config["group"]
28
32
 
29
- w.start = "thin start -C #{file} -o #{i}"
33
+ w.start = "thin start -C #{file} -o #{number}"
30
34
  w.start_grace = 10.seconds
31
35
 
32
- w.stop = "thin stop -C #{file} -o #{i}"
36
+ w.stop = "thin stop -C #{file} -o #{number}"
33
37
  w.stop_grace = 10.seconds
34
38
 
35
- w.restart = "thin restart -C #{file} -o #{i}"
39
+ w.restart = "thin restart -C #{file} -o #{number}"
36
40
 
37
41
  pid_path = config["chdir"] + "/" + config["pid"]
38
42
  ext = File.extname(pid_path)
39
43
 
40
- w.pid_file = pid_path.gsub(/#{ext}$/, ".#{i}#{ext}")
44
+ w.pid_file = pid_path.gsub(/#{ext}$/, ".#{number}#{ext}")
41
45
 
42
46
  w.behavior(:clean_pid_file)
43
47
 
@@ -11,27 +11,28 @@ require 'thin/version'
11
11
  require 'thin/statuses'
12
12
 
13
13
  module Thin
14
- autoload :Command, 'thin/command'
15
- autoload :Connection, 'thin/connection'
16
- autoload :Daemonizable, 'thin/daemonizing'
17
- autoload :Logging, 'thin/logging'
18
- autoload :Headers, 'thin/headers'
19
- autoload :Request, 'thin/request'
20
- autoload :Response, 'thin/response'
21
- autoload :Runner, 'thin/runner'
22
- autoload :Server, 'thin/server'
23
- autoload :Stats, 'thin/stats'
14
+ autoload :Command, 'thin/command'
15
+ autoload :Connection, 'thin/connection'
16
+ autoload :Daemonizable, 'thin/daemonizing'
17
+ autoload :Logging, 'thin/logging'
18
+ autoload :Headers, 'thin/headers'
19
+ autoload :Request, 'thin/request'
20
+ autoload :Response, 'thin/response'
21
+ autoload :Runner, 'thin/runner'
22
+ autoload :Server, 'thin/server'
23
+ autoload :Stats, 'thin/stats'
24
24
 
25
25
  module Connectors
26
- autoload :Connector, 'thin/connectors/connector'
27
- autoload :TcpServer, 'thin/connectors/tcp_server'
28
- autoload :UnixServer, 'thin/connectors/unix_server'
26
+ autoload :Connector, 'thin/connectors/connector'
27
+ autoload :SwiftiplyClient, 'thin/connectors/swiftiply_client'
28
+ autoload :TcpServer, 'thin/connectors/tcp_server'
29
+ autoload :UnixServer, 'thin/connectors/unix_server'
29
30
  end
30
31
 
31
32
  module Controllers
32
- autoload :Cluster, 'thin/controllers/cluster'
33
- autoload :Controller, 'thin/controllers/controller'
34
- autoload :Service, 'thin/controllers/service'
33
+ autoload :Cluster, 'thin/controllers/cluster'
34
+ autoload :Controller, 'thin/controllers/controller'
35
+ autoload :Service, 'thin/controllers/service'
35
36
  end
36
37
  end
37
38
 
@@ -1,3 +1,5 @@
1
+ require 'open3'
2
+
1
3
  module Thin
2
4
  # Run a command though the +thin+ command-line script.
3
5
  class Command
@@ -22,16 +24,20 @@ module Thin
22
24
  def run
23
25
  shell_cmd = shellify
24
26
  trace shell_cmd
25
- ouput = `#{shell_cmd}`.chomp
26
- log " " + ouput.gsub("\n", " \n") unless ouput.empty?
27
+ trap('INT') {} # Ignore INT signal to pass CTRL+C to subprocess
28
+ Open3.popen3(shell_cmd) do |stdin, stdout, stderr|
29
+ log stdout.gets until stdout.eof?
30
+ log stderr.gets until stderr.eof?
31
+ end
27
32
  end
28
33
 
29
34
  # Turn into a runnable shell command
30
35
  def shellify
31
36
  shellified_options = @options.inject([]) do |args, (name, value)|
32
37
  args << case value
33
- when NilClass
34
- when TrueClass then "--#{name}"
38
+ when NilClass,
39
+ TrueClass then "--#{name}"
40
+ when FalseClass
35
41
  else "--#{name.to_s.tr('_', '-')}=#{value.inspect}"
36
42
  end
37
43
  end
@@ -2,6 +2,8 @@ require 'socket'
2
2
 
3
3
  module Thin
4
4
  # Connection between the server and client.
5
+ # This class is instanciated by EventMachine on each new connection
6
+ # that is opened.
5
7
  class Connection < EventMachine::Connection
6
8
  include Logging
7
9
 
@@ -11,62 +13,93 @@ module Thin
11
13
  # Connector to the server
12
14
  attr_accessor :connector
13
15
 
14
- attr_accessor :request, :response
16
+ # Current request served by the connection
17
+ attr_accessor :request
15
18
 
19
+ # Next response sent through connection
20
+ attr_accessor :response
21
+
22
+ # Get the connection ready to process a request.
16
23
  def post_init
17
24
  @request = Request.new
18
25
  @response = Response.new
19
26
  end
20
27
 
28
+ # Called when data is received from the client.
21
29
  def receive_data(data)
22
30
  trace { data }
23
31
  process if @request.parse(data)
24
32
  rescue InvalidRequest => e
25
- log "Invalid request"
33
+ log "!! Invalid request"
26
34
  log_error e
27
35
  close_connection
28
36
  end
29
37
 
38
+ # Called when all data was received and the request
39
+ # is ready to being processed.
30
40
  def process
31
41
  # Add client info to the request env
32
- @request.env[Request::REMOTE_ADDR] = remote_address
42
+ @request.remote_address = remote_address
33
43
 
34
44
  # Process the request
35
45
  @response.status, @response.headers, @response.body = @app.call(@request.env)
36
46
 
47
+ # Make the response persistent if requested by the client
48
+ @response.persistent! if @request.persistent?
49
+
37
50
  # Send the response
38
51
  @response.each do |chunk|
39
52
  trace { chunk }
40
53
  send_data chunk
41
54
  end
42
55
 
43
- close_connection_after_writing
56
+ # If no more request on that same connection, we close it.
57
+ close_connection_after_writing unless persistent?
44
58
 
45
- rescue Object => e
46
- log "Unexpected error while processing request: #{e.message}"
47
- log_error e
59
+ rescue
60
+ log "!! Unexpected error while processing request: #{$!.message}"
61
+ log_error
48
62
  close_connection rescue nil
49
63
  ensure
50
64
  @request.close rescue nil
51
65
  @response.close rescue nil
66
+
67
+ # Prepare the connection for another request if the client
68
+ # supports HTTP pipelining (persistent connection).
69
+ post_init if persistent?
52
70
  end
53
71
 
72
+ # Called when the connection is unbinded from the socket
73
+ # and can no longer be used to process requests.
54
74
  def unbind
55
75
  @connector.connection_finished(self)
56
76
  end
57
77
 
78
+ # Allows this connection to be persistent.
79
+ def can_persist!
80
+ @can_persist = true
81
+ end
82
+
83
+ # Return +true+ if this connection is allowed to stay open and be persistent.
84
+ def can_persist?
85
+ @can_persist
86
+ end
87
+
88
+ # Return +true+ if the connection must be left open
89
+ # and ready to be reused for another request.
90
+ def persistent?
91
+ @can_persist && @response.persistent?
92
+ end
93
+
94
+ # IP Address of the remote client.
58
95
  def remote_address
59
- @request.env[Request::FORWARDED_FOR] || (has_peername? ? socket_address : nil)
96
+ @request.forwarded_for || socket_address
60
97
  rescue
61
- log_error($!)
98
+ log_error
62
99
  nil
63
100
  end
64
101
 
65
102
  protected
66
- def has_peername?
67
- !get_peername.nil? && !get_peername.empty?
68
- end
69
-
70
103
  def socket_address
71
104
  Socket.unpack_sockaddr_in(get_peername)[1]
72
105
  end