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.
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