steamcannon-thin 1.2.8
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +288 -0
- data/COPYING +18 -0
- data/README +69 -0
- data/Rakefile +44 -0
- data/benchmark/abc +51 -0
- data/benchmark/benchmarker.rb +80 -0
- data/benchmark/runner +82 -0
- data/bin/thin +6 -0
- data/example/adapter.rb +32 -0
- data/example/async_app.ru +126 -0
- data/example/async_chat.ru +247 -0
- data/example/async_tailer.ru +100 -0
- data/example/config.ru +22 -0
- data/example/monit_sockets +20 -0
- data/example/monit_unixsock +20 -0
- data/example/myapp.rb +1 -0
- data/example/ramaze.ru +12 -0
- data/example/thin.god +80 -0
- data/example/thin_solaris_smf.erb +36 -0
- data/example/thin_solaris_smf.readme.txt +150 -0
- data/example/vlad.rake +64 -0
- data/ext/thin_parser/common.rl +55 -0
- data/ext/thin_parser/ext_help.h +14 -0
- data/ext/thin_parser/extconf.rb +6 -0
- data/ext/thin_parser/parser.c +1249 -0
- data/ext/thin_parser/parser.h +49 -0
- data/ext/thin_parser/parser.rl +157 -0
- data/ext/thin_parser/thin.c +436 -0
- data/lib/rack/adapter/loader.rb +91 -0
- data/lib/rack/adapter/rails.rb +183 -0
- data/lib/thin.rb +56 -0
- data/lib/thin/backends/base.rb +149 -0
- data/lib/thin/backends/swiftiply_client.rb +56 -0
- data/lib/thin/backends/tcp_server.rb +29 -0
- data/lib/thin/backends/unix_server.rb +51 -0
- data/lib/thin/command.rb +53 -0
- data/lib/thin/connection.rb +224 -0
- data/lib/thin/controllers/cluster.rb +178 -0
- data/lib/thin/controllers/controller.rb +188 -0
- data/lib/thin/controllers/service.rb +75 -0
- data/lib/thin/controllers/service.sh.erb +39 -0
- data/lib/thin/daemonizing.rb +180 -0
- data/lib/thin/headers.rb +39 -0
- data/lib/thin/logging.rb +54 -0
- data/lib/thin/request.rb +156 -0
- data/lib/thin/response.rb +101 -0
- data/lib/thin/runner.rb +220 -0
- data/lib/thin/server.rb +253 -0
- data/lib/thin/stats.html.erb +216 -0
- data/lib/thin/stats.rb +52 -0
- data/lib/thin/statuses.rb +43 -0
- data/lib/thin/version.rb +32 -0
- data/lib/thin_parser.so +0 -0
- data/spec/backends/swiftiply_client_spec.rb +66 -0
- data/spec/backends/tcp_server_spec.rb +33 -0
- data/spec/backends/unix_server_spec.rb +37 -0
- data/spec/command_spec.rb +25 -0
- data/spec/configs/cluster.yml +9 -0
- data/spec/configs/single.yml +9 -0
- data/spec/connection_spec.rb +106 -0
- data/spec/controllers/cluster_spec.rb +267 -0
- data/spec/controllers/controller_spec.rb +129 -0
- data/spec/controllers/service_spec.rb +50 -0
- data/spec/daemonizing_spec.rb +196 -0
- data/spec/headers_spec.rb +40 -0
- data/spec/logging_spec.rb +46 -0
- data/spec/perf/request_perf_spec.rb +50 -0
- data/spec/perf/response_perf_spec.rb +19 -0
- data/spec/perf/server_perf_spec.rb +39 -0
- data/spec/rack/loader_spec.rb +42 -0
- data/spec/rack/rails_adapter_spec.rb +173 -0
- data/spec/rails_app/app/controllers/application.rb +10 -0
- data/spec/rails_app/app/controllers/simple_controller.rb +19 -0
- data/spec/rails_app/app/helpers/application_helper.rb +3 -0
- data/spec/rails_app/app/views/simple/index.html.erb +15 -0
- data/spec/rails_app/config/boot.rb +109 -0
- data/spec/rails_app/config/environment.rb +64 -0
- data/spec/rails_app/config/environments/development.rb +18 -0
- data/spec/rails_app/config/environments/production.rb +19 -0
- data/spec/rails_app/config/environments/test.rb +22 -0
- data/spec/rails_app/config/initializers/inflections.rb +10 -0
- data/spec/rails_app/config/initializers/mime_types.rb +5 -0
- data/spec/rails_app/config/routes.rb +35 -0
- data/spec/rails_app/public/404.html +30 -0
- data/spec/rails_app/public/422.html +30 -0
- data/spec/rails_app/public/500.html +30 -0
- data/spec/rails_app/public/dispatch.cgi +10 -0
- data/spec/rails_app/public/dispatch.fcgi +24 -0
- data/spec/rails_app/public/dispatch.rb +10 -0
- data/spec/rails_app/public/favicon.ico +0 -0
- data/spec/rails_app/public/images/rails.png +0 -0
- data/spec/rails_app/public/index.html +277 -0
- data/spec/rails_app/public/javascripts/application.js +2 -0
- data/spec/rails_app/public/javascripts/controls.js +963 -0
- data/spec/rails_app/public/javascripts/dragdrop.js +972 -0
- data/spec/rails_app/public/javascripts/effects.js +1120 -0
- data/spec/rails_app/public/javascripts/prototype.js +4225 -0
- data/spec/rails_app/public/robots.txt +5 -0
- data/spec/rails_app/script/about +3 -0
- data/spec/rails_app/script/console +3 -0
- data/spec/rails_app/script/destroy +3 -0
- data/spec/rails_app/script/generate +3 -0
- data/spec/rails_app/script/performance/benchmarker +3 -0
- data/spec/rails_app/script/performance/profiler +3 -0
- data/spec/rails_app/script/performance/request +3 -0
- data/spec/rails_app/script/plugin +3 -0
- data/spec/rails_app/script/process/inspector +3 -0
- data/spec/rails_app/script/process/reaper +3 -0
- data/spec/rails_app/script/process/spawner +3 -0
- data/spec/rails_app/script/runner +3 -0
- data/spec/rails_app/script/server +3 -0
- data/spec/request/mongrel_spec.rb +39 -0
- data/spec/request/parser_spec.rb +254 -0
- data/spec/request/persistent_spec.rb +35 -0
- data/spec/request/processing_spec.rb +50 -0
- data/spec/response_spec.rb +91 -0
- data/spec/runner_spec.rb +168 -0
- data/spec/server/builder_spec.rb +44 -0
- data/spec/server/pipelining_spec.rb +110 -0
- data/spec/server/robustness_spec.rb +34 -0
- data/spec/server/stopping_spec.rb +55 -0
- data/spec/server/swiftiply.yml +6 -0
- data/spec/server/swiftiply_spec.rb +32 -0
- data/spec/server/tcp_spec.rb +57 -0
- data/spec/server/threaded_spec.rb +27 -0
- data/spec/server/unix_socket_spec.rb +26 -0
- data/spec/server_spec.rb +100 -0
- data/spec/spec_helper.rb +220 -0
- data/tasks/announce.rake +22 -0
- data/tasks/deploy.rake +13 -0
- data/tasks/email.erb +30 -0
- data/tasks/gem.rake +66 -0
- data/tasks/rdoc.rake +25 -0
- data/tasks/site.rake +15 -0
- data/tasks/spec.rake +43 -0
- data/tasks/stats.rake +28 -0
- metadata +251 -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,51 @@
|
|
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
|
+
EventMachine.start_unix_domain_server(@socket, UnixConnection, &method(:initialize_connection))
|
18
|
+
# HACK EventMachine.start_unix_domain_server doesn't return the connection signature
|
19
|
+
# so we have to go in the internal stuff to find it.
|
20
|
+
@signature = EventMachine.instance_eval{@acceptors.keys.first}
|
21
|
+
end
|
22
|
+
|
23
|
+
# Stops the server
|
24
|
+
def disconnect
|
25
|
+
EventMachine.stop_server(@signature)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Free up resources used by the backend.
|
29
|
+
def close
|
30
|
+
remove_socket_file
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
@socket
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
def remove_socket_file
|
39
|
+
File.delete(@socket) if @socket && File.exist?(@socket)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Connection through a UNIX domain socket.
|
45
|
+
class UnixConnection < Connection
|
46
|
+
protected
|
47
|
+
def socket_address
|
48
|
+
'127.0.0.1' # Unix domain sockets can only be local
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/thin/command.rb
ADDED
@@ -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,224 @@
|
|
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
|
+
CONTENT_LENGTH = 'Content-Length'.freeze
|
9
|
+
TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
|
10
|
+
CHUNKED_REGEXP = /\bchunked\b/i.freeze
|
11
|
+
|
12
|
+
include Logging
|
13
|
+
|
14
|
+
# This is a template async response. N.B. Can't use string for body on 1.9
|
15
|
+
AsyncResponse = [-1, {}, []].freeze
|
16
|
+
|
17
|
+
# Rack application (adapter) served by this connection.
|
18
|
+
attr_accessor :app
|
19
|
+
|
20
|
+
# Backend to the server
|
21
|
+
attr_accessor :backend
|
22
|
+
|
23
|
+
# Current request served by the connection
|
24
|
+
attr_accessor :request
|
25
|
+
|
26
|
+
# Next response sent through the connection
|
27
|
+
attr_accessor :response
|
28
|
+
|
29
|
+
# Calling the application in a threaded allowing
|
30
|
+
# concurrent processing of requests.
|
31
|
+
attr_writer :threaded
|
32
|
+
|
33
|
+
# Get the connection ready to process a request.
|
34
|
+
def post_init
|
35
|
+
@request = Request.new
|
36
|
+
@response = Response.new
|
37
|
+
end
|
38
|
+
|
39
|
+
# Called when data is received from the client.
|
40
|
+
def receive_data(data)
|
41
|
+
trace { data }
|
42
|
+
process if @request.parse(data)
|
43
|
+
rescue InvalidRequest => e
|
44
|
+
log "!! Invalid request"
|
45
|
+
log_error e
|
46
|
+
close_connection
|
47
|
+
end
|
48
|
+
|
49
|
+
# Called when all data was received and the request
|
50
|
+
# is ready to be processed.
|
51
|
+
def process
|
52
|
+
if threaded?
|
53
|
+
@request.threaded = true
|
54
|
+
EventMachine.defer(method(:pre_process), method(:post_process))
|
55
|
+
else
|
56
|
+
@request.threaded = false
|
57
|
+
post_process(pre_process)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def pre_process
|
62
|
+
# Add client info to the request env
|
63
|
+
@request.remote_address = remote_address
|
64
|
+
|
65
|
+
# Connection may be closed unless the App#call response was a [-1, ...]
|
66
|
+
# It should be noted that connection objects will linger until this
|
67
|
+
# callback is no longer referenced, so be tidy!
|
68
|
+
@request.async_callback = method(:post_process)
|
69
|
+
|
70
|
+
@request.env["rack.url_scheme"] = "https" if backend.ssl?
|
71
|
+
|
72
|
+
# When we're under a non-async framework like rails, we can still spawn
|
73
|
+
# off async responses using the callback info, so there's little point
|
74
|
+
# in removing this.
|
75
|
+
response = AsyncResponse
|
76
|
+
catch(:async) do
|
77
|
+
# Process the request calling the Rack adapter
|
78
|
+
response = @app.call(@request.env)
|
79
|
+
end
|
80
|
+
response
|
81
|
+
rescue Exception
|
82
|
+
handle_error
|
83
|
+
terminate_request
|
84
|
+
nil # Signal to post_process that the request could not be processed
|
85
|
+
end
|
86
|
+
|
87
|
+
def post_process(result)
|
88
|
+
return unless result
|
89
|
+
result = result.to_a
|
90
|
+
|
91
|
+
# Status code -1 indicates that we're going to respond later (async).
|
92
|
+
return if result.first == AsyncResponse.first
|
93
|
+
|
94
|
+
# Set the Content-Length header if possible
|
95
|
+
set_content_length(result) if need_content_length?(result)
|
96
|
+
|
97
|
+
@response.status, @response.headers, @response.body = *result
|
98
|
+
|
99
|
+
log "!! Rack application returned nil body. Probably you wanted it to be an empty string?" if @response.body.nil?
|
100
|
+
|
101
|
+
# Make the response persistent if requested by the client
|
102
|
+
@response.persistent! if @request.persistent?
|
103
|
+
|
104
|
+
# Send the response
|
105
|
+
@response.each do |chunk|
|
106
|
+
trace { chunk }
|
107
|
+
send_data chunk
|
108
|
+
end
|
109
|
+
|
110
|
+
rescue Exception
|
111
|
+
handle_error
|
112
|
+
ensure
|
113
|
+
# If the body is being deferred, then terminate afterward.
|
114
|
+
if @response.body.respond_to?(:callback) && @response.body.respond_to?(:errback)
|
115
|
+
@response.body.callback { terminate_request }
|
116
|
+
@response.body.errback { terminate_request }
|
117
|
+
else
|
118
|
+
# Don't terminate the response if we're going async.
|
119
|
+
terminate_request unless result && result.first == AsyncResponse.first
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Logs catched exception and closes the connection.
|
124
|
+
def handle_error
|
125
|
+
log "!! Unexpected error while processing request: #{$!.message}"
|
126
|
+
log_error
|
127
|
+
close_connection rescue nil
|
128
|
+
end
|
129
|
+
|
130
|
+
def close_request_response
|
131
|
+
@request.async_close.succeed if @request.async_close
|
132
|
+
@request.close rescue nil
|
133
|
+
@response.close rescue nil
|
134
|
+
end
|
135
|
+
|
136
|
+
# Does request and response cleanup (closes open IO streams and
|
137
|
+
# deletes created temporary files).
|
138
|
+
# Re-initializes response and request if client supports persistent
|
139
|
+
# connection.
|
140
|
+
def terminate_request
|
141
|
+
unless persistent?
|
142
|
+
close_connection_after_writing rescue nil
|
143
|
+
close_request_response
|
144
|
+
else
|
145
|
+
close_request_response
|
146
|
+
# Prepare the connection for another request if the client
|
147
|
+
# supports HTTP pipelining (persistent connection).
|
148
|
+
post_init
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Called when the connection is unbinded from the socket
|
153
|
+
# and can no longer be used to process requests.
|
154
|
+
def unbind
|
155
|
+
@request.async_close.succeed if @request.async_close
|
156
|
+
@response.body.fail if @response.body.respond_to?(:fail)
|
157
|
+
@backend.connection_finished(self)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Allows this connection to be persistent.
|
161
|
+
def can_persist!
|
162
|
+
@can_persist = true
|
163
|
+
end
|
164
|
+
|
165
|
+
# Return +true+ if this connection is allowed to stay open and be persistent.
|
166
|
+
def can_persist?
|
167
|
+
@can_persist
|
168
|
+
end
|
169
|
+
|
170
|
+
# Return +true+ if the connection must be left open
|
171
|
+
# and ready to be reused for another request.
|
172
|
+
def persistent?
|
173
|
+
@can_persist && @response.persistent?
|
174
|
+
end
|
175
|
+
|
176
|
+
# +true+ if <tt>app.call</tt> will be called inside a thread.
|
177
|
+
# You can set all requests as threaded setting <tt>Connection#threaded=true</tt>
|
178
|
+
# or on a per-request case returning +true+ in <tt>app.deferred?</tt>.
|
179
|
+
def threaded?
|
180
|
+
@threaded || (@app.respond_to?(:deferred?) && @app.deferred?(@request.env))
|
181
|
+
end
|
182
|
+
|
183
|
+
# IP Address of the remote client.
|
184
|
+
def remote_address
|
185
|
+
socket_address
|
186
|
+
rescue Exception
|
187
|
+
log_error
|
188
|
+
nil
|
189
|
+
end
|
190
|
+
|
191
|
+
protected
|
192
|
+
|
193
|
+
# Returns IP address of peer as a string.
|
194
|
+
def socket_address
|
195
|
+
Socket.unpack_sockaddr_in(get_peername)[1]
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
def need_content_length?(result)
|
200
|
+
status, headers, body = result
|
201
|
+
return false if status == -1
|
202
|
+
return false if headers.has_key?(CONTENT_LENGTH)
|
203
|
+
return false if (100..199).include?(status) || status == 204 || status == 304
|
204
|
+
return false if headers.has_key?(TRANSFER_ENCODING) && headers[TRANSFER_ENCODING] =~ CHUNKED_REGEXP
|
205
|
+
return false unless body.kind_of?(String) || body.kind_of?(Array)
|
206
|
+
true
|
207
|
+
end
|
208
|
+
|
209
|
+
def set_content_length(result)
|
210
|
+
headers, body = result[1..2]
|
211
|
+
case body
|
212
|
+
when String
|
213
|
+
# See http://redmine.ruby-lang.org/issues/show/203
|
214
|
+
headers[CONTENT_LENGTH] = (body.respond_to?(:bytesize) ? body.bytesize : body.size).to_s
|
215
|
+
when Array
|
216
|
+
bytes = 0
|
217
|
+
body.each do |p|
|
218
|
+
bytes += p.respond_to?(:bytesize) ? p.bytesize : p.size
|
219
|
+
end
|
220
|
+
headers[CONTENT_LENGTH] = bytes.to_s
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
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
|