gross 1.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +410 -0
  3. data/README.md +102 -0
  4. data/Rakefile +25 -0
  5. data/bin/thin +6 -0
  6. data/example/adapter.rb +32 -0
  7. data/example/async_app.ru +126 -0
  8. data/example/async_chat.ru +247 -0
  9. data/example/async_tailer.ru +100 -0
  10. data/example/config.ru +22 -0
  11. data/example/monit_sockets +20 -0
  12. data/example/monit_unixsock +20 -0
  13. data/example/myapp.rb +1 -0
  14. data/example/ramaze.ru +12 -0
  15. data/example/thin.god +80 -0
  16. data/example/thin_solaris_smf.erb +36 -0
  17. data/example/thin_solaris_smf.readme.txt +150 -0
  18. data/example/vlad.rake +72 -0
  19. data/ext/thin_parser/common.rl +59 -0
  20. data/ext/thin_parser/ext_help.h +14 -0
  21. data/ext/thin_parser/extconf.rb +6 -0
  22. data/ext/thin_parser/parser.c +1447 -0
  23. data/ext/thin_parser/parser.h +49 -0
  24. data/ext/thin_parser/parser.rl +152 -0
  25. data/ext/thin_parser/thin.c +435 -0
  26. data/lib/rack/adapter/loader.rb +75 -0
  27. data/lib/rack/adapter/rails.rb +178 -0
  28. data/lib/thin.rb +45 -0
  29. data/lib/thin/backends/base.rb +167 -0
  30. data/lib/thin/backends/swiftiply_client.rb +56 -0
  31. data/lib/thin/backends/tcp_server.rb +34 -0
  32. data/lib/thin/backends/unix_server.rb +56 -0
  33. data/lib/thin/command.rb +53 -0
  34. data/lib/thin/connection.rb +215 -0
  35. data/lib/thin/controllers/cluster.rb +178 -0
  36. data/lib/thin/controllers/controller.rb +189 -0
  37. data/lib/thin/controllers/service.rb +76 -0
  38. data/lib/thin/controllers/service.sh.erb +39 -0
  39. data/lib/thin/daemonizing.rb +180 -0
  40. data/lib/thin/headers.rb +40 -0
  41. data/lib/thin/logging.rb +174 -0
  42. data/lib/thin/request.rb +162 -0
  43. data/lib/thin/response.rb +117 -0
  44. data/lib/thin/runner.rb +238 -0
  45. data/lib/thin/server.rb +290 -0
  46. data/lib/thin/stats.html.erb +216 -0
  47. data/lib/thin/stats.rb +52 -0
  48. data/lib/thin/statuses.rb +44 -0
  49. data/lib/thin/version.rb +32 -0
  50. metadata +156 -0
@@ -0,0 +1,56 @@
1
+ module Thin
2
+ module Backends
3
+ # Backend to act as a Swiftiply client (http://swiftiply.swiftcore.org).
4
+ class SwiftiplyClient < Base
5
+ attr_accessor :key
6
+
7
+ attr_accessor :host, :port
8
+
9
+ def initialize(host, port, options = {})
10
+ @host = host
11
+ @port = port.to_i
12
+ @key = options[:swiftiply].to_s
13
+ super()
14
+ end
15
+
16
+ # Connect the server
17
+ def connect
18
+ EventMachine.connect(@host, @port, SwiftiplyConnection, &method(:initialize_connection))
19
+ end
20
+
21
+ # Stops the server
22
+ def disconnect
23
+ EventMachine.stop
24
+ end
25
+
26
+ def to_s
27
+ "#{@host}:#{@port} swiftiply"
28
+ end
29
+ end
30
+ end
31
+
32
+ class SwiftiplyConnection < Connection
33
+ def connection_completed
34
+ send_data swiftiply_handshake(@backend.key)
35
+ end
36
+
37
+ def persistent?
38
+ true
39
+ end
40
+
41
+ def unbind
42
+ super
43
+ EventMachine.add_timer(rand(2)) { reconnect(@backend.host, @backend.port) } if @backend.running?
44
+ end
45
+
46
+ protected
47
+ def swiftiply_handshake(key)
48
+ 'swiftclient' << host_ip.collect { |x| sprintf('%02x', x.to_i) }.join << sprintf('%04x', @backend.port) << sprintf('%02x', key.length) << key
49
+ end
50
+
51
+ # For some reason Swiftiply request the current host
52
+ def host_ip
53
+ Socket.gethostbyname(@backend.host)[3].unpack('CCCC') rescue [0, 0, 0, 0]
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,34 @@
1
+ module Thin
2
+ module Backends
3
+ # Backend to act as a TCP socket server.
4
+ class TcpServer < Base
5
+ # Address and port on which the server is listening for connections.
6
+ attr_accessor :host, :port
7
+
8
+ def initialize(host, port)
9
+ @host = host
10
+ @port = port
11
+ super()
12
+ end
13
+
14
+ # Connect the server
15
+ def connect
16
+ @signature = EventMachine.start_server(@host, @port, Connection, &method(:initialize_connection))
17
+ binary_name = EventMachine.get_sockname( @signature )
18
+ port_name = Socket.unpack_sockaddr_in( binary_name )
19
+ @port = port_name[0]
20
+ @host = port_name[1]
21
+ @signature
22
+ end
23
+
24
+ # Stops the server
25
+ def disconnect
26
+ EventMachine.stop_server(@signature)
27
+ end
28
+
29
+ def to_s
30
+ "#{@host}:#{@port}"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,56 @@
1
+ module Thin
2
+ module Backends
3
+ # Backend to act as a UNIX domain socket server.
4
+ class UnixServer < Base
5
+ # UNIX domain socket on which the server is listening for connections.
6
+ attr_accessor :socket
7
+
8
+ def initialize(socket)
9
+ raise PlatformNotSupported, 'UNIX domain sockets not available on Windows' if Thin.win?
10
+ @socket = socket
11
+ super()
12
+ end
13
+
14
+ # Connect the server
15
+ def connect
16
+ at_exit { remove_socket_file } # In case it crashes
17
+ old_umask = File.umask(0)
18
+ begin
19
+ EventMachine.start_unix_domain_server(@socket, UnixConnection, &method(:initialize_connection))
20
+ # HACK EventMachine.start_unix_domain_server doesn't return the connection signature
21
+ # so we have to go in the internal stuff to find it.
22
+ @signature = EventMachine.instance_eval{@acceptors.keys.first}
23
+ ensure
24
+ File.umask(old_umask)
25
+ end
26
+ end
27
+
28
+ # Stops the server
29
+ def disconnect
30
+ EventMachine.stop_server(@signature)
31
+ end
32
+
33
+ # Free up resources used by the backend.
34
+ def close
35
+ remove_socket_file
36
+ end
37
+
38
+ def to_s
39
+ @socket
40
+ end
41
+
42
+ protected
43
+ def remove_socket_file
44
+ File.delete(@socket) if @socket && File.exist?(@socket)
45
+ end
46
+ end
47
+ end
48
+
49
+ # Connection through a UNIX domain socket.
50
+ class UnixConnection < Connection
51
+ protected
52
+ def socket_address
53
+ '127.0.0.1' # Unix domain sockets can only be local
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,53 @@
1
+ require 'open3'
2
+
3
+ module Thin
4
+ # Run a command through the +thin+ command-line script.
5
+ class Command
6
+ include Logging
7
+
8
+ class << self
9
+ # Path to the +thin+ script used to control the servers.
10
+ # Leave this to default to use the one in the path.
11
+ attr_accessor :script
12
+ end
13
+
14
+ def initialize(name, options={})
15
+ @name = name
16
+ @options = options
17
+ end
18
+
19
+ def self.run(*args)
20
+ new(*args).run
21
+ end
22
+
23
+ # Send the command to the +thin+ script
24
+ def run
25
+ shell_cmd = shellify
26
+ trace shell_cmd
27
+ trap('INT') {} # Ignore INT signal to pass CTRL+C to subprocess
28
+ Open3.popen3(shell_cmd) do |stdin, stdout, stderr|
29
+ log_info stdout.gets until stdout.eof?
30
+ log_info stderr.gets until stderr.eof?
31
+ end
32
+ end
33
+
34
+ # Turn into a runnable shell command
35
+ def shellify
36
+ shellified_options = @options.inject([]) do |args, (name, value)|
37
+ option_name = name.to_s.tr("_", "-")
38
+ case value
39
+ when NilClass,
40
+ TrueClass then args << "--#{option_name}"
41
+ when FalseClass
42
+ when Array then value.each { |v| args << "--#{option_name}=#{v.inspect}" }
43
+ else args << "--#{option_name}=#{value.inspect}"
44
+ end
45
+ args
46
+ end
47
+
48
+ raise ArgumentError, "Path to thin script can't be found, set Command.script" unless self.class.script
49
+
50
+ "#{self.class.script} #{@name} #{shellified_options.compact.join(' ')}"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,215 @@
1
+ require 'socket'
2
+
3
+ module Thin
4
+ # Connection between the server and client.
5
+ # This class is instanciated by EventMachine on each new connection
6
+ # that is opened.
7
+ class Connection < EventMachine::Connection
8
+ include Logging
9
+
10
+ # This is a template async response. N.B. Can't use string for body on 1.9
11
+ AsyncResponse = [-1, {}, []].freeze
12
+
13
+ # Rack application (adapter) served by this connection.
14
+ attr_accessor :app
15
+
16
+ # Backend to the server
17
+ attr_accessor :backend
18
+
19
+ # Current request served by the connection
20
+ attr_accessor :request
21
+
22
+ # Next response sent through the connection
23
+ attr_accessor :response
24
+
25
+ # Calling the application in a threaded allowing
26
+ # concurrent processing of requests.
27
+ attr_writer :threaded
28
+
29
+ # Get the connection ready to process a request.
30
+ def post_init
31
+ @request = Request.new
32
+ @response = Response.new
33
+ end
34
+
35
+ # Called when data is received from the client.
36
+ def receive_data(data)
37
+ @idle = false
38
+ trace data
39
+ process if @request.parse(data)
40
+ rescue InvalidRequest => e
41
+ log_error("Invalid request", e)
42
+ post_process Response::BAD_REQUEST
43
+ end
44
+
45
+ # Called when all data was received and the request
46
+ # is ready to be processed.
47
+ def process
48
+ if threaded?
49
+ @request.threaded = true
50
+ EventMachine.defer { post_process(pre_process) }
51
+ else
52
+ @request.threaded = false
53
+ post_process(pre_process)
54
+ end
55
+ end
56
+
57
+ def ssl_verify_peer(cert)
58
+ # In order to make the cert available later we have to have made at least
59
+ # a show of verifying it.
60
+ true
61
+ end
62
+
63
+ def pre_process
64
+ # Add client info to the request env
65
+ @request.remote_address = remote_address
66
+
67
+ # Connection may be closed unless the App#call response was a [-1, ...]
68
+ # It should be noted that connection objects will linger until this
69
+ # callback is no longer referenced, so be tidy!
70
+ @request.async_callback = method(:post_process)
71
+
72
+ if @backend.ssl?
73
+ @request.env["rack.url_scheme"] = "https"
74
+
75
+ if cert = get_peer_cert
76
+ @request.env['rack.peer_cert'] = cert
77
+ end
78
+ end
79
+
80
+ # When we're under a non-async framework like rails, we can still spawn
81
+ # off async responses using the callback info, so there's little point
82
+ # in removing this.
83
+ response = AsyncResponse
84
+ catch(:async) do
85
+ # Process the request calling the Rack adapter
86
+ response = @app.call(@request.env)
87
+ end
88
+ response
89
+ rescue Exception => e
90
+ unexpected_error(e)
91
+ # Pass through error response
92
+ can_persist? && @request.persistent? ? Response::PERSISTENT_ERROR : Response::ERROR
93
+ end
94
+
95
+ def post_process(result)
96
+ return unless result
97
+ result = result.to_a
98
+
99
+ # Status code -1 indicates that we're going to respond later (async).
100
+ return if result.first == AsyncResponse.first
101
+
102
+ @response.status, @response.headers, @response.body = *result
103
+
104
+ log_error("Rack application returned nil body. " \
105
+ "Probably you wanted it to be an empty string?") if @response.body.nil?
106
+
107
+ # HEAD requests should not return a body.
108
+ @response.skip_body! if @request.head?
109
+
110
+ # Make the response persistent if requested by the client
111
+ @response.persistent! if @request.persistent?
112
+
113
+ # Send the response
114
+ @response.each do |chunk|
115
+ trace chunk
116
+ send_data chunk
117
+ end
118
+
119
+ rescue Exception => e
120
+ unexpected_error(e)
121
+ # Close connection since we can't handle response gracefully
122
+ close_connection
123
+ ensure
124
+ # If the body is being deferred, then terminate afterward.
125
+ if @response.body.respond_to?(:callback) && @response.body.respond_to?(:errback)
126
+ @response.body.callback { terminate_request }
127
+ @response.body.errback { terminate_request }
128
+ else
129
+ # Don't terminate the response if we're going async.
130
+ terminate_request unless result && result.first == AsyncResponse.first
131
+ end
132
+ end
133
+
134
+ # Logs information about an unexpected exceptional condition
135
+ def unexpected_error(e)
136
+ log_error("Unexpected error while processing request", e)
137
+ end
138
+
139
+ def close_request_response
140
+ @request.async_close.succeed if @request.async_close
141
+ @request.close rescue nil
142
+ @response.close rescue nil
143
+ end
144
+
145
+ # Does request and response cleanup (closes open IO streams and
146
+ # deletes created temporary files).
147
+ # Re-initializes response and request if client supports persistent
148
+ # connection.
149
+ def terminate_request
150
+ unless persistent?
151
+ close_connection_after_writing rescue nil
152
+ close_request_response
153
+ else
154
+ close_request_response
155
+ # Connection become idle but it's still open
156
+ @idle = true
157
+ # Prepare the connection for another request if the client
158
+ # supports HTTP pipelining (persistent connection).
159
+ post_init
160
+ end
161
+ end
162
+
163
+ # Called when the connection is unbinded from the socket
164
+ # and can no longer be used to process requests.
165
+ def unbind
166
+ @request.async_close.succeed if @request.async_close
167
+ @response.body.fail if @response.body.respond_to?(:fail)
168
+ @backend.connection_finished(self)
169
+ end
170
+
171
+ # Allows this connection to be persistent.
172
+ def can_persist!
173
+ @can_persist = true
174
+ end
175
+
176
+ # Return +true+ if this connection is allowed to stay open and be persistent.
177
+ def can_persist?
178
+ @can_persist
179
+ end
180
+
181
+ # Return +true+ if the connection must be left open
182
+ # and ready to be reused for another request.
183
+ def persistent?
184
+ @can_persist && @response.persistent?
185
+ end
186
+
187
+ # Return +true+ if the connection is open but is not
188
+ # processing any user requests
189
+ def idle?
190
+ @idle
191
+ end
192
+
193
+ # +true+ if <tt>app.call</tt> will be called inside a thread.
194
+ # You can set all requests as threaded setting <tt>Connection#threaded=true</tt>
195
+ # or on a per-request case returning +true+ in <tt>app.deferred?</tt>.
196
+ def threaded?
197
+ @threaded || (@app.respond_to?(:deferred?) && @app.deferred?(@request.env))
198
+ end
199
+
200
+ # IP Address of the remote client.
201
+ def remote_address
202
+ socket_address
203
+ rescue Exception => e
204
+ log_error('Could not infer remote address', e)
205
+ nil
206
+ end
207
+
208
+ protected
209
+ # Returns IP address of peer as a string.
210
+ def socket_address
211
+ peer = get_peername
212
+ Socket.unpack_sockaddr_in(peer)[1] if peer
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,178 @@
1
+ require 'socket'
2
+
3
+ module Thin
4
+ # An exception class to handle the event that server didn't start on time
5
+ class RestartTimeout < RuntimeError; end
6
+
7
+ module Controllers
8
+ # Control a set of servers.
9
+ # * Generate start and stop commands and run them.
10
+ # * Inject the port or socket number in the pid and log filenames.
11
+ # Servers are started throught the +thin+ command-line script.
12
+ class Cluster < Controller
13
+ # Cluster only options that should not be passed in the command sent
14
+ # to the indiviual servers.
15
+ CLUSTER_OPTIONS = [:servers, :only, :onebyone, :wait]
16
+
17
+ # Maximum wait time for the server to be restarted
18
+ DEFAULT_WAIT_TIME = 30 # seconds
19
+
20
+ # Create a new cluster of servers launched using +options+.
21
+ def initialize(options)
22
+ super
23
+ # Cluster can only contain daemonized servers
24
+ @options.merge!(:daemonize => true)
25
+ end
26
+
27
+ def first_port; @options[:port] end
28
+ def address; @options[:address] end
29
+ def socket; @options[:socket] end
30
+ def pid_file; @options[:pid] end
31
+ def log_file; @options[:log] end
32
+ def size; @options[:servers] end
33
+ def only; @options[:only] end
34
+ def onebyone; @options[:onebyone] end
35
+ def wait; @options[:wait] end
36
+
37
+ def swiftiply?
38
+ @options.has_key?(:swiftiply)
39
+ end
40
+
41
+ # Start the servers
42
+ def start
43
+ with_each_server { |n| start_server n }
44
+ end
45
+
46
+ # Start a single server
47
+ def start_server(number)
48
+ log_info "Starting server on #{server_id(number)} ... "
49
+
50
+ run :start, number
51
+ end
52
+
53
+ # Stop the servers
54
+ def stop
55
+ with_each_server { |n| stop_server n }
56
+ end
57
+
58
+ # Stop a single server
59
+ def stop_server(number)
60
+ log_info "Stopping server on #{server_id(number)} ... "
61
+
62
+ run :stop, number
63
+ end
64
+
65
+ # Stop and start the servers.
66
+ def restart
67
+ unless onebyone
68
+ # Let's do a normal restart by defaults
69
+ stop
70
+ sleep 0.1 # Let's breath a bit shall we ?
71
+ start
72
+ else
73
+ with_each_server do |n|
74
+ stop_server(n)
75
+ sleep 0.1 # Let's breath a bit shall we ?
76
+ start_server(n)
77
+ wait_until_server_started(n)
78
+ end
79
+ end
80
+ end
81
+
82
+ def test_socket(number)
83
+ if socket
84
+ UNIXSocket.new(socket_for(number))
85
+ else
86
+ TCPSocket.new(address, number)
87
+ end
88
+ rescue
89
+ nil
90
+ end
91
+
92
+ # Make sure the server is running before moving on to the next one.
93
+ def wait_until_server_started(number)
94
+ log_info "Waiting for server to start ..."
95
+ STDOUT.flush # Need this to make sure user got the message
96
+
97
+ tries = 0
98
+ loop do
99
+ if test_socket = test_socket(number)
100
+ test_socket.close
101
+ break
102
+ elsif tries < wait
103
+ sleep 1
104
+ tries += 1
105
+ else
106
+ raise RestartTimeout, "The server didn't start in time. Please look at server's log file " +
107
+ "for more information, or set the value of 'wait' in your config " +
108
+ "file to be higher (defaults: 30)."
109
+ end
110
+ end
111
+ end
112
+
113
+ def server_id(number)
114
+ if socket
115
+ socket_for(number)
116
+ elsif swiftiply?
117
+ [address, first_port, number].join(':')
118
+ else
119
+ [address, number].join(':')
120
+ end
121
+ end
122
+
123
+ def log_file_for(number)
124
+ include_server_number log_file, number
125
+ end
126
+
127
+ def pid_file_for(number)
128
+ include_server_number pid_file, number
129
+ end
130
+
131
+ def socket_for(number)
132
+ include_server_number socket, number
133
+ end
134
+
135
+ def pid_for(number)
136
+ File.read(pid_file_for(number)).chomp.to_i
137
+ end
138
+
139
+ private
140
+ # Send the command to the +thin+ script
141
+ def run(cmd, number)
142
+ cmd_options = @options.reject { |option, value| CLUSTER_OPTIONS.include?(option) }
143
+ cmd_options.merge!(:pid => pid_file_for(number), :log => log_file_for(number))
144
+ if socket
145
+ cmd_options.merge!(:socket => socket_for(number))
146
+ elsif swiftiply?
147
+ cmd_options.merge!(:port => first_port)
148
+ else
149
+ cmd_options.merge!(:port => number)
150
+ end
151
+ Command.run(cmd, cmd_options)
152
+ end
153
+
154
+ def with_each_server
155
+ if only
156
+ if first_port && only < 80
157
+ # interpret +only+ as a sequence number
158
+ yield first_port + only
159
+ else
160
+ # interpret +only+ as an absolute port number
161
+ yield only
162
+ end
163
+ elsif socket || swiftiply?
164
+ size.times { |n| yield n }
165
+ else
166
+ size.times { |n| yield first_port + n }
167
+ end
168
+ end
169
+
170
+ # Add the server port or number in the filename
171
+ # so each instance get its own file
172
+ def include_server_number(path, number)
173
+ ext = File.extname(path)
174
+ path.gsub(/#{ext}$/, ".#{number}#{ext}")
175
+ end
176
+ end
177
+ end
178
+ end