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
@@ -4,27 +4,32 @@ module Thin
4
4
  # * connection/disconnection to the server
5
5
  # * initialization of the connections
6
6
  # * manitoring of the active connections.
7
- class Connector
8
- include Logging
9
-
7
+ class Connector
10
8
  # Server serving the connections throught the connector
11
- attr_reader :server
9
+ attr_accessor :server
12
10
 
13
11
  # Maximum time for incoming data to arrive
14
12
  attr_accessor :timeout
15
13
 
14
+ # Maximum number of connections that can be persistent
15
+ attr_accessor :maximum_persistent_connections
16
+
17
+ # Number of persistent connections currently opened
18
+ attr_accessor :persistent_connection_count
19
+
16
20
  def initialize
17
- @connections = []
18
- @timeout = 60 # sec
21
+ @connections = []
22
+ @timeout = Server::DEFAULT_TIMEOUT
23
+ @persistent_connection_count = 0
24
+ @maximum_persistent_connections = Server::DEFAULT_MAXIMUM_PERSISTENT_CONNECTIONS
19
25
  end
20
26
 
21
27
  # Free up resources used by the connector.
22
28
  def close
23
29
  end
24
30
 
25
- def server=(server)
26
- @server = server
27
- @silent = @server.silent
31
+ def running?
32
+ @server.running?
28
33
  end
29
34
 
30
35
  # Initialize a new connection to a client.
@@ -32,7 +37,13 @@ module Thin
32
37
  connection.connector = self
33
38
  connection.app = @server.app
34
39
  connection.comm_inactivity_timeout = @timeout
35
- connection.silent = @silent
40
+
41
+ # We control the number of persistent connections by keeping
42
+ # a count of the total one allowed yet.
43
+ if @persistent_connection_count < @maximum_persistent_connections
44
+ connection.can_persist!
45
+ @persistent_connection_count += 1
46
+ end
36
47
 
37
48
  @connections << connection
38
49
  end
@@ -44,6 +55,7 @@ module Thin
44
55
 
45
56
  # Called by a connection when it's unbinded.
46
57
  def connection_finished(connection)
58
+ @persistent_connection_count -= 1 if connection.can_persist?
47
59
  @connections.delete(connection)
48
60
  end
49
61
 
@@ -0,0 +1,55 @@
1
+ module Thin
2
+ module Connectors
3
+ class SwiftiplyClient < Connector
4
+ attr_accessor :key
5
+
6
+ attr_accessor :host, :port
7
+
8
+ def initialize(host, port, key=nil)
9
+ @host = host
10
+ @port = port.to_i
11
+ @key = key || ''
12
+ super()
13
+ end
14
+
15
+ # Connect the server
16
+ def connect
17
+ EventMachine.connect(@host, @port, SwiftiplyConnection, &method(:initialize_connection))
18
+ end
19
+
20
+ # Stops the server
21
+ def disconnect
22
+ EventMachine.stop
23
+ end
24
+
25
+ def to_s
26
+ "#{@host}:#{@port} swiftiply"
27
+ end
28
+ end
29
+ end
30
+
31
+ class SwiftiplyConnection < Connection
32
+ def connection_completed
33
+ send_data swiftiply_handshake(@connector.key)
34
+ end
35
+
36
+ def persistent?
37
+ true
38
+ end
39
+
40
+ def unbind
41
+ super
42
+ EventMachine.add_timer(rand(2)) { reconnect(@connector.host, @connector.port) } if @connector.running?
43
+ end
44
+
45
+ protected
46
+ def swiftiply_handshake(key)
47
+ 'swiftclient' << host_ip.collect { |x| sprintf('%02x', x.to_i)}.join << sprintf('%04x', @connector.port) << sprintf('%02x', key.length) << key
48
+ end
49
+
50
+ # For some reason Swiftiply request the current host
51
+ def host_ip
52
+ Socket.gethostbyname(@connector.host)[3].unpack('CCCC') rescue [0,0,0,0]
53
+ end
54
+ end
55
+ end
@@ -5,19 +5,15 @@ module Thin
5
5
  # * Inject the port or socket number in the pid and log filenames.
6
6
  # Servers are started throught the +thin+ command-line script.
7
7
  class Cluster < Controller
8
- # Number of servers in the cluster.
9
- attr_accessor :size
10
-
8
+ # Cluster only options that should not be passed in the command sent
9
+ # to the indiviual servers.
10
+ CLUSTER_OPTIONS = [:servers, :only]
11
+
11
12
  # Create a new cluster of servers launched using +options+.
12
13
  def initialize(options)
13
- @options = options.merge(:daemonize => true)
14
- @size = @options.delete(:servers)
15
- @only = @options.delete(:only)
16
-
17
- if socket
18
- @options.delete(:address)
19
- @options.delete(:port)
20
- end
14
+ super
15
+ # Cluster can only contain daemonized servers
16
+ @options.merge!(:daemonize => true)
21
17
  end
22
18
 
23
19
  def first_port; @options[:port] end
@@ -25,17 +21,23 @@ module Thin
25
21
  def socket; @options[:socket] end
26
22
  def pid_file; @options[:pid] end
27
23
  def log_file; @options[:log] end
24
+ def size; @options[:servers] end
25
+ def only; @options[:only] end
26
+
27
+ def swiftiply?
28
+ @options.has_key?(:swiftiply)
29
+ end
28
30
 
29
31
  # Start the servers
30
32
  def start
31
- with_each_server { |port| start_server port }
33
+ with_each_server { |n| start_server n }
32
34
  end
33
35
 
34
36
  # Start a single server
35
37
  def start_server(number)
36
38
  log "Starting server on #{server_id(number)} ... "
37
39
 
38
- run :start, @options, number
40
+ run :start, number
39
41
  end
40
42
 
41
43
  # Stop the servers
@@ -47,7 +49,7 @@ module Thin
47
49
  def stop_server(number)
48
50
  log "Stopping server on #{server_id(number)} ... "
49
51
 
50
- run :stop, @options, number
52
+ run :stop, number
51
53
  end
52
54
 
53
55
  # Stop and start the servers.
@@ -60,6 +62,8 @@ module Thin
60
62
  def server_id(number)
61
63
  if socket
62
64
  socket_for(number)
65
+ elsif swiftiply?
66
+ [address, first_port, number].join(':')
63
67
  else
64
68
  [address, number].join(':')
65
69
  end
@@ -80,14 +84,16 @@ module Thin
80
84
  def pid_for(number)
81
85
  File.read(pid_file_for(number)).chomp.to_i
82
86
  end
83
-
87
+
84
88
  private
85
89
  # Send the command to the +thin+ script
86
- def run(cmd, options, number)
87
- cmd_options = options.dup
90
+ def run(cmd, number)
91
+ cmd_options = @options.reject { |option, value| CLUSTER_OPTIONS.include?(option) }
88
92
  cmd_options.merge!(:pid => pid_file_for(number), :log => log_file_for(number))
89
93
  if socket
90
94
  cmd_options.merge!(:socket => socket_for(number))
95
+ elsif swiftiply?
96
+ cmd_options.merge!(:port => first_port)
91
97
  else
92
98
  cmd_options.merge!(:port => number)
93
99
  end
@@ -95,12 +101,12 @@ module Thin
95
101
  end
96
102
 
97
103
  def with_each_server
98
- if @only
99
- yield @only
104
+ if only
105
+ yield only
106
+ elsif socket || swiftiply?
107
+ size.times { |n| yield n }
100
108
  else
101
- @size.times do |n|
102
- yield socket ? n : (first_port + n)
103
- end
109
+ size.times { |n| yield first_port + n }
104
110
  end
105
111
  end
106
112
 
@@ -19,30 +19,41 @@ module Thin
19
19
 
20
20
  def initialize(options)
21
21
  @options = options
22
+
23
+ if @options[:socket]
24
+ @options.delete(:address)
25
+ @options.delete(:port)
26
+ end
22
27
  end
23
28
 
24
29
  def start
25
- if @options[:socket]
26
- server = Server.new(@options[:socket])
30
+ server = case
31
+ when @options.has_key?(:socket)
32
+ Server.new(@options[:socket])
33
+ when @options.has_key?(:swiftiply)
34
+ Server.new(Connectors::SwiftiplyClient.new(@options[:address], @options[:port], @options[:swiftiply]))
27
35
  else
28
- server = Server.new(@options[:address], @options[:port])
36
+ Server.new(@options[:address], @options[:port])
29
37
  end
30
38
 
31
- server.pid_file = @options[:pid]
32
- server.log_file = @options[:log]
33
- server.timeout = @options[:timeout]
39
+ server.pid_file = @options[:pid]
40
+ server.log_file = @options[:log]
41
+ server.timeout = @options[:timeout]
42
+ server.maximum_connections = @options[:max_conns]
43
+ server.maximum_persistent_connections = @options[:max_persistent_conns]
34
44
 
35
- if @options[:daemonize]
36
- server.daemonize
37
- server.change_privilege @options[:user], @options[:group] if @options[:user] && @options[:group]
38
- end
45
+ server.daemonize if @options[:daemonize]
46
+
47
+ # Must be called before changing privileges since it might require superuser power.
48
+ server.set_descriptor_table_size!
49
+ server.change_privilege @options[:user], @options[:group] if @options[:user] && @options[:group]
39
50
 
40
51
  # If a Rack config file is specified we eval it inside a Rack::Builder block to create
41
52
  # a Rack adapter from it. DHH was hacker of the year a couple years ago so we default
42
53
  # to Rails adapter.
43
54
  if @options[:rackup]
44
55
  rackup_code = File.read(@options[:rackup])
45
- server.app = eval("Rack::Builder.new {( #{rackup_code}\n )}.to_app", nil, @options[:rackup])
56
+ server.app = eval("Rack::Builder.new {( #{rackup_code}\n )}.to_app", TOPLEVEL_BINDING, @options[:rackup])
46
57
  else
47
58
  server.app = Rack::Adapter::Rails.new(@options.merge(:root => @options[:chdir]))
48
59
  end
@@ -50,7 +61,7 @@ module Thin
50
61
  # If a prefix is required, wrap in Rack URL mapper
51
62
  server.app = Rack::URLMap.new(@options[:prefix] => server.app) if @options[:prefix]
52
63
 
53
- # If a stats are required, wrap in Stats adapter
64
+ # If a stats URL is specified, wrap in Stats adapter
54
65
  server.app = Stats::Adapter.new(server.app, @options[:stats]) if @options[:stats]
55
66
 
56
67
  # Register restart procedure
@@ -62,13 +73,19 @@ module Thin
62
73
  def stop
63
74
  raise OptionRequired, :pid unless @options[:pid]
64
75
 
65
- Server.kill(@options[:pid], @options[:timeout] || 60)
76
+ tail_log(@options[:log]) do
77
+ Server.kill(@options[:pid], @options[:timeout] || 60)
78
+ wait_for_file :deletion, @options[:pid]
79
+ end
66
80
  end
67
81
 
68
82
  def restart
69
83
  raise OptionRequired, :pid unless @options[:pid]
70
84
 
71
- Server.restart(@options[:pid])
85
+ tail_log(@options[:log]) do
86
+ Server.restart(@options[:pid])
87
+ wait_for_file :creation, @options[:pid]
88
+ end
72
89
  end
73
90
 
74
91
  def config
@@ -80,6 +97,49 @@ module Thin
80
97
  File.open(config_file, 'w') { |f| f << @options.to_yaml }
81
98
  log ">> Wrote configuration to #{config_file}"
82
99
  end
100
+
101
+ protected
102
+ # Wait for a pid file to either be created or deleted.
103
+ def wait_for_file(state, file)
104
+ case state
105
+ when :creation then sleep 0.1 until File.exist?(file)
106
+ when :deletion then sleep 0.1 while File.exist?(file)
107
+ end
108
+ end
109
+
110
+ # Tail the log file of server +number+ during the execution of the block.
111
+ def tail_log(log_file)
112
+ if log_file
113
+ tail_thread = tail(log_file)
114
+ yield
115
+ tail_thread.kill
116
+ else
117
+ yield
118
+ end
119
+ end
120
+
121
+ # Acts like GNU tail command. Taken from Rails.
122
+ def tail(file)
123
+ tail_thread = Thread.new do
124
+ Thread.pass until File.exist?(file)
125
+ cursor = File.size(file)
126
+ last_checked = Time.now
127
+ File.open(file, 'r') do |f|
128
+ loop do
129
+ f.seek cursor
130
+ if f.mtime > last_checked
131
+ last_checked = f.mtime
132
+ contents = f.read
133
+ cursor += contents.length
134
+ print contents
135
+ STDOUT.flush
136
+ end
137
+ sleep 0.1
138
+ end
139
+ end
140
+ end
141
+ tail_thread
142
+ end
83
143
  end
84
144
  end
85
145
  end
@@ -10,7 +10,7 @@ module Thin
10
10
  TEMPLATE = File.dirname(__FILE__) + '/service.sh.erb'
11
11
 
12
12
  def initialize(options)
13
- @options = options
13
+ super
14
14
 
15
15
  raise PlatformNotSupported, 'Running as a service only supported on Linux' unless Thin.linux?
16
16
  end
@@ -1,5 +1,5 @@
1
1
  require 'etc'
2
- require 'daemons'
2
+ require 'daemons' unless Thin.win?
3
3
 
4
4
  module Process
5
5
  # Returns +true+ the process identied by +pid+ is running.
@@ -89,17 +89,19 @@ module Thin
89
89
  end
90
90
 
91
91
  module ClassMethods
92
- # Send a INT signal the process which PID is stored in +pid_file+.
92
+ # Send a QUIT signal the process which PID is stored in +pid_file+.
93
93
  # If the process is still running after +timeout+, KILL signal is
94
94
  # sent.
95
95
  def kill(pid_file, timeout=60)
96
- if pid = send_signal('INT', pid_file)
96
+ if pid = send_signal('QUIT', pid_file)
97
97
  begin
98
98
  Timeout.timeout(timeout) do
99
99
  sleep 0.1 while Process.running?(pid)
100
100
  end
101
101
  rescue Timeout::Error
102
- print "timeout! "
102
+ print "Timeout! "
103
+ send_signal('KILL', pid_file)
104
+ rescue Interrupt
103
105
  send_signal('KILL', pid_file)
104
106
  end
105
107
  end
@@ -20,6 +20,10 @@ module Thin
20
20
  end
21
21
  end
22
22
 
23
+ def has_key?(key)
24
+ @sent[key]
25
+ end
26
+
23
27
  def to_s
24
28
  @out.join
25
29
  end
@@ -1,24 +1,49 @@
1
1
  module Thin
2
- # To be included into classes to allow some basic logging
3
- # that can be silented (+silent+) or made more verbose ($DEBUG=true).
2
+ # To be included in classes to allow some basic logging
3
+ # that can be silented (<tt>Logging.silent=</tt>) or made
4
+ # more verbose.
5
+ # <tt>Logging.debug=</tt>: log all error backtrace and messages
6
+ # logged with +debug+.
7
+ # <tt>Logging.trace=</tt>: log all raw request and response and
8
+ # messages logged with +trace+.
4
9
  module Logging
5
- # Don't output any message if +true+.
6
- attr_accessor :silent
10
+ class << self
11
+ attr_writer :trace, :debug, :silent
12
+
13
+ def trace?; !@silent && @trace end
14
+ def debug?; !@silent && @debug end
15
+ def silent?; @silent end
16
+ end
17
+
18
+ # Deprecated silencer methods, those are now a module methods
19
+ def silent
20
+ warn "`#{self.class.name}\#silent` deprecated, use `Thin::Logging.silent?` instead"
21
+ Logging.silent?
22
+ end
23
+ def silent=(value)
24
+ warn "`#{self.class.name}\#silent=` deprecated, use `Thin::Logging.silent = #{value}` instead"
25
+ Logging.silent = value
26
+ end
7
27
 
8
28
  protected
9
29
  # Log a message to the console
10
30
  def log(msg)
11
- puts msg unless @silent
31
+ puts msg unless Logging.silent?
12
32
  end
13
33
 
14
34
  # Log a message to the console if tracing is activated
15
35
  def trace(msg=nil)
16
- puts msg || yield if ($DEBUG || $TRACE) && !@silent
36
+ log msg || yield if Logging.trace?
37
+ end
38
+
39
+ # Log a message to the console if debugging is activated
40
+ def debug(msg=nil)
41
+ log msg || yield if Logging.debug?
17
42
  end
18
43
 
19
- # Log an error backtrace if tracing is activated
20
- def log_error(e)
21
- trace { "#{e}\n\t" + e.backtrace.join("\n\t") }
44
+ # Log an error backtrace if debugging is activated
45
+ def log_error(e=$!)
46
+ debug "#{e}\n\t" + e.backtrace.join("\n\t")
22
47
  end
23
48
  end
24
49
  end