macournoyer-thin 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +233 -0
- data/COPYING +18 -0
- data/README +77 -0
- data/Rakefile +13 -0
- data/benchmark/abc +51 -0
- data/benchmark/benchmarker.rb +80 -0
- data/benchmark/runner +79 -0
- data/bin/thin +6 -0
- data/example/adapter.rb +32 -0
- data/example/config.ru +23 -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 +173 -0
- data/lib/rack/handler/thin.rb +18 -0
- data/lib/thin.rb +50 -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 +52 -0
- data/lib/thin/connection.rb +186 -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 +163 -0
- data/lib/thin/headers.rb +39 -0
- data/lib/thin/logging.rb +54 -0
- data/lib/thin/request.rb +147 -0
- data/lib/thin/response.rb +99 -0
- data/lib/thin/runner.rb +208 -0
- data/lib/thin/server.rb +241 -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/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 +20 -0
- data/spec/configs/cluster.yml +9 -0
- data/spec/configs/single.yml +9 -0
- data/spec/connection_spec.rb +105 -0
- data/spec/controllers/cluster_spec.rb +212 -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 +83 -0
- data/spec/runner_spec.rb +167 -0
- data/spec/server/builder_spec.rb +44 -0
- data/spec/server/pipelining_spec.rb +109 -0
- data/spec/server/robustness_spec.rb +34 -0
- data/spec/server/stopping_spec.rb +45 -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 +16 -0
- data/tasks/email.erb +30 -0
- data/tasks/ext.rake +42 -0
- data/tasks/gem.rake +108 -0
- data/tasks/rdoc.rake +25 -0
- data/tasks/site.rake +15 -0
- data/tasks/spec.rake +48 -0
- data/tasks/stats.rake +28 -0
- metadata +248 -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,52 @@
|
|
|
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
|
+
case value
|
|
38
|
+
when NilClass,
|
|
39
|
+
TrueClass then args << "--#{name}"
|
|
40
|
+
when FalseClass
|
|
41
|
+
when Array then value.each { |v| args << "--#{name}=#{v.inspect}" }
|
|
42
|
+
else args << "--#{name.to_s.tr('_', '-')}=#{value.inspect}"
|
|
43
|
+
end
|
|
44
|
+
args
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
raise ArgumentError, "Path to thin script can't be found, set Command.script" unless self.class.script
|
|
48
|
+
|
|
49
|
+
"#{self.class.script} #{@name} #{shellified_options.compact.join(' ')}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
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
|
+
# Rack application (adapter) served by this connection.
|
|
15
|
+
attr_accessor :app
|
|
16
|
+
|
|
17
|
+
# Backend to the server
|
|
18
|
+
attr_accessor :backend
|
|
19
|
+
|
|
20
|
+
# Current request served by the connection
|
|
21
|
+
attr_accessor :request
|
|
22
|
+
|
|
23
|
+
# Next response sent through the connection
|
|
24
|
+
attr_accessor :response
|
|
25
|
+
|
|
26
|
+
# Calling the application in a threaded allowing
|
|
27
|
+
# concurrent processing of requests.
|
|
28
|
+
attr_writer :threaded
|
|
29
|
+
|
|
30
|
+
# Get the connection ready to process a request.
|
|
31
|
+
def post_init
|
|
32
|
+
@request = Request.new
|
|
33
|
+
@response = Response.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Called when data is received from the client.
|
|
37
|
+
def receive_data(data)
|
|
38
|
+
trace { data }
|
|
39
|
+
process if @request.parse(data)
|
|
40
|
+
rescue InvalidRequest => e
|
|
41
|
+
log "!! Invalid request"
|
|
42
|
+
log_error e
|
|
43
|
+
close_connection
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Called when all data was received and the request
|
|
47
|
+
# is ready to be processed.
|
|
48
|
+
def process
|
|
49
|
+
if threaded?
|
|
50
|
+
@request.threaded = true
|
|
51
|
+
EventMachine.defer(method(:pre_process), method(:post_process))
|
|
52
|
+
else
|
|
53
|
+
@request.threaded = false
|
|
54
|
+
post_process(pre_process)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def pre_process
|
|
59
|
+
# Add client info to the request env
|
|
60
|
+
@request.remote_address = remote_address
|
|
61
|
+
|
|
62
|
+
# Process the request calling the Rack adapter
|
|
63
|
+
@app.call(@request.env)
|
|
64
|
+
rescue Exception
|
|
65
|
+
handle_error
|
|
66
|
+
terminate_request
|
|
67
|
+
nil # Signal to post_process that the request could not be processed
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def post_process(result)
|
|
71
|
+
return unless result
|
|
72
|
+
|
|
73
|
+
# Set the Content-Length header if possible
|
|
74
|
+
set_content_length(result) if need_content_length?(result)
|
|
75
|
+
|
|
76
|
+
@response.status, @response.headers, @response.body = result
|
|
77
|
+
|
|
78
|
+
log "!! Rack application returned nil body. Probably you wanted it to be an empty string?" if @response.body.nil?
|
|
79
|
+
# Make the response persistent if requested by the client
|
|
80
|
+
@response.persistent! if @request.persistent?
|
|
81
|
+
|
|
82
|
+
# Send the response
|
|
83
|
+
@response.each do |chunk|
|
|
84
|
+
trace { chunk }
|
|
85
|
+
send_data chunk
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# If no more request on that same connection, we close it.
|
|
89
|
+
close_connection_after_writing unless persistent?
|
|
90
|
+
|
|
91
|
+
rescue Exception
|
|
92
|
+
handle_error
|
|
93
|
+
ensure
|
|
94
|
+
terminate_request
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Logs catched exception and closes the connection.
|
|
98
|
+
def handle_error
|
|
99
|
+
log "!! Unexpected error while processing request: #{$!.message}"
|
|
100
|
+
log_error
|
|
101
|
+
close_connection rescue nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Does request and response cleanup (closes open IO streams and
|
|
105
|
+
# deletes created temporary files).
|
|
106
|
+
# Re-initializes response and request if client supports persistent
|
|
107
|
+
# connection.
|
|
108
|
+
def terminate_request
|
|
109
|
+
@request.close rescue nil
|
|
110
|
+
@response.close rescue nil
|
|
111
|
+
|
|
112
|
+
# Prepare the connection for another request if the client
|
|
113
|
+
# supports HTTP pipelining (persistent connection).
|
|
114
|
+
post_init if persistent?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Called when the connection is unbinded from the socket
|
|
118
|
+
# and can no longer be used to process requests.
|
|
119
|
+
def unbind
|
|
120
|
+
@backend.connection_finished(self)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Allows this connection to be persistent.
|
|
124
|
+
def can_persist!
|
|
125
|
+
@can_persist = true
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Return +true+ if this connection is allowed to stay open and be persistent.
|
|
129
|
+
def can_persist?
|
|
130
|
+
@can_persist
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Return +true+ if the connection must be left open
|
|
134
|
+
# and ready to be reused for another request.
|
|
135
|
+
def persistent?
|
|
136
|
+
@can_persist && @response.persistent?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# +true+ if <tt>app.call</tt> will be called inside a thread.
|
|
140
|
+
# You can set all requests as threaded setting <tt>Connection#threaded=true</tt>
|
|
141
|
+
# or on a per-request case returning +true+ in <tt>app.deferred?</tt>.
|
|
142
|
+
def threaded?
|
|
143
|
+
@threaded || (@app.respond_to?(:deferred?) && @app.deferred?(@request.env))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# IP Address of the remote client.
|
|
147
|
+
def remote_address
|
|
148
|
+
@request.forwarded_for || socket_address
|
|
149
|
+
rescue Exception
|
|
150
|
+
log_error
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
protected
|
|
155
|
+
|
|
156
|
+
# Returns IP address of peer as a string.
|
|
157
|
+
def socket_address
|
|
158
|
+
Socket.unpack_sockaddr_in(get_peername)[1]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
def need_content_length?(result)
|
|
163
|
+
status, headers, body = result
|
|
164
|
+
return false if headers.has_key?(CONTENT_LENGTH)
|
|
165
|
+
return false if (100..199).include?(status) || status == 204 || status == 304
|
|
166
|
+
return false if headers.has_key?(TRANSFER_ENCODING) && headers[TRANSFER_ENCODING] =~ CHUNKED_REGEXP
|
|
167
|
+
return false unless body.kind_of?(String) || body.kind_of?(Array)
|
|
168
|
+
true
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def set_content_length(result)
|
|
172
|
+
headers, body = result[1..2]
|
|
173
|
+
case body
|
|
174
|
+
when String
|
|
175
|
+
# See http://redmine.ruby-lang.org/issues/show/203
|
|
176
|
+
headers[CONTENT_LENGTH] = (body.respond_to?(:bytesize) ? body.bytesize : body.size).to_s
|
|
177
|
+
when Array
|
|
178
|
+
bytes = 0
|
|
179
|
+
body.each do |p|
|
|
180
|
+
bytes += p.respond_to?(:bytesize) ? p.bytesize : p.size
|
|
181
|
+
end
|
|
182
|
+
headers[CONTENT_LENGTH] = bytes.to_s
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
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 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
|