thin 1.2.3-x86-mswin32
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.
- data/CHANGELOG +263 -0
- data/COPYING +18 -0
- data/README +69 -0
- data/Rakefile +36 -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 +452 -0
- data/ext/thin_parser/parser.h +49 -0
- data/ext/thin_parser/parser.rl +157 -0
- data/ext/thin_parser/thin.c +433 -0
- data/lib/rack/adapter/loader.rb +79 -0
- data/lib/rack/adapter/rails.rb +181 -0
- data/lib/thin.rb +46 -0
- data/lib/thin/backends/base.rb +141 -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 +222 -0
- data/lib/thin/controllers/cluster.rb +127 -0
- data/lib/thin/controllers/controller.rb +183 -0
- data/lib/thin/controllers/service.rb +75 -0
- data/lib/thin/controllers/service.sh.erb +39 -0
- data/lib/thin/daemonizing.rb +174 -0
- data/lib/thin/headers.rb +39 -0
- data/lib/thin/logging.rb +54 -0
- data/lib/thin/request.rb +153 -0
- data/lib/thin/response.rb +101 -0
- data/lib/thin/runner.rb +209 -0
- data/lib/thin/server.rb +247 -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 +235 -0
- data/spec/controllers/controller_spec.rb +129 -0
- data/spec/controllers/service_spec.rb +50 -0
- data/spec/daemonizing_spec.rb +192 -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 +29 -0
- data/spec/rack/rails_adapter_spec.rb +106 -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 +215 -0
- data/spec/request/persistent_spec.rb +35 -0
- data/spec/request/processing_spec.rb +45 -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 +96 -0
- data/spec/spec_helper.rb +219 -0
- data/tasks/announce.rake +22 -0
- data/tasks/deploy.rake +13 -0
- data/tasks/email.erb +30 -0
- data/tasks/gem.rake +74 -0
- data/tasks/rdoc.rake +25 -0
- data/tasks/site.rake +15 -0
- data/tasks/spec.rake +49 -0
- data/tasks/stats.rake +28 -0
- metadata +246 -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,222 @@
|
|
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
|
+
# When we're under a non-async framework like rails, we can still spawn
|
71
|
+
# off async responses using the callback info, so there's little point
|
72
|
+
# in removing this.
|
73
|
+
response = AsyncResponse
|
74
|
+
catch(:async) do
|
75
|
+
# Process the request calling the Rack adapter
|
76
|
+
response = @app.call(@request.env)
|
77
|
+
end
|
78
|
+
response
|
79
|
+
rescue Exception
|
80
|
+
handle_error
|
81
|
+
terminate_request
|
82
|
+
nil # Signal to post_process that the request could not be processed
|
83
|
+
end
|
84
|
+
|
85
|
+
def post_process(result)
|
86
|
+
return unless result
|
87
|
+
result = result.to_a
|
88
|
+
|
89
|
+
# Status code -1 indicates that we're going to respond later (async).
|
90
|
+
return if result.first == AsyncResponse.first
|
91
|
+
|
92
|
+
# Set the Content-Length header if possible
|
93
|
+
set_content_length(result) if need_content_length?(result)
|
94
|
+
|
95
|
+
@response.status, @response.headers, @response.body = *result
|
96
|
+
|
97
|
+
log "!! Rack application returned nil body. Probably you wanted it to be an empty string?" if @response.body.nil?
|
98
|
+
|
99
|
+
# Make the response persistent if requested by the client
|
100
|
+
@response.persistent! if @request.persistent?
|
101
|
+
|
102
|
+
# Send the response
|
103
|
+
@response.each do |chunk|
|
104
|
+
trace { chunk }
|
105
|
+
send_data chunk
|
106
|
+
end
|
107
|
+
|
108
|
+
rescue Exception
|
109
|
+
handle_error
|
110
|
+
ensure
|
111
|
+
# If the body is being deferred, then terminate afterward.
|
112
|
+
if @response.body.respond_to?(:callback) && @response.body.respond_to?(:errback)
|
113
|
+
@response.body.callback { terminate_request }
|
114
|
+
@response.body.errback { terminate_request }
|
115
|
+
else
|
116
|
+
# Don't terminate the response if we're going async.
|
117
|
+
terminate_request unless result && result.first == AsyncResponse.first
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Logs catched exception and closes the connection.
|
122
|
+
def handle_error
|
123
|
+
log "!! Unexpected error while processing request: #{$!.message}"
|
124
|
+
log_error
|
125
|
+
close_connection rescue nil
|
126
|
+
end
|
127
|
+
|
128
|
+
def close_request_response
|
129
|
+
@request.async_close.succeed if @request.async_close
|
130
|
+
@request.close rescue nil
|
131
|
+
@response.close rescue nil
|
132
|
+
end
|
133
|
+
|
134
|
+
# Does request and response cleanup (closes open IO streams and
|
135
|
+
# deletes created temporary files).
|
136
|
+
# Re-initializes response and request if client supports persistent
|
137
|
+
# connection.
|
138
|
+
def terminate_request
|
139
|
+
unless persistent?
|
140
|
+
close_connection_after_writing rescue nil
|
141
|
+
close_request_response
|
142
|
+
else
|
143
|
+
close_request_response
|
144
|
+
# Prepare the connection for another request if the client
|
145
|
+
# supports HTTP pipelining (persistent connection).
|
146
|
+
post_init
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Called when the connection is unbinded from the socket
|
151
|
+
# and can no longer be used to process requests.
|
152
|
+
def unbind
|
153
|
+
@request.async_close.succeed if @request.async_close
|
154
|
+
@response.body.fail if @response.body.respond_to?(:fail)
|
155
|
+
@backend.connection_finished(self)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Allows this connection to be persistent.
|
159
|
+
def can_persist!
|
160
|
+
@can_persist = true
|
161
|
+
end
|
162
|
+
|
163
|
+
# Return +true+ if this connection is allowed to stay open and be persistent.
|
164
|
+
def can_persist?
|
165
|
+
@can_persist
|
166
|
+
end
|
167
|
+
|
168
|
+
# Return +true+ if the connection must be left open
|
169
|
+
# and ready to be reused for another request.
|
170
|
+
def persistent?
|
171
|
+
@can_persist && @response.persistent?
|
172
|
+
end
|
173
|
+
|
174
|
+
# +true+ if <tt>app.call</tt> will be called inside a thread.
|
175
|
+
# You can set all requests as threaded setting <tt>Connection#threaded=true</tt>
|
176
|
+
# or on a per-request case returning +true+ in <tt>app.deferred?</tt>.
|
177
|
+
def threaded?
|
178
|
+
@threaded || (@app.respond_to?(:deferred?) && @app.deferred?(@request.env))
|
179
|
+
end
|
180
|
+
|
181
|
+
# IP Address of the remote client.
|
182
|
+
def remote_address
|
183
|
+
socket_address
|
184
|
+
rescue Exception
|
185
|
+
log_error
|
186
|
+
nil
|
187
|
+
end
|
188
|
+
|
189
|
+
protected
|
190
|
+
|
191
|
+
# Returns IP address of peer as a string.
|
192
|
+
def socket_address
|
193
|
+
Socket.unpack_sockaddr_in(get_peername)[1]
|
194
|
+
end
|
195
|
+
|
196
|
+
private
|
197
|
+
def need_content_length?(result)
|
198
|
+
status, headers, body = result
|
199
|
+
return false if status == -1
|
200
|
+
return false if headers.has_key?(CONTENT_LENGTH)
|
201
|
+
return false if (100..199).include?(status) || status == 204 || status == 304
|
202
|
+
return false if headers.has_key?(TRANSFER_ENCODING) && headers[TRANSFER_ENCODING] =~ CHUNKED_REGEXP
|
203
|
+
return false unless body.kind_of?(String) || body.kind_of?(Array)
|
204
|
+
true
|
205
|
+
end
|
206
|
+
|
207
|
+
def set_content_length(result)
|
208
|
+
headers, body = result[1..2]
|
209
|
+
case body
|
210
|
+
when String
|
211
|
+
# See http://redmine.ruby-lang.org/issues/show/203
|
212
|
+
headers[CONTENT_LENGTH] = (body.respond_to?(:bytesize) ? body.bytesize : body.size).to_s
|
213
|
+
when Array
|
214
|
+
bytes = 0
|
215
|
+
body.each do |p|
|
216
|
+
bytes += p.respond_to?(:bytesize) ? p.bytesize : p.size
|
217
|
+
end
|
218
|
+
headers[CONTENT_LENGTH] = bytes.to_s
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Thin
|
2
|
+
module Controllers
|
3
|
+
# Control a set of servers.
|
4
|
+
# * Generate start and stop commands and run them.
|
5
|
+
# * Inject the port or socket number in the pid and log filenames.
|
6
|
+
# Servers are started throught the +thin+ command-line script.
|
7
|
+
class Cluster < Controller
|
8
|
+
# Cluster only options that should not be passed in the command sent
|
9
|
+
# to the indiviual servers.
|
10
|
+
CLUSTER_OPTIONS = [:servers, :only]
|
11
|
+
|
12
|
+
# Create a new cluster of servers launched using +options+.
|
13
|
+
def initialize(options)
|
14
|
+
super
|
15
|
+
# Cluster can only contain daemonized servers
|
16
|
+
@options.merge!(:daemonize => true)
|
17
|
+
end
|
18
|
+
|
19
|
+
def first_port; @options[:port] end
|
20
|
+
def address; @options[:address] end
|
21
|
+
def socket; @options[:socket] end
|
22
|
+
def pid_file; @options[:pid] end
|
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
|
30
|
+
|
31
|
+
# Start the servers
|
32
|
+
def start
|
33
|
+
with_each_server { |n| start_server n }
|
34
|
+
end
|
35
|
+
|
36
|
+
# Start a single server
|
37
|
+
def start_server(number)
|
38
|
+
log "Starting server on #{server_id(number)} ... "
|
39
|
+
|
40
|
+
run :start, number
|
41
|
+
end
|
42
|
+
|
43
|
+
# Stop the servers
|
44
|
+
def stop
|
45
|
+
with_each_server { |n| stop_server n }
|
46
|
+
end
|
47
|
+
|
48
|
+
# Stop a single server
|
49
|
+
def stop_server(number)
|
50
|
+
log "Stopping server on #{server_id(number)} ... "
|
51
|
+
|
52
|
+
run :stop, number
|
53
|
+
end
|
54
|
+
|
55
|
+
# Stop and start the servers.
|
56
|
+
def restart
|
57
|
+
stop
|
58
|
+
sleep 0.1 # Let's breath a bit shall we ?
|
59
|
+
start
|
60
|
+
end
|
61
|
+
|
62
|
+
def server_id(number)
|
63
|
+
if socket
|
64
|
+
socket_for(number)
|
65
|
+
elsif swiftiply?
|
66
|
+
[address, first_port, number].join(':')
|
67
|
+
else
|
68
|
+
[address, number].join(':')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def log_file_for(number)
|
73
|
+
include_server_number log_file, number
|
74
|
+
end
|
75
|
+
|
76
|
+
def pid_file_for(number)
|
77
|
+
include_server_number pid_file, number
|
78
|
+
end
|
79
|
+
|
80
|
+
def socket_for(number)
|
81
|
+
include_server_number socket, number
|
82
|
+
end
|
83
|
+
|
84
|
+
def pid_for(number)
|
85
|
+
File.read(pid_file_for(number)).chomp.to_i
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
# Send the command to the +thin+ script
|
90
|
+
def run(cmd, number)
|
91
|
+
cmd_options = @options.reject { |option, value| CLUSTER_OPTIONS.include?(option) }
|
92
|
+
cmd_options.merge!(:pid => pid_file_for(number), :log => log_file_for(number))
|
93
|
+
if socket
|
94
|
+
cmd_options.merge!(:socket => socket_for(number))
|
95
|
+
elsif swiftiply?
|
96
|
+
cmd_options.merge!(:port => first_port)
|
97
|
+
else
|
98
|
+
cmd_options.merge!(:port => number)
|
99
|
+
end
|
100
|
+
Command.run(cmd, cmd_options)
|
101
|
+
end
|
102
|
+
|
103
|
+
def with_each_server
|
104
|
+
if only
|
105
|
+
if first_port && only < 80
|
106
|
+
# interpret +only+ as a sequence number
|
107
|
+
yield first_port + only
|
108
|
+
else
|
109
|
+
# interpret +only+ as an absolute port number
|
110
|
+
yield only
|
111
|
+
end
|
112
|
+
elsif socket || swiftiply?
|
113
|
+
size.times { |n| yield n }
|
114
|
+
else
|
115
|
+
size.times { |n| yield first_port + n }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Add the server port or number in the filename
|
120
|
+
# so each instance get its own file
|
121
|
+
def include_server_number(path, number)
|
122
|
+
ext = File.extname(path)
|
123
|
+
path.gsub(/#{ext}$/, ".#{number}#{ext}")
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|