rack-rabbit 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/EXAMPLES.md +212 -0
  4. data/Gemfile +14 -0
  5. data/Gemfile.lock +42 -0
  6. data/LICENSE +21 -0
  7. data/README.md +412 -0
  8. data/Rakefile +5 -0
  9. data/bin/rack-rabbit +96 -0
  10. data/bin/rr +99 -0
  11. data/lib/rack-rabbit.rb +63 -0
  12. data/lib/rack-rabbit/adapter.rb +85 -0
  13. data/lib/rack-rabbit/adapter/amqp.rb +114 -0
  14. data/lib/rack-rabbit/adapter/bunny.rb +87 -0
  15. data/lib/rack-rabbit/adapter/mock.rb +92 -0
  16. data/lib/rack-rabbit/client.rb +181 -0
  17. data/lib/rack-rabbit/config.rb +260 -0
  18. data/lib/rack-rabbit/handler.rb +44 -0
  19. data/lib/rack-rabbit/message.rb +95 -0
  20. data/lib/rack-rabbit/middleware/program_name.rb +34 -0
  21. data/lib/rack-rabbit/response.rb +43 -0
  22. data/lib/rack-rabbit/server.rb +263 -0
  23. data/lib/rack-rabbit/signals.rb +62 -0
  24. data/lib/rack-rabbit/subscriber.rb +77 -0
  25. data/lib/rack-rabbit/worker.rb +84 -0
  26. data/rack-rabbit.gemspec +26 -0
  27. data/test/apps/config.ru +7 -0
  28. data/test/apps/custom.conf +27 -0
  29. data/test/apps/custom.ru +7 -0
  30. data/test/apps/empty.conf +1 -0
  31. data/test/apps/error.ru +7 -0
  32. data/test/apps/mirror.ru +19 -0
  33. data/test/apps/sinatra.ru +37 -0
  34. data/test/apps/sleep.ru +21 -0
  35. data/test/test_case.rb +154 -0
  36. data/test/unit/middleware/test_program_name.rb +32 -0
  37. data/test/unit/test_client.rb +275 -0
  38. data/test/unit/test_config.rb +403 -0
  39. data/test/unit/test_handler.rb +92 -0
  40. data/test/unit/test_message.rb +213 -0
  41. data/test/unit/test_response.rb +59 -0
  42. data/test/unit/test_signals.rb +45 -0
  43. data/test/unit/test_subscriber.rb +140 -0
  44. 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