michaelyta-thin 1.2.2
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 +257 -0
- data/COPYING +18 -0
- data/README +69 -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/async_app.ru +126 -0
- data/example/async_chat.ru +247 -0
- data/example/async_tailer.ru +100 -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 +175 -0
- data/lib/thin.rb +49 -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 +68 -0
- data/lib/thin/backends/unix_server.rb +51 -0
- data/lib/thin/command.rb +53 -0
- data/lib/thin/connection.rb +214 -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 +158 -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/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 +105 -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 +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 +254 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require 'neverblock'
|
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/../connection')
|
|
3
|
+
|
|
4
|
+
module Thin
|
|
5
|
+
module Backends
|
|
6
|
+
# Backend to act as a TCP socket server.
|
|
7
|
+
class TcpServer < Base
|
|
8
|
+
|
|
9
|
+
# Allow using fibers in the backend.
|
|
10
|
+
def fibered?; true; end
|
|
11
|
+
|
|
12
|
+
def initialize(host, port, options={})
|
|
13
|
+
@host = host
|
|
14
|
+
@port = port.to_i
|
|
15
|
+
@timeout = 30
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def start
|
|
19
|
+
@reactor = NB.reactor
|
|
20
|
+
@server_socket = TCPServer.new(@host, @port)
|
|
21
|
+
@server_socket.listen(511)
|
|
22
|
+
@reactor.attach(:read, @server_socket) do |server, reactor|
|
|
23
|
+
begin
|
|
24
|
+
loop do
|
|
25
|
+
connection = accept_connection
|
|
26
|
+
end
|
|
27
|
+
rescue Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::EINTR
|
|
28
|
+
rescue Exception => e
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
loop do
|
|
32
|
+
begin
|
|
33
|
+
@reactor.run
|
|
34
|
+
break unless @reactor.running?
|
|
35
|
+
rescue Exception => e
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
@server_socket.close
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def stop;@reactor.stop;end
|
|
42
|
+
|
|
43
|
+
alias :stop! :stop
|
|
44
|
+
|
|
45
|
+
def trace=(trace);@trace = trace;end
|
|
46
|
+
|
|
47
|
+
def maxfds=(maxfds);raise "not implemented";end
|
|
48
|
+
|
|
49
|
+
def maxfds;raise "not implemented";end
|
|
50
|
+
|
|
51
|
+
def to_s;"#{@host}:#{@port} (NeverBlock)";end
|
|
52
|
+
|
|
53
|
+
def running?;@reactor.running?;end
|
|
54
|
+
|
|
55
|
+
protected
|
|
56
|
+
|
|
57
|
+
def accept_connection
|
|
58
|
+
socket = @server_socket.accept_nonblock
|
|
59
|
+
connection = ::Thin::ReactorConnection.new(socket, @reactor)
|
|
60
|
+
connection.backend = self
|
|
61
|
+
connection.app = @server.app
|
|
62
|
+
connection.threaded = false
|
|
63
|
+
connection.post_init
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
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,214 @@
|
|
|
1
|
+
require 'neverblock/io/io'
|
|
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 ReactorConnection
|
|
8
|
+
|
|
9
|
+
CONTENT_LENGTH = 'Content-Length'.freeze
|
|
10
|
+
TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
|
|
11
|
+
CHUNKED_REGEXP = /\bchunked\b/i.freeze
|
|
12
|
+
|
|
13
|
+
# whenever we accumulate this buffer size we flush to the socket
|
|
14
|
+
# when flush is called the buffer is emptied (and the conn is blocked)
|
|
15
|
+
BUFFER_SIZE = 256*1024
|
|
16
|
+
|
|
17
|
+
include Logging
|
|
18
|
+
|
|
19
|
+
# Rack application (adapter) served by this connection.
|
|
20
|
+
attr_accessor :app
|
|
21
|
+
|
|
22
|
+
# Backend to the server
|
|
23
|
+
attr_accessor :backend
|
|
24
|
+
|
|
25
|
+
# Current request served by the connection
|
|
26
|
+
attr_accessor :request
|
|
27
|
+
|
|
28
|
+
# Next response sent through the connection
|
|
29
|
+
attr_accessor :response
|
|
30
|
+
|
|
31
|
+
# Calling the application in a threaded allowing
|
|
32
|
+
# concurrent processing of requests.
|
|
33
|
+
attr_writer :threaded
|
|
34
|
+
|
|
35
|
+
# Calling the application in a fiber allowing
|
|
36
|
+
# concurrent processing of requests.
|
|
37
|
+
attr_writer :fibered
|
|
38
|
+
attr_writer :reactor
|
|
39
|
+
attr_writer :socket
|
|
40
|
+
|
|
41
|
+
def initialize(socket, reactor)
|
|
42
|
+
@socket = socket
|
|
43
|
+
@reactor = reactor
|
|
44
|
+
@data = ""
|
|
45
|
+
@sends = 0
|
|
46
|
+
@t = Time.now
|
|
47
|
+
@request = Request.new
|
|
48
|
+
@response = Response.new
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def post_init
|
|
52
|
+
@request = Request.new
|
|
53
|
+
@response = Response.new
|
|
54
|
+
@reactor.attach(:read, @socket) do |socket, reactor|
|
|
55
|
+
begin
|
|
56
|
+
receive_data(@socket.read_nonblock(8*1024))
|
|
57
|
+
rescue Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::EINTR
|
|
58
|
+
rescue Exception => e
|
|
59
|
+
close_connection
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def close_connection(after_writing=false)
|
|
65
|
+
unless after_writing
|
|
66
|
+
@reactor.detach(:read, @socket)
|
|
67
|
+
end
|
|
68
|
+
if after_writing
|
|
69
|
+
@reactor.detach(:read, @socket) unless @reactor.attached?(:read, @socket)
|
|
70
|
+
@socket.close unless @socket.closed?
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def close_connection_after_writing
|
|
75
|
+
close_connection(true)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def send_data(data)
|
|
79
|
+
@data << data
|
|
80
|
+
flush_data if @data.length >= BUFFER_SIZE
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def flush_data
|
|
84
|
+
return if @data.blank?
|
|
85
|
+
begin
|
|
86
|
+
@socket.syswrite(@data)
|
|
87
|
+
rescue Exception => e
|
|
88
|
+
puts e
|
|
89
|
+
close_connection
|
|
90
|
+
return
|
|
91
|
+
end
|
|
92
|
+
@data = ''
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Called when data is received from the client.
|
|
96
|
+
def receive_data(data)
|
|
97
|
+
trace { data }
|
|
98
|
+
post_process(pre_process) if @request.parse(data)
|
|
99
|
+
rescue InvalidRequest => e
|
|
100
|
+
log "!! Invalid request"
|
|
101
|
+
log_error e
|
|
102
|
+
close_connection
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Called when all data was received and the request
|
|
106
|
+
# is ready to be processed.
|
|
107
|
+
|
|
108
|
+
def pre_process
|
|
109
|
+
@app.call(@request.env)
|
|
110
|
+
rescue Exception
|
|
111
|
+
handle_error
|
|
112
|
+
terminate_request
|
|
113
|
+
nil # Signal to post_process that the request could not be processed
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def post_process(result)
|
|
117
|
+
begin
|
|
118
|
+
return unless result
|
|
119
|
+
result = result.to_a
|
|
120
|
+
# Set the Content-Length header if possible
|
|
121
|
+
set_content_length(result) if need_content_length?(result)
|
|
122
|
+
@response.status, @response.headers, @response.body = *result
|
|
123
|
+
log "!! Rack application returned nil body. Probably you wanted it to be an empty string?" if @response.body.nil?
|
|
124
|
+
# Send the response
|
|
125
|
+
@response.each do |chunk|
|
|
126
|
+
trace { chunk }
|
|
127
|
+
send_data chunk
|
|
128
|
+
end
|
|
129
|
+
flush_data
|
|
130
|
+
rescue Exception
|
|
131
|
+
handle_error
|
|
132
|
+
ensure
|
|
133
|
+
terminate_request
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Logs catched exception and closes the connection.
|
|
138
|
+
def handle_error
|
|
139
|
+
log "!! Unexpected error while processing request: #{$!.message}"
|
|
140
|
+
log_error
|
|
141
|
+
close_connection rescue nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def close_request_response
|
|
145
|
+
@request.close rescue nil
|
|
146
|
+
@response.close rescue nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def terminate_request
|
|
150
|
+
close_connection_after_writing rescue nil
|
|
151
|
+
close_request_response
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Allows this connection to be persistent.
|
|
155
|
+
def can_persist!
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Return +true+ if this connection is allowed to stay open and be persistent.
|
|
159
|
+
def can_persist?
|
|
160
|
+
false
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Return +true+ if the connection must be left open
|
|
164
|
+
# and ready to be reused for another request.
|
|
165
|
+
def persistent?
|
|
166
|
+
false
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def threaded?
|
|
170
|
+
false
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# IP Address of the remote client.
|
|
174
|
+
def remote_address
|
|
175
|
+
@request.forwarded_for || socket_address
|
|
176
|
+
rescue Exception
|
|
177
|
+
log_error
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
protected
|
|
182
|
+
|
|
183
|
+
# Returns IP address of peer as a string.
|
|
184
|
+
def socket_address
|
|
185
|
+
Socket.unpack_sockaddr_in(get_peername)[1]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private
|
|
189
|
+
def need_content_length?(result)
|
|
190
|
+
status, headers, body = result
|
|
191
|
+
return false if status == -1
|
|
192
|
+
return false if headers.has_key?(CONTENT_LENGTH)
|
|
193
|
+
return false if (100..199).include?(status) || status == 204 || status == 304
|
|
194
|
+
return false if headers.has_key?(TRANSFER_ENCODING) && headers[TRANSFER_ENCODING] =~ CHUNKED_REGEXP
|
|
195
|
+
return false unless body.kind_of?(String) || body.kind_of?(Array)
|
|
196
|
+
true
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def set_content_length(result)
|
|
200
|
+
headers, body = result[1..2]
|
|
201
|
+
case body
|
|
202
|
+
when String
|
|
203
|
+
# See http://redmine.ruby-lang.org/issues/show/203
|
|
204
|
+
headers[CONTENT_LENGTH] = (body.respond_to?(:bytesize) ? body.bytesize : body.size).to_s
|
|
205
|
+
when Array
|
|
206
|
+
bytes = 0
|
|
207
|
+
body.each do |p|
|
|
208
|
+
bytes += p.respond_to?(:bytesize) ? p.bytesize : p.size
|
|
209
|
+
end
|
|
210
|
+
headers[CONTENT_LENGTH] = bytes.to_s
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
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
|