friendlyfashion-thin 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/CHANGELOG +346 -0
  2. data/README.md +64 -0
  3. data/Rakefile +24 -0
  4. data/bin/thin +6 -0
  5. data/example/adapter.rb +32 -0
  6. data/example/async_app.ru +126 -0
  7. data/example/async_chat.ru +247 -0
  8. data/example/async_tailer.ru +100 -0
  9. data/example/config.ru +22 -0
  10. data/example/monit_sockets +20 -0
  11. data/example/monit_unixsock +20 -0
  12. data/example/myapp.rb +1 -0
  13. data/example/ramaze.ru +12 -0
  14. data/example/thin.god +80 -0
  15. data/example/thin_solaris_smf.erb +36 -0
  16. data/example/thin_solaris_smf.readme.txt +150 -0
  17. data/example/vlad.rake +64 -0
  18. data/ext/thin_parser/common.rl +55 -0
  19. data/ext/thin_parser/ext_help.h +14 -0
  20. data/ext/thin_parser/extconf.rb +6 -0
  21. data/ext/thin_parser/parser.c +1249 -0
  22. data/ext/thin_parser/parser.h +49 -0
  23. data/ext/thin_parser/parser.rl +157 -0
  24. data/ext/thin_parser/thin.c +436 -0
  25. data/lib/rack/adapter/loader.rb +75 -0
  26. data/lib/rack/adapter/rails.rb +183 -0
  27. data/lib/thin.rb +45 -0
  28. data/lib/thin/backends/base.rb +151 -0
  29. data/lib/thin/backends/swiftiply_client.rb +56 -0
  30. data/lib/thin/backends/tcp_server.rb +29 -0
  31. data/lib/thin/backends/unix_server.rb +56 -0
  32. data/lib/thin/command.rb +53 -0
  33. data/lib/thin/connection.rb +201 -0
  34. data/lib/thin/controllers/cluster.rb +178 -0
  35. data/lib/thin/controllers/controller.rb +188 -0
  36. data/lib/thin/controllers/service.rb +75 -0
  37. data/lib/thin/controllers/service.sh.erb +39 -0
  38. data/lib/thin/daemonizing.rb +185 -0
  39. data/lib/thin/headers.rb +39 -0
  40. data/lib/thin/logging.rb +54 -0
  41. data/lib/thin/request.rb +156 -0
  42. data/lib/thin/response.rb +104 -0
  43. data/lib/thin/runner.rb +222 -0
  44. data/lib/thin/server.rb +270 -0
  45. data/lib/thin/stats.html.erb +216 -0
  46. data/lib/thin/stats.rb +52 -0
  47. data/lib/thin/statuses.rb +43 -0
  48. data/lib/thin/version.rb +32 -0
  49. metadata +151 -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,29 @@
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
+ end
18
+
19
+ # Stops the server
20
+ def disconnect
21
+ EventMachine.stop_server(@signature)
22
+ end
23
+
24
+ def to_s
25
+ "#{@host}:#{@port}"
26
+ end
27
+ end
28
+ end
29
+ 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 stdout.gets until stdout.eof?
30
+ log 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,201 @@
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
+ trace { data }
38
+ process if @request.parse(data)
39
+ rescue InvalidRequest => e
40
+ log "!! Invalid request"
41
+ log_error e
42
+
43
+ send_data "HTTP/1.0 400 Bad Request\r\nConnection: close\r\nContent-Type: text/plain\r\n\r\nBad Request!\r\n"
44
+ close_connection_after_writing
45
+ end
46
+
47
+ # Called when all data was received and the request
48
+ # is ready to be processed.
49
+ def process
50
+ if threaded?
51
+ @request.threaded = true
52
+ EventMachine.defer(method(:pre_process), method(:post_process))
53
+ else
54
+ @request.threaded = false
55
+ post_process(pre_process)
56
+ end
57
+ end
58
+
59
+ def pre_process
60
+ # Add client info to the request env
61
+ @request.remote_address = remote_address
62
+
63
+ # Connection may be closed unless the App#call response was a [-1, ...]
64
+ # It should be noted that connection objects will linger until this
65
+ # callback is no longer referenced, so be tidy!
66
+ @request.async_callback = method(:post_process)
67
+
68
+ if @backend.ssl?
69
+ @request.env["rack.url_scheme"] = "https"
70
+
71
+ if cert = get_peer_cert
72
+ @request.env['rack.peer_cert'] = cert
73
+ end
74
+ end
75
+
76
+ # When we're under a non-async framework like rails, we can still spawn
77
+ # off async responses using the callback info, so there's little point
78
+ # in removing this.
79
+ response = AsyncResponse
80
+ catch(:async) do
81
+ # Process the request calling the Rack adapter
82
+ response = @app.call(@request.env)
83
+ end
84
+ response
85
+ rescue Exception
86
+ handle_error
87
+ terminate_request
88
+ nil # Signal to post_process that the request could not be processed
89
+ end
90
+
91
+ def post_process(result)
92
+ return unless result
93
+ result = result.to_a
94
+
95
+ # Status code -1 indicates that we're going to respond later (async).
96
+ return if result.first == AsyncResponse.first
97
+
98
+ @response.status, @response.headers, @response.body = *result
99
+
100
+ log "!! Rack application returned nil body. Probably you wanted it to be an empty string?" if @response.body.nil?
101
+
102
+ # Make the response persistent if requested by the client
103
+ @response.persistent! if @request.persistent?
104
+
105
+ # Send the response
106
+ @response.each do |chunk|
107
+ trace { chunk }
108
+ send_data chunk
109
+ end
110
+
111
+ rescue Exception
112
+ handle_error
113
+ ensure
114
+ # If the body is being deferred, then terminate afterward.
115
+ if @response.body.respond_to?(:callback) && @response.body.respond_to?(:errback)
116
+ @response.body.callback { terminate_request }
117
+ @response.body.errback { terminate_request }
118
+ else
119
+ # Don't terminate the response if we're going async.
120
+ terminate_request unless result && result.first == AsyncResponse.first
121
+ end
122
+ end
123
+
124
+ # Logs catched exception and closes the connection.
125
+ def handle_error
126
+ log "!! Unexpected error while processing request: #{$!.message}"
127
+ log_error
128
+
129
+ send_data "HTTP/1.0 400 Bad Request\r\nConnection: close\r\nContent-Type: text/plain\r\n\r\nBad Request!\r\n"
130
+ close_connection_after_writing
131
+ end
132
+
133
+ def close_request_response
134
+ @request.async_close.succeed if @request.async_close
135
+ @request.close rescue nil
136
+ @response.close rescue nil
137
+ end
138
+
139
+ # Does request and response cleanup (closes open IO streams and
140
+ # deletes created temporary files).
141
+ # Re-initializes response and request if client supports persistent
142
+ # connection.
143
+ def terminate_request
144
+ unless persistent?
145
+ close_connection_after_writing rescue nil
146
+ close_request_response
147
+ else
148
+ close_request_response
149
+ # Prepare the connection for another request if the client
150
+ # supports HTTP pipelining (persistent connection).
151
+ post_init
152
+ end
153
+ end
154
+
155
+ # Called when the connection is unbinded from the socket
156
+ # and can no longer be used to process requests.
157
+ def unbind
158
+ @request.async_close.succeed if @request.async_close
159
+ @response.body.fail if @response.body.respond_to?(:fail)
160
+ @backend.connection_finished(self)
161
+ end
162
+
163
+ # Allows this connection to be persistent.
164
+ def can_persist!
165
+ @can_persist = true
166
+ end
167
+
168
+ # Return +true+ if this connection is allowed to stay open and be persistent.
169
+ def can_persist?
170
+ @can_persist
171
+ end
172
+
173
+ # Return +true+ if the connection must be left open
174
+ # and ready to be reused for another request.
175
+ def persistent?
176
+ @can_persist && @response.persistent?
177
+ end
178
+
179
+ # +true+ if <tt>app.call</tt> will be called inside a thread.
180
+ # You can set all requests as threaded setting <tt>Connection#threaded=true</tt>
181
+ # or on a per-request case returning +true+ in <tt>app.deferred?</tt>.
182
+ def threaded?
183
+ @threaded || (@app.respond_to?(:deferred?) && @app.deferred?(@request.env))
184
+ end
185
+
186
+ # IP Address of the remote client.
187
+ def remote_address
188
+ socket_address
189
+ rescue Exception
190
+ log_error
191
+ nil
192
+ end
193
+
194
+ protected
195
+ # Returns IP address of peer as a string.
196
+ def socket_address
197
+ peer = get_peername
198
+ Socket.unpack_sockaddr_in(peer)[1] if peer
199
+ end
200
+ end
201
+ 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 "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 "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 "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