karsthammer-passenger 2.2.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README +45 -0
- data/bin/passenger-config +46 -0
- data/bin/passenger-install-apache2-module +217 -0
- data/bin/passenger-install-nginx-module +465 -0
- data/bin/passenger-make-enterprisey +83 -0
- data/bin/passenger-memory-stats +301 -0
- data/bin/passenger-spawn-server +68 -0
- data/bin/passenger-status +125 -0
- data/bin/passenger-stress-test +344 -0
- data/ext/phusion_passenger/extconf.rb +36 -0
- metadata +78 -0
@@ -0,0 +1,125 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Phusion Passenger - http://www.modrails.com/
|
3
|
+
# Copyright (c) 2008, 2009 Phusion
|
4
|
+
#
|
5
|
+
# "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.
|
6
|
+
#
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
+
# of this software and associated documentation files (the "Software"), to deal
|
9
|
+
# in the Software without restriction, including without limitation the rights
|
10
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
# copies of the Software, and to permit persons to whom the Software is
|
12
|
+
# furnished to do so, subject to the following conditions:
|
13
|
+
#
|
14
|
+
# The above copyright notice and this permission notice shall be included in
|
15
|
+
# all copies or substantial portions of the Software.
|
16
|
+
#
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
23
|
+
# THE SOFTWARE.
|
24
|
+
|
25
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
|
26
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../ext"))
|
27
|
+
require 'phusion_passenger/admin_tools/control_process'
|
28
|
+
require 'optparse'
|
29
|
+
|
30
|
+
include PhusionPassenger::AdminTools
|
31
|
+
|
32
|
+
# ANSI color codes
|
33
|
+
RESET = "\e[0m"
|
34
|
+
BOLD = "\e[1m"
|
35
|
+
YELLOW = "\e[33m"
|
36
|
+
BLACK_BG = "\e[40m"
|
37
|
+
BLUE_BG = "\e[44m"
|
38
|
+
|
39
|
+
def show_status(control_process, options = {})
|
40
|
+
case options[:show]
|
41
|
+
when 'pool'
|
42
|
+
begin
|
43
|
+
text = control_process.status
|
44
|
+
rescue SystemCallError => e
|
45
|
+
STDERR.puts "*** ERROR: Cannot query status for Passenger instance #{control_process.pid}:"
|
46
|
+
STDERR.puts e.to_s
|
47
|
+
exit 2
|
48
|
+
end
|
49
|
+
|
50
|
+
# Colorize output
|
51
|
+
text.gsub!(/^(----)(.*)$/, YELLOW + BLUE_BG + BOLD + '\1\2' + RESET)
|
52
|
+
|
53
|
+
puts text
|
54
|
+
|
55
|
+
when 'backtraces'
|
56
|
+
begin
|
57
|
+
text = control_process.backtraces
|
58
|
+
rescue SystemCallError => e
|
59
|
+
STDERR.puts "*** ERROR: Cannot query status for Passenger instance #{control_process.pid}:"
|
60
|
+
STDERR.puts e.to_s
|
61
|
+
exit 2
|
62
|
+
end
|
63
|
+
|
64
|
+
# Colorize output
|
65
|
+
text.gsub!(/^(Thread .*:)$/, BLACK_BG + YELLOW + '\1' + RESET)
|
66
|
+
text.gsub!(/^( +in '.*? )(.*?)\(/, '\1' + BOLD + '\2' + RESET + '(')
|
67
|
+
|
68
|
+
puts text
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def start
|
73
|
+
options = { :show => 'pool' }
|
74
|
+
parser = OptionParser.new do |opts|
|
75
|
+
opts.banner = "Usage: passenger-status [options] [Phusion Passenger's PID]"
|
76
|
+
opts.separator ""
|
77
|
+
opts.separator "Tool for inspecting Phusion Passenger's internal status."
|
78
|
+
opts.separator ""
|
79
|
+
|
80
|
+
opts.separator "Options:"
|
81
|
+
opts.on("--show=pool|backtraces", String,
|
82
|
+
"Whether to show the pool's contents or\n" <<
|
83
|
+
"#{' ' * 37}the backtraces of all threads.") do |what|
|
84
|
+
if what !~ /\A(pool|backtraces)\Z/
|
85
|
+
STDERR.puts "Invalid argument for --show."
|
86
|
+
exit 1
|
87
|
+
else
|
88
|
+
options[:show] = what
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
begin
|
93
|
+
parser.parse!
|
94
|
+
rescue OptionParser::ParseError => e
|
95
|
+
puts e
|
96
|
+
puts
|
97
|
+
puts "Please see '--help' for valid options."
|
98
|
+
exit 1
|
99
|
+
end
|
100
|
+
|
101
|
+
if ARGV.empty?
|
102
|
+
control_processes = ControlProcess.list
|
103
|
+
if control_processes.empty?
|
104
|
+
STDERR.puts("ERROR: Phusion Passenger doesn't seem to be running.")
|
105
|
+
exit 2
|
106
|
+
elsif control_processes.size == 1
|
107
|
+
show_status(control_processes.first, options)
|
108
|
+
else
|
109
|
+
puts "It appears that multiple Passenger instances are running. Please select a"
|
110
|
+
puts "specific one by running:"
|
111
|
+
puts
|
112
|
+
puts " passenger-status <PID>"
|
113
|
+
puts
|
114
|
+
puts "The following Passenger instances are running:"
|
115
|
+
control_processes.each do |control|
|
116
|
+
puts " PID: #{control.pid}"
|
117
|
+
end
|
118
|
+
exit 1
|
119
|
+
end
|
120
|
+
else
|
121
|
+
show_status(ControlProcess.for_pid(ARGV[0].to_i), options)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
start
|
@@ -0,0 +1,344 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Phusion Passenger - http://www.modrails.com/
|
3
|
+
# Copyright (c) 2008, 2009 Phusion
|
4
|
+
#
|
5
|
+
# "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.
|
6
|
+
#
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
+
# of this software and associated documentation files (the "Software"), to deal
|
9
|
+
# in the Software without restriction, including without limitation the rights
|
10
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
# copies of the Software, and to permit persons to whom the Software is
|
12
|
+
# furnished to do so, subject to the following conditions:
|
13
|
+
#
|
14
|
+
# The above copyright notice and this permission notice shall be included in
|
15
|
+
# all copies or substantial portions of the Software.
|
16
|
+
#
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
23
|
+
# THE SOFTWARE.
|
24
|
+
|
25
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
|
26
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../ext"))
|
27
|
+
require 'rubygems'
|
28
|
+
require 'optparse'
|
29
|
+
require 'socket'
|
30
|
+
require 'thread'
|
31
|
+
require 'phusion_passenger/platform_info'
|
32
|
+
require 'phusion_passenger/message_channel'
|
33
|
+
require 'phusion_passenger/utils'
|
34
|
+
|
35
|
+
include PhusionPassenger
|
36
|
+
include PhusionPassenger::Utils
|
37
|
+
include PlatformInfo
|
38
|
+
|
39
|
+
# A thread or a process, depending on the Ruby VM implementation.
|
40
|
+
class Subprocess
|
41
|
+
attr_accessor :channel
|
42
|
+
|
43
|
+
def initialize(name, &block)
|
44
|
+
if RUBY_PLATFORM == "java"
|
45
|
+
a, b = UNIXSocket.pair
|
46
|
+
@thread = Thread.new do
|
47
|
+
block.call(true, MessageChannel.new(b))
|
48
|
+
end
|
49
|
+
@channel = MessageChannel.new(a)
|
50
|
+
@thread_channel = b
|
51
|
+
else
|
52
|
+
a, b = UNIXSocket.pair
|
53
|
+
@pid = safe_fork(name) do
|
54
|
+
a.close
|
55
|
+
$0 = name
|
56
|
+
Process.setsid
|
57
|
+
block.call(false, MessageChannel.new(b))
|
58
|
+
end
|
59
|
+
b.close
|
60
|
+
@channel = MessageChannel.new(a)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def stop
|
65
|
+
if RUBY_PLATFORM == "java"
|
66
|
+
@thread.terminate
|
67
|
+
@channel.close
|
68
|
+
@thread_channel.close
|
69
|
+
else
|
70
|
+
Process.kill('SIGKILL', @pid) rescue nil
|
71
|
+
Process.waitpid(@pid) rescue nil
|
72
|
+
@channel.close
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class StressTester
|
78
|
+
def start
|
79
|
+
@options = parse_options
|
80
|
+
load_hawler
|
81
|
+
|
82
|
+
Thread.abort_on_exception = true
|
83
|
+
if GC.respond_to?(:copy_on_write_friendly=)
|
84
|
+
GC.copy_on_write_friendly = true
|
85
|
+
end
|
86
|
+
@terminal_height = ENV['LINES'] ? ENV['LINES'].to_i : 24
|
87
|
+
@terminal_width = ENV['COLUMNS'] ? ENV['COLUMNS'].to_i : 80
|
88
|
+
|
89
|
+
if Process.euid != 0
|
90
|
+
puts "*** WARNING: This program might not be able to restart " <<
|
91
|
+
"Apache because it's not running as root. Please run " <<
|
92
|
+
"this tool as root."
|
93
|
+
puts
|
94
|
+
puts "Press Enter to continue..."
|
95
|
+
begin
|
96
|
+
STDIN.readline
|
97
|
+
rescue Interrupt
|
98
|
+
exit 1
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
run_crawlers
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse_options
|
106
|
+
options = {
|
107
|
+
:concurrency => 20,
|
108
|
+
:depth => 20,
|
109
|
+
:nice => true,
|
110
|
+
:apache_restart_interval => 24 * 60,
|
111
|
+
:app_restart_interval => 55
|
112
|
+
}
|
113
|
+
parser = OptionParser.new do |opts|
|
114
|
+
opts.banner = "Usage: passenger-stress-test <hostname> <app_root> [options]\n\n" <<
|
115
|
+
"Stress test the given (Passenger-powered) website by:\n" <<
|
116
|
+
" * crawling it with multiple concurrently running crawlers.\n" <<
|
117
|
+
" * gracefully restarting Apache at random times (please point the 'APXS2'\n" <<
|
118
|
+
" variable to your Apache's 'apxs' binary).\n" <<
|
119
|
+
" * restarting the target (Passenger-powered) application at random time.\n" <<
|
120
|
+
"\n" <<
|
121
|
+
"Example:\n" <<
|
122
|
+
" passenger-stress-test mywebsite.com /webapps/mywebsite\n" <<
|
123
|
+
"\n"
|
124
|
+
|
125
|
+
opts.separator "Options:"
|
126
|
+
opts.on("-c", "--concurrency N", Integer,
|
127
|
+
"Number of crawlers to start (default = #{options[:concurrency]})") do |v|
|
128
|
+
options[:concurrency] = v
|
129
|
+
end
|
130
|
+
opts.on("-p", "--apache-restart-interval N", Integer,
|
131
|
+
"Gracefully restart Apache after N minutes\n" <<
|
132
|
+
(" " * 37) << "(default = #{options[:apache_restart_interval]})") do |v|
|
133
|
+
options[:apache_restart_interval] = v
|
134
|
+
end
|
135
|
+
opts.on("-a", "--app-restart-interval N", Integer,
|
136
|
+
"Restart the application after N minutes\n" <<
|
137
|
+
(" " * 37) << "(default = #{options[:app_restart_interval]})") do |v|
|
138
|
+
options[:app_restart_interval] = v
|
139
|
+
end
|
140
|
+
opts.on("-h", "--help", "Show this message") do
|
141
|
+
puts opts
|
142
|
+
exit
|
143
|
+
end
|
144
|
+
end
|
145
|
+
parser.parse!
|
146
|
+
|
147
|
+
options[:host] = ARGV[0]
|
148
|
+
options[:app_root] = ARGV[1]
|
149
|
+
if !options[:host] || !options[:app_root]
|
150
|
+
puts parser
|
151
|
+
exit 1
|
152
|
+
end
|
153
|
+
return options
|
154
|
+
end
|
155
|
+
|
156
|
+
def load_hawler
|
157
|
+
begin
|
158
|
+
require 'hawler'
|
159
|
+
rescue LoadError
|
160
|
+
STDERR.puts "This tool requires Hawler (http://tinyurl.com/ywgk6x). Please install it with:"
|
161
|
+
STDERR.puts
|
162
|
+
STDERR.puts " gem install --source http://spoofed.org/files/hawler/ hawler"
|
163
|
+
exit 1
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def run_crawlers
|
168
|
+
@started = false
|
169
|
+
@crawlers = []
|
170
|
+
|
171
|
+
# Start crawler processes.
|
172
|
+
GC.start if GC.copy_on_write_friendly?
|
173
|
+
@options[:concurrency].times do |i|
|
174
|
+
STDOUT.write("Starting crawler #{i + 1} of #{@options[:concurrency]}...\n")
|
175
|
+
STDOUT.flush
|
176
|
+
process = Subprocess.new("crawler #{i + 1}") do |is_thread, channel|
|
177
|
+
if !is_thread && @options[:nice]
|
178
|
+
system("renice 1 #{Process.pid} >/dev/null 2>/dev/null")
|
179
|
+
end
|
180
|
+
while true
|
181
|
+
crawl!(i + 1, channel)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
@crawlers << {
|
185
|
+
:id => i + 1,
|
186
|
+
:process => process,
|
187
|
+
:channel => process.channel,
|
188
|
+
:mutex => Mutex.new,
|
189
|
+
:current_uri => nil,
|
190
|
+
:crawled => 0
|
191
|
+
}
|
192
|
+
end
|
193
|
+
|
194
|
+
puts
|
195
|
+
if RUBY_PLATFORM != "java"
|
196
|
+
# 'sleep' b0rks when running in JRuby?
|
197
|
+
sleep 1
|
198
|
+
end
|
199
|
+
begin
|
200
|
+
$0 = "Passenger Crawler: control process"
|
201
|
+
io_to_crawler = {}
|
202
|
+
ios = []
|
203
|
+
@crawlers.each do |crawler|
|
204
|
+
io_to_crawler[crawler[:channel].io] = crawler
|
205
|
+
ios << crawler[:channel].io
|
206
|
+
end
|
207
|
+
|
208
|
+
# Tell each crawler to start crawling.
|
209
|
+
@crawlers.each do |crawler|
|
210
|
+
crawler[:channel].write("start")
|
211
|
+
end
|
212
|
+
|
213
|
+
# Show progress periodically.
|
214
|
+
@start_time = Time.now
|
215
|
+
progress_reporter = Thread.new(&method(:report_progress))
|
216
|
+
@next_apache_restart = Time.now + @options[:apache_restart_interval] * 60
|
217
|
+
apache_restarter = Thread.new(&method(:restart_apache))
|
218
|
+
@next_app_restart = Time.now + @options[:app_restart_interval] * 60
|
219
|
+
app_restarter = Thread.new(&method(:restart_app))
|
220
|
+
|
221
|
+
while true
|
222
|
+
note_progress(ios, io_to_crawler)
|
223
|
+
end
|
224
|
+
rescue Interrupt
|
225
|
+
trap('SIGINT') {}
|
226
|
+
puts "Shutting down..."
|
227
|
+
@done = true
|
228
|
+
@crawlers.each do |crawler|
|
229
|
+
STDOUT.write("Stopping crawler #{crawler[:id]} of #{@options[:concurrency]}...\r")
|
230
|
+
STDOUT.flush
|
231
|
+
crawler[:process].stop
|
232
|
+
end
|
233
|
+
progress_reporter.join if progress_reporter
|
234
|
+
apache_restarter.join if apache_restarter
|
235
|
+
app_restarter.join if app_restarter
|
236
|
+
puts
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def note_progress(ios, io_to_crawler)
|
241
|
+
select(ios)[0].each do |io|
|
242
|
+
crawler = io_to_crawler[io]
|
243
|
+
uri = crawler[:channel].read[0]
|
244
|
+
crawler[:mutex].synchronize do
|
245
|
+
crawler[:current_uri] = uri
|
246
|
+
crawler[:crawled] += 1
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def report_progress
|
252
|
+
while !@done
|
253
|
+
output = "\n" * @terminal_height
|
254
|
+
output << "### Running for #{duration(Time.now.to_i - @start_time.to_i)}\n"
|
255
|
+
@crawlers.each do |crawler|
|
256
|
+
crawler[:mutex].synchronize do
|
257
|
+
line = sprintf("Crawler %-2d: %-3d -> %s",
|
258
|
+
crawler[:id],
|
259
|
+
crawler[:crawled],
|
260
|
+
crawler[:current_uri])
|
261
|
+
output << sprintf("%-#{@terminal_width}s\n", line)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
output << "Next Apache restart: in #{duration(@next_apache_restart.to_i - Time.now.to_i)}\n"
|
265
|
+
output << "Next app restart : in #{duration(@next_app_restart.to_i - Time.now.to_i)}\n"
|
266
|
+
STDOUT.write(output)
|
267
|
+
sleep 0.5
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def restart_apache
|
272
|
+
while !@done
|
273
|
+
if Time.now > @next_apache_restart
|
274
|
+
@next_apache_restart = Time.now + @options[:apache_restart_interval] * 60
|
275
|
+
system("#{HTTPD} -k graceful")
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def restart_app
|
281
|
+
while !@done
|
282
|
+
if Time.now > @next_app_restart
|
283
|
+
@next_app_restart = Time.now + @options[:app_restart_interval] * 60
|
284
|
+
system("touch #{@options[:app_root]}/tmp/restart.txt")
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def duration(seconds)
|
290
|
+
result = ""
|
291
|
+
if seconds >= 60
|
292
|
+
minutes = (seconds / 60)
|
293
|
+
if minutes >= 60
|
294
|
+
hours = minutes / 60
|
295
|
+
minutes = minutes % 60
|
296
|
+
if hours == 1
|
297
|
+
result << "#{hours} hour "
|
298
|
+
else
|
299
|
+
result << "#{hours} hours "
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
seconds = seconds % 60
|
304
|
+
if minutes == 1
|
305
|
+
result << "#{minutes} minute "
|
306
|
+
else
|
307
|
+
result << "#{minutes} minutes "
|
308
|
+
end
|
309
|
+
end
|
310
|
+
result << "#{seconds} seconds"
|
311
|
+
return result
|
312
|
+
end
|
313
|
+
|
314
|
+
def crawl!(id, channel)
|
315
|
+
progress_reporter = lambda do |uri, referer, response|
|
316
|
+
begin
|
317
|
+
if !@started
|
318
|
+
# At the beginning, wait until the control process
|
319
|
+
# tells us to start.
|
320
|
+
@started = true
|
321
|
+
channel.read
|
322
|
+
end
|
323
|
+
channel.write(uri, referer, response)
|
324
|
+
rescue
|
325
|
+
if RUBY_PLATFORM == "java"
|
326
|
+
Thread.current.terminate
|
327
|
+
else
|
328
|
+
Process.kill('SIGKILL', Process.pid)
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
crawler = Hawler.new(@options[:host], progress_reporter)
|
333
|
+
if RUBY_PLATFORM == "java"
|
334
|
+
trap('SIGINT') do
|
335
|
+
raise Interrupt, "Interrupted"
|
336
|
+
end
|
337
|
+
end
|
338
|
+
crawler.recurse = true
|
339
|
+
crawler.depth = @options[:depth]
|
340
|
+
crawler.start
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
StressTester.new.start
|