rack-rabbit 0.5.0
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.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/EXAMPLES.md +212 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +42 -0
- data/LICENSE +21 -0
- data/README.md +412 -0
- data/Rakefile +5 -0
- data/bin/rack-rabbit +96 -0
- data/bin/rr +99 -0
- data/lib/rack-rabbit.rb +63 -0
- data/lib/rack-rabbit/adapter.rb +85 -0
- data/lib/rack-rabbit/adapter/amqp.rb +114 -0
- data/lib/rack-rabbit/adapter/bunny.rb +87 -0
- data/lib/rack-rabbit/adapter/mock.rb +92 -0
- data/lib/rack-rabbit/client.rb +181 -0
- data/lib/rack-rabbit/config.rb +260 -0
- data/lib/rack-rabbit/handler.rb +44 -0
- data/lib/rack-rabbit/message.rb +95 -0
- data/lib/rack-rabbit/middleware/program_name.rb +34 -0
- data/lib/rack-rabbit/response.rb +43 -0
- data/lib/rack-rabbit/server.rb +263 -0
- data/lib/rack-rabbit/signals.rb +62 -0
- data/lib/rack-rabbit/subscriber.rb +77 -0
- data/lib/rack-rabbit/worker.rb +84 -0
- data/rack-rabbit.gemspec +26 -0
- data/test/apps/config.ru +7 -0
- data/test/apps/custom.conf +27 -0
- data/test/apps/custom.ru +7 -0
- data/test/apps/empty.conf +1 -0
- data/test/apps/error.ru +7 -0
- data/test/apps/mirror.ru +19 -0
- data/test/apps/sinatra.ru +37 -0
- data/test/apps/sleep.ru +21 -0
- data/test/test_case.rb +154 -0
- data/test/unit/middleware/test_program_name.rb +32 -0
- data/test/unit/test_client.rb +275 -0
- data/test/unit/test_config.rb +403 -0
- data/test/unit/test_handler.rb +92 -0
- data/test/unit/test_message.rb +213 -0
- data/test/unit/test_response.rb +59 -0
- data/test/unit/test_signals.rb +45 -0
- data/test/unit/test_subscriber.rb +140 -0
- metadata +91 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rack-rabbit/response'
|
2
|
+
|
3
|
+
module RackRabbit
|
4
|
+
class Handler
|
5
|
+
|
6
|
+
#--------------------------------------------------------------------------
|
7
|
+
|
8
|
+
attr_reader :app, :config, :logger
|
9
|
+
|
10
|
+
#--------------------------------------------------------------------------
|
11
|
+
|
12
|
+
def initialize(app, config)
|
13
|
+
@app = app
|
14
|
+
@config = config
|
15
|
+
@logger = config.logger
|
16
|
+
end
|
17
|
+
|
18
|
+
#--------------------------------------------------------------------------
|
19
|
+
|
20
|
+
def handle(message)
|
21
|
+
|
22
|
+
env = message.get_rack_env(config.rack_env)
|
23
|
+
|
24
|
+
status, headers, body_chunks = app.call(env)
|
25
|
+
|
26
|
+
body = []
|
27
|
+
body_chunks.each{|c| body << c }
|
28
|
+
body_chunks.close if body_chunks.respond_to?(:close)
|
29
|
+
|
30
|
+
Response.new(status, headers, body.join)
|
31
|
+
|
32
|
+
rescue Exception => e # don't let exceptions bubble out of worker process
|
33
|
+
|
34
|
+
logger.error e
|
35
|
+
logger.error e.backtrace.join("\n")
|
36
|
+
|
37
|
+
Response.new(500, {}, "Internal Server Error")
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
#--------------------------------------------------------------------------
|
42
|
+
|
43
|
+
end # class Handler
|
44
|
+
end # module RackRabbit
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
module RackRabbit
|
4
|
+
class Message
|
5
|
+
|
6
|
+
#--------------------------------------------------------------------------
|
7
|
+
|
8
|
+
attr_reader :delivery_tag, :reply_to, :correlation_id,
|
9
|
+
:body, :headers,
|
10
|
+
:method, :uri, :path, :query, :status,
|
11
|
+
:content_type, :content_encoding, :content_length,
|
12
|
+
:rabbit
|
13
|
+
|
14
|
+
def initialize(delivery_tag, properties, body, rabbit)
|
15
|
+
@delivery_tag = delivery_tag
|
16
|
+
@reply_to = properties.reply_to
|
17
|
+
@correlation_id = properties.correlation_id
|
18
|
+
@body = body
|
19
|
+
@headers = properties.headers || {}
|
20
|
+
@method = headers.delete(RackRabbit::HEADER::METHOD) || :GET
|
21
|
+
@uri = headers.delete(RackRabbit::HEADER::PATH) || ""
|
22
|
+
@status = headers.delete(RackRabbit::HEADER::STATUS)
|
23
|
+
@path, @query = uri.split(/\?/)
|
24
|
+
@content_type = properties.content_type
|
25
|
+
@content_encoding = properties.content_encoding
|
26
|
+
@content_length = body.nil? ? 0 : body.length
|
27
|
+
@rabbit = rabbit
|
28
|
+
end
|
29
|
+
|
30
|
+
#--------------------------------------------------------------------------
|
31
|
+
|
32
|
+
def get_rack_env(defaults = {})
|
33
|
+
|
34
|
+
defaults.merge({
|
35
|
+
'rabbit.message' => self,
|
36
|
+
'rack.input' => StringIO.new(body || ""),
|
37
|
+
'REQUEST_METHOD' => method,
|
38
|
+
'REQUEST_PATH' => uri,
|
39
|
+
'PATH_INFO' => path,
|
40
|
+
'QUERY_STRING' => query,
|
41
|
+
'CONTENT_TYPE' => "#{content_type || 'text/plain'}; charset=\"#{content_encoding || 'utf-8'}\"",
|
42
|
+
'CONTENT_LENGTH' => content_length
|
43
|
+
}).merge(headers)
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
#--------------------------------------------------------------------------
|
48
|
+
|
49
|
+
def should_reply?
|
50
|
+
!reply_to.nil?
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_reply_properties(response, config)
|
54
|
+
return {
|
55
|
+
:app_id => config.app_id,
|
56
|
+
:routing_key => reply_to,
|
57
|
+
:correlation_id => correlation_id,
|
58
|
+
:timestamp => Time.now.to_i,
|
59
|
+
:headers => response.headers.merge(RackRabbit::HEADER::STATUS => response.status),
|
60
|
+
:content_type => response.headers[RackRabbit::HEADER::CONTENT_TYPE],
|
61
|
+
:content_encoding => response.headers[RackRabbit::HEADER::CONTENT_ENCODING]
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
#--------------------------------------------------------------------------
|
66
|
+
|
67
|
+
def ack
|
68
|
+
raise RuntimeError, "already acknowledged" if acknowledged?
|
69
|
+
raise RuntimeError, "already rejected" if rejected?
|
70
|
+
@acknowledged = true
|
71
|
+
rabbit.ack(delivery_tag)
|
72
|
+
end
|
73
|
+
|
74
|
+
def reject
|
75
|
+
raise RuntimeError, "already acknowledged" if acknowledged?
|
76
|
+
raise RuntimeError, "already rejected" if rejected?
|
77
|
+
@rejected = true
|
78
|
+
rabbit.reject(delivery_tag)
|
79
|
+
end
|
80
|
+
|
81
|
+
def acknowledged?
|
82
|
+
@acknowledged == true
|
83
|
+
end
|
84
|
+
|
85
|
+
def rejected?
|
86
|
+
@rejected == true
|
87
|
+
end
|
88
|
+
|
89
|
+
#--------------------------------------------------------------------------
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module RackRabbit
|
2
|
+
module Middleware
|
3
|
+
class ProgramName
|
4
|
+
|
5
|
+
def initialize(app, default = "waiting for request")
|
6
|
+
@app = app
|
7
|
+
@default = default
|
8
|
+
set_program_name(default)
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
info = "#{env['REQUEST_METHOD']} #{env['REQUEST_PATH'] || env['PATH_INFO']}"
|
13
|
+
set_program_name info
|
14
|
+
status, headers, body = @app.call(env)
|
15
|
+
set_program_name @default
|
16
|
+
[status, headers, body]
|
17
|
+
end
|
18
|
+
|
19
|
+
def set_program_name(name)
|
20
|
+
$PROGRAM_NAME = sanitize($PROGRAM_NAME.split(" -- ")[0] + " -- #{name}")[0..200]
|
21
|
+
end
|
22
|
+
|
23
|
+
def sanitize(value)
|
24
|
+
if value.valid_encoding?
|
25
|
+
value.force_encoding('utf-8')
|
26
|
+
else
|
27
|
+
value.chars.select(&:valid_encoding?).join.force_encoding('utf-8')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end # class ProgramName
|
32
|
+
end # module Middleware
|
33
|
+
end # module RackRabbit
|
34
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module RackRabbit
|
2
|
+
class Response
|
3
|
+
|
4
|
+
#--------------------------------------------------------------------------
|
5
|
+
|
6
|
+
attr_reader :status, :headers, :body
|
7
|
+
|
8
|
+
def initialize(status, headers, body)
|
9
|
+
@status = status
|
10
|
+
@headers = headers
|
11
|
+
@body = body
|
12
|
+
end
|
13
|
+
|
14
|
+
#--------------------------------------------------------------------------
|
15
|
+
|
16
|
+
def succeeded?
|
17
|
+
(200..299).include?(status)
|
18
|
+
end
|
19
|
+
|
20
|
+
def failed?
|
21
|
+
!succeeded?
|
22
|
+
end
|
23
|
+
|
24
|
+
#--------------------------------------------------------------------------
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
if succeeded?
|
28
|
+
body
|
29
|
+
else
|
30
|
+
case status
|
31
|
+
when RackRabbit::STATUS::BAD_REQUEST then "#{status} Bad Request"
|
32
|
+
when RackRabbit::STATUS::NOT_FOUND then "#{status} Not Found"
|
33
|
+
when RackRabbit::STATUS::FAILED then "#{status} Internal Server Error"
|
34
|
+
else
|
35
|
+
status.to_s
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
#--------------------------------------------------------------------------
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,263 @@
|
|
1
|
+
require 'rack/builder'
|
2
|
+
require 'rack/server'
|
3
|
+
|
4
|
+
require 'rack-rabbit'
|
5
|
+
require 'rack-rabbit/config'
|
6
|
+
require 'rack-rabbit/signals'
|
7
|
+
require 'rack-rabbit/worker'
|
8
|
+
require 'rack-rabbit/middleware/program_name'
|
9
|
+
|
10
|
+
module RackRabbit
|
11
|
+
class Server
|
12
|
+
|
13
|
+
#--------------------------------------------------------------------------
|
14
|
+
|
15
|
+
attr_reader :app,
|
16
|
+
:config,
|
17
|
+
:logger,
|
18
|
+
:server_pid,
|
19
|
+
:worker_pids,
|
20
|
+
:killed_pids,
|
21
|
+
:signals
|
22
|
+
|
23
|
+
def initialize(options)
|
24
|
+
@config = Config.new(options)
|
25
|
+
@logger = config.logger
|
26
|
+
@server_pid = $$
|
27
|
+
@worker_pids = []
|
28
|
+
@killed_pids = []
|
29
|
+
@signals = Signals.new
|
30
|
+
end
|
31
|
+
|
32
|
+
#--------------------------------------------------------------------------
|
33
|
+
|
34
|
+
def run
|
35
|
+
|
36
|
+
check_pid
|
37
|
+
|
38
|
+
if config.daemonize
|
39
|
+
daemonize
|
40
|
+
elsif config.logfile
|
41
|
+
redirect_output
|
42
|
+
end
|
43
|
+
|
44
|
+
write_pid
|
45
|
+
|
46
|
+
logger.info "RUNNING #{config.app_id} (#{config.rack_file}) #{'DAEMONIZED' if config.daemonize}"
|
47
|
+
logger.info " rabbit : #{config.rabbit}"
|
48
|
+
logger.info " exchange : #{config.exchange} (#{config.exchange_type})" if config.exchange
|
49
|
+
logger.info " queue : #{config.queue}" if config.queue
|
50
|
+
logger.info " route : #{config.routing_key}" if config.routing_key
|
51
|
+
logger.info " workers : #{config.workers}"
|
52
|
+
logger.info " preload : true" if config.preload_app
|
53
|
+
logger.info " logfile : #{config.logfile}" unless config.logfile.nil?
|
54
|
+
logger.info " pidfile : #{config.pidfile}" unless config.pidfile.nil?
|
55
|
+
|
56
|
+
load_app if config.preload_app
|
57
|
+
|
58
|
+
trap_server_signals
|
59
|
+
manage_workers
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
#--------------------------------------------------------------------------
|
64
|
+
|
65
|
+
def manage_workers
|
66
|
+
while true
|
67
|
+
|
68
|
+
maintain_worker_count
|
69
|
+
|
70
|
+
sig = signals.pop # BLOCKS until there is a signal
|
71
|
+
case sig
|
72
|
+
|
73
|
+
when :INT then shutdown(:INT)
|
74
|
+
when :QUIT then shutdown(:QUIT)
|
75
|
+
when :TERM then shutdown(:TERM)
|
76
|
+
|
77
|
+
when :HUP
|
78
|
+
reload
|
79
|
+
|
80
|
+
when :CHLD
|
81
|
+
reap_workers
|
82
|
+
|
83
|
+
when :TTIN
|
84
|
+
config.workers [config.max_workers, config.workers + 1].min
|
85
|
+
|
86
|
+
when :TTOU
|
87
|
+
config.workers [config.min_workers, config.workers - 1].max
|
88
|
+
|
89
|
+
else
|
90
|
+
raise RuntimeError, "unknown signal #{sig}"
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
#--------------------------------------------------------------------------
|
98
|
+
|
99
|
+
def reload
|
100
|
+
logger.info "RELOADING"
|
101
|
+
config.reload
|
102
|
+
load_app if config.preload_app
|
103
|
+
kill_all_workers(:QUIT) # they will respawn automatically
|
104
|
+
end
|
105
|
+
|
106
|
+
def maintain_worker_count
|
107
|
+
unless shutting_down?
|
108
|
+
diff = worker_pids.length - config.workers
|
109
|
+
if diff > 0
|
110
|
+
diff.times { kill_random_worker(:QUIT) }
|
111
|
+
elsif diff < 0
|
112
|
+
(-diff).times { spawn_worker }
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def spawn_worker
|
118
|
+
config.before_fork(self)
|
119
|
+
worker_pids << fork do
|
120
|
+
signals.close
|
121
|
+
load_app unless config.preload_app
|
122
|
+
worker = Worker.new(config, app)
|
123
|
+
config.after_fork(self, worker)
|
124
|
+
worker.run
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def kill_random_worker(sig)
|
129
|
+
kill_worker(sig, worker_pids.sample) # choose a random wpid
|
130
|
+
end
|
131
|
+
|
132
|
+
def kill_all_workers(sig)
|
133
|
+
kill_worker(sig, worker_pids.last) until worker_pids.empty?
|
134
|
+
end
|
135
|
+
|
136
|
+
def kill_worker(sig, wpid)
|
137
|
+
worker_pids.delete(wpid)
|
138
|
+
killed_pids.push(wpid)
|
139
|
+
Process.kill(sig, wpid)
|
140
|
+
end
|
141
|
+
|
142
|
+
def reap_workers
|
143
|
+
while true
|
144
|
+
wpid = Process.waitpid(-1, Process::WNOHANG)
|
145
|
+
return if wpid.nil?
|
146
|
+
worker_pids.delete(wpid)
|
147
|
+
killed_pids.delete(wpid)
|
148
|
+
end
|
149
|
+
rescue Errno::ECHILD
|
150
|
+
end
|
151
|
+
|
152
|
+
#--------------------------------------------------------------------------
|
153
|
+
|
154
|
+
def shutdown(sig)
|
155
|
+
@shutting_down = true
|
156
|
+
kill_all_workers(sig)
|
157
|
+
Process.waitall
|
158
|
+
logger.info "#{RackRabbit.friendly_signal(sig)} server"
|
159
|
+
exit
|
160
|
+
end
|
161
|
+
|
162
|
+
def shutting_down?
|
163
|
+
@shutting_down
|
164
|
+
end
|
165
|
+
|
166
|
+
#==========================================================================
|
167
|
+
# DAEMONIZING, PID MANAGEMENT, and OUTPUT REDIRECTION
|
168
|
+
#==========================================================================
|
169
|
+
|
170
|
+
def daemonize
|
171
|
+
exit if fork
|
172
|
+
Process.setsid
|
173
|
+
exit if fork
|
174
|
+
Dir.chdir "/"
|
175
|
+
redirect_output
|
176
|
+
logger.master_pid = $$ if logger.respond_to?(:master_pid) # inform logger of new master_pid so it can continue to distinguish between "SERVER" and "worker" in log preamble
|
177
|
+
end
|
178
|
+
|
179
|
+
def redirect_output
|
180
|
+
if logfile = config.logfile
|
181
|
+
logfile = File.expand_path(logfile)
|
182
|
+
FileUtils.mkdir_p(File.dirname(logfile), :mode => 0755)
|
183
|
+
FileUtils.touch logfile
|
184
|
+
File.chmod(0644, logfile)
|
185
|
+
$stderr.reopen(logfile, 'a')
|
186
|
+
$stdout.reopen($stderr)
|
187
|
+
$stdout.sync = $stderr.sync = true
|
188
|
+
else
|
189
|
+
$stderr.reopen('/dev/null', 'a')
|
190
|
+
$stdout.reopen($stderr)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def write_pid
|
195
|
+
pidfile = config.pidfile
|
196
|
+
if pidfile
|
197
|
+
begin
|
198
|
+
File.open(pidfile, ::File::CREAT | ::File::EXCL | ::File::WRONLY){|f| f.write("#{Process.pid}") }
|
199
|
+
at_exit { File.delete(pidfile) if File.exists?(pidfile) }
|
200
|
+
rescue Errno::EEXIST
|
201
|
+
check_pid
|
202
|
+
retry
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def check_pid
|
208
|
+
pidfile = config.pidfile
|
209
|
+
if pidfile
|
210
|
+
case pid_status(pidfile)
|
211
|
+
when :running, :not_owned
|
212
|
+
logger.fatal "A server is already running. Check #{pidfile}"
|
213
|
+
exit(1)
|
214
|
+
when :dead
|
215
|
+
File.delete(pidfile)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def pid_status(pidfile)
|
221
|
+
return :exited unless File.exists?(pidfile)
|
222
|
+
pid = ::File.read(pidfile).to_i
|
223
|
+
return :dead if pid == 0
|
224
|
+
Process.kill(0, pid)
|
225
|
+
:running
|
226
|
+
rescue Errno::ESRCH
|
227
|
+
:dead
|
228
|
+
rescue Errno::EPERM
|
229
|
+
:not_owned
|
230
|
+
end
|
231
|
+
|
232
|
+
#==========================================================================
|
233
|
+
# SIGNAL HANDLING
|
234
|
+
#==========================================================================
|
235
|
+
|
236
|
+
def trap_server_signals
|
237
|
+
|
238
|
+
[:HUP, :INT, :QUIT, :TERM, :CHLD, :TTIN, :TTOU].each do |sig|
|
239
|
+
trap(sig) do
|
240
|
+
signals.push(sig)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
end
|
245
|
+
|
246
|
+
#==========================================================================
|
247
|
+
# RACK APP HANDLING
|
248
|
+
#==========================================================================
|
249
|
+
|
250
|
+
def load_app
|
251
|
+
inner_app, options = Rack::Builder.parse_file(config.rack_file)
|
252
|
+
@app = Rack::Builder.new do
|
253
|
+
use RackRabbit::Middleware::ProgramName
|
254
|
+
run inner_app
|
255
|
+
end.to_app
|
256
|
+
logger.info "LOADED #{inner_app.name if inner_app.respond_to?(:name)} FROM #{config.rack_file}"
|
257
|
+
@app
|
258
|
+
end
|
259
|
+
|
260
|
+
#--------------------------------------------------------------------------
|
261
|
+
|
262
|
+
end
|
263
|
+
end
|