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,87 @@
1
+ begin
2
+ require 'bunny'
3
+ rescue LoadError
4
+ abort "missing 'bunny' gem"
5
+ end
6
+
7
+ module RackRabbit
8
+ class Adapter
9
+ class Bunny < RackRabbit::Adapter
10
+
11
+ attr_accessor :connection, :channel
12
+
13
+ def connect
14
+ return if connected?
15
+ @connection = ::Bunny.new(connection_options)
16
+ connection.start
17
+ @channel = connection.create_channel
18
+ channel.prefetch(1)
19
+ end
20
+
21
+ def disconnect
22
+ channel.close unless channel.nil?
23
+ connection.close unless connection.nil?
24
+ end
25
+
26
+ def connected?
27
+ !@connection.nil?
28
+ end
29
+
30
+ def subscribe(options = {}, &block)
31
+ queue = get_queue(options.delete(:queue)) || channel.queue("", :exclusive => true)
32
+ exchange = get_exchange(options.delete(:exchange), options.delete(:exchange_type))
33
+ if exchange
34
+ queue.bind(exchange, :routing_key => options.delete(:routing_key))
35
+ end
36
+ queue.subscribe(options) do |delivery_info, properties, payload|
37
+ yield Message.new(delivery_info.delivery_tag, properties, payload, self)
38
+ end
39
+ end
40
+
41
+ def publish(payload, properties)
42
+ exchange = get_exchange(properties.delete(:exchange), properties.delete(:exchange_type))
43
+ exchange ||= channel.default_exchange
44
+ exchange.publish(payload || "", properties)
45
+ end
46
+
47
+ def with_reply_queue(&block)
48
+ yield channel.queue("", :exclusive => true, :auto_delete => true)
49
+ end
50
+
51
+ def ack(delivery_tag)
52
+ channel.acknowledge(delivery_tag, false)
53
+ end
54
+
55
+ def reject(delivery_tag)
56
+ channel.reject(delivery_tag, false)
57
+ end
58
+
59
+ #========================================================================
60
+ # PRIVATE IMPLEMENTATION
61
+ #========================================================================
62
+
63
+ private
64
+
65
+ def get_exchange(ex = :default, type = :direct)
66
+ case ex
67
+ when ::Bunny::Exchange then ex
68
+ when Symbol, String then channel.send(type || :direct, ex) unless ex.to_s.downcase.to_sym == :default
69
+ else
70
+ nil
71
+ end
72
+ end
73
+
74
+ def get_queue(q)
75
+ case q
76
+ when ::Bunny::Queue then q
77
+ when Symbol, String then channel.queue(q)
78
+ else
79
+ nil
80
+ end
81
+ end
82
+
83
+ #------------------------------------------------------------------------
84
+
85
+ end # class Bunny
86
+ end # module Adapter
87
+ end # module RackRabbit
@@ -0,0 +1,92 @@
1
+ module RackRabbit
2
+ class Adapter
3
+ class Mock < RackRabbit::Adapter
4
+
5
+ attr_accessor :connection
6
+
7
+ def startup
8
+ @started = true
9
+ end
10
+
11
+ def shutdown
12
+ @started = false
13
+ end
14
+
15
+ def started?
16
+ !!@started
17
+ end
18
+
19
+ def connect
20
+ @connected = true
21
+ end
22
+
23
+ def disconnect
24
+ @connected = false
25
+ end
26
+
27
+ def connected?
28
+ !!@connected
29
+ end
30
+
31
+ def subscribe(options = {}, &block)
32
+ @subscribe_options = options
33
+ while !queue.empty?
34
+ message = queue.shift
35
+ yield message
36
+ subscribed_messages << message
37
+ end
38
+ end
39
+
40
+ def publish(body, properties)
41
+ published_messages << properties.merge(:body => body)
42
+ end
43
+
44
+ def with_reply_queue(&block)
45
+ yield OpenStruct.new :name => "reply.queue"
46
+ end
47
+
48
+ def ack(delivery_tag)
49
+ acked_messages << delivery_tag
50
+ end
51
+
52
+ def reject(delivery_tag)
53
+ rejected_messages << delivery_tag
54
+ end
55
+
56
+ #========================================================================
57
+ # TEST HELPER METHODS
58
+ #========================================================================
59
+
60
+ def acked_messages
61
+ @acked_messages ||= []
62
+ end
63
+
64
+ def rejected_messages
65
+ @rejected_messages ||= []
66
+ end
67
+
68
+ def published_messages
69
+ @published_messages ||= []
70
+ end
71
+
72
+ def subscribed_messages
73
+ @subscribed_messages ||= []
74
+ end
75
+
76
+ def queue
77
+ @queue ||= []
78
+ end
79
+
80
+ def prime(message)
81
+ queue << message
82
+ end
83
+
84
+ def subscribe_options
85
+ @subscribe_options
86
+ end
87
+
88
+ end
89
+
90
+ end
91
+ end
92
+
@@ -0,0 +1,181 @@
1
+ require 'securerandom'
2
+
3
+ require 'rack-rabbit'
4
+ require 'rack-rabbit/adapter'
5
+ require 'rack-rabbit/message'
6
+ require 'rack-rabbit/response'
7
+
8
+ module RackRabbit
9
+ class Client
10
+
11
+ #--------------------------------------------------------------------------
12
+
13
+ attr_reader :rabbit
14
+
15
+ def initialize(options = nil)
16
+ @rabbit = Adapter.load(DEFAULT_RABBIT.merge(options || {}))
17
+ connect
18
+ end
19
+
20
+ #--------------------------------------------------------------------------
21
+
22
+ def connect
23
+ rabbit.connect
24
+ end
25
+
26
+ def disconnect
27
+ rabbit.disconnect
28
+ end
29
+
30
+ #--------------------------------------------------------------------------
31
+
32
+ def get(queue, path, options = {})
33
+ request(queue, path, "", options.merge(:method => :GET))
34
+ end
35
+
36
+ def post(queue, path, body, options = {})
37
+ request(queue, path, body, options.merge(:method => :POST))
38
+ end
39
+
40
+ def put(queue, path, body, options = {})
41
+ request(queue, path, body, options.merge(:method => :PUT))
42
+ end
43
+
44
+ def delete(queue, path, options = {})
45
+ request(queue, path, "", options.merge(:method => :DELETE))
46
+ end
47
+
48
+ #--------------------------------------------------------------------------
49
+
50
+ def request(queue, path, body, options = {})
51
+
52
+ id = options[:id] || SecureRandom.uuid # allow dependency injection for test purposes
53
+ lock = Mutex.new
54
+ condition = ConditionVariable.new
55
+ method = options[:method] || :GET
56
+ headers = options[:headers] || {}
57
+ response = nil
58
+
59
+ rabbit.with_reply_queue do |reply_queue|
60
+
61
+ rabbit.subscribe(:queue => reply_queue) do |message|
62
+ if message.correlation_id == id
63
+ lock.synchronize do
64
+ response = Response.new(message.status, message.headers, message.body)
65
+ condition.signal
66
+ end
67
+ end
68
+ end
69
+
70
+ rabbit.publish(body,
71
+ :correlation_id => id,
72
+ :routing_key => queue,
73
+ :reply_to => reply_queue.name,
74
+ :priority => options[:priority],
75
+ :content_type => options[:content_type] || default_content_type,
76
+ :content_encoding => options[:content_encoding] || default_content_encoding,
77
+ :timestamp => options[:timestamp] || default_timestamp,
78
+ :headers => headers.merge({
79
+ RackRabbit::HEADER::METHOD => method.to_s.upcase,
80
+ RackRabbit::HEADER::PATH => path
81
+ })
82
+ )
83
+
84
+ end
85
+
86
+ lock.synchronize do
87
+ condition.wait(lock) unless response
88
+ end
89
+
90
+ response
91
+
92
+ end
93
+
94
+ #--------------------------------------------------------------------------
95
+
96
+ def enqueue(queue, path, body, options = {})
97
+
98
+ method = options[:method] || :POST
99
+ headers = options[:headers] || {}
100
+
101
+ rabbit.publish(body,
102
+ :routing_key => queue,
103
+ :priority => options[:priority],
104
+ :content_type => options[:content_type] || default_content_type,
105
+ :content_encoding => options[:content_encoding] || default_content_encoding,
106
+ :timestamp => options[:timestamp] || default_timestamp,
107
+ :headers => headers.merge({
108
+ RackRabbit::HEADER::METHOD => method.to_s.upcase,
109
+ RackRabbit::HEADER::PATH => path
110
+ })
111
+ )
112
+
113
+ true
114
+
115
+ end
116
+
117
+ #--------------------------------------------------------------------------
118
+
119
+ def publish(exchange, path, body, options = {})
120
+
121
+ method = options[:method] || :POST
122
+ headers = options[:headers] || {}
123
+
124
+ rabbit.publish(body,
125
+ :exchange => exchange,
126
+ :exchange_type => options[:exchange_type] || options[:type] || :fanout,
127
+ :routing_key => options[:routing_key] || options[:route],
128
+ :priority => options[:priority],
129
+ :content_type => options[:content_type] || default_content_type,
130
+ :content_encoding => options[:content_encoding] || default_content_encoding,
131
+ :timestamp => options[:timestamp] || default_timestamp,
132
+ :headers => headers.merge({
133
+ RackRabbit::HEADER::METHOD => method.to_s.upcase,
134
+ RackRabbit::HEADER::PATH => path
135
+ })
136
+ )
137
+
138
+ true
139
+
140
+ end
141
+ #--------------------------------------------------------------------------
142
+
143
+ def default_content_type
144
+ 'text/plain'
145
+ end
146
+
147
+ def default_content_encoding
148
+ 'utf-8'
149
+ end
150
+
151
+ def default_timestamp
152
+ Time.now.to_i
153
+ end
154
+
155
+ #--------------------------------------------------------------------------
156
+
157
+ def self.define_class_method_for(method_name)
158
+ define_singleton_method(method_name) do |*params|
159
+ options = params.last.is_a?(Hash) ? params.pop : {}
160
+ client = Client.new(options.delete(:rabbit))
161
+ response = client.send(method_name, *params, options)
162
+ client.disconnect
163
+ response
164
+ end
165
+ end
166
+
167
+ define_class_method_for :get
168
+ define_class_method_for :post
169
+ define_class_method_for :put
170
+ define_class_method_for :delete
171
+ define_class_method_for :request
172
+ define_class_method_for :enqueue
173
+ define_class_method_for :publish
174
+
175
+ #--------------------------------------------------------------------------
176
+
177
+ end
178
+ end
179
+
180
+ RR = RackRabbit::Client # much less typing for client applications
181
+
@@ -0,0 +1,260 @@
1
+ require 'logger'
2
+ require 'rack'
3
+
4
+ require 'rack-rabbit'
5
+
6
+ module RackRabbit
7
+ class Config
8
+
9
+ #--------------------------------------------------------------------------
10
+
11
+ def initialize(options = {})
12
+ @values = {}
13
+ options.each{|key, value| send(key, value) if respond_to?(key)}
14
+ reload(options)
15
+ end
16
+
17
+ def reload(options = {})
18
+ @rack_env = nil
19
+ instance_eval(File.read(config_file), config_file) if config_file && File.exists?(config_file)
20
+ validate(options) unless options[:validate] == false
21
+ end
22
+
23
+ #--------------------------------------------------------------------------
24
+
25
+ def rabbit(value = :missing)
26
+ if value == :missing
27
+ values[:rabbit] ||= {}.merge(DEFAULT_RABBIT)
28
+ elsif value.is_a?(Hash)
29
+ rabbit.merge!(value)
30
+ end
31
+ end
32
+
33
+ def config_file(value = :missing)
34
+ if value == :missing
35
+ values[:config_file]
36
+ else
37
+ values[:config_file] = filename(value)
38
+ end
39
+ end
40
+
41
+ def rack_file(value = :missing)
42
+ if value == :missing
43
+ values[:rack_file] ||= filename("config.ru", File.dirname(config_file || ""))
44
+ else
45
+ values[:rack_file] = filename(value, File.dirname(config_file || ""))
46
+ end
47
+ end
48
+
49
+ def queue(value = :missing)
50
+ if value == :missing
51
+ values[:queue]
52
+ else
53
+ values[:queue] = value
54
+ end
55
+ end
56
+
57
+ def exchange(value = :missing)
58
+ if value == :missing
59
+ values[:exchange]
60
+ else
61
+ values[:exchange] = value
62
+ end
63
+ end
64
+
65
+ def exchange_type(value = :missing)
66
+ if value == :missing
67
+ values[:exchange_type] ||= :direct
68
+ else
69
+ values[:exchange_type] = value.to_s.downcase.to_sym
70
+ end
71
+ end
72
+
73
+ def routing_key(value = :missing)
74
+ if value == :missing
75
+ values[:routing_key]
76
+ else
77
+ values[:routing_key] = value
78
+ end
79
+ end
80
+
81
+ def app_id(value = :missing)
82
+ if value == :missing
83
+ values[:app_id] ||= "rr-#{exchange || 'default'}-#{queue || routing_key || 'null'}"
84
+ else
85
+ values[:app_id] = value
86
+ end
87
+ end
88
+
89
+ def workers(value = :missing)
90
+ if value == :missing
91
+ values[:workers] ||= 1
92
+ else
93
+ values[:workers] = value.to_i
94
+ end
95
+ end
96
+
97
+ def min_workers(value = :missing)
98
+ if value == :missing
99
+ values[:min_workers] ||= 1
100
+ else
101
+ values[:min_workers] = value.to_i
102
+ end
103
+ end
104
+
105
+ def max_workers(value = :missing)
106
+ if value == :missing
107
+ values[:max_workers] ||= 32
108
+ else
109
+ values[:max_workers] = value.to_i
110
+ end
111
+ end
112
+
113
+ def ack(value = :missing)
114
+ if value == :missing
115
+ values[:ack]
116
+ else
117
+ values[:ack] = !!value
118
+ end
119
+ end
120
+
121
+ def preload_app(value = :missing)
122
+ if value == :missing
123
+ values[:preload_app]
124
+ else
125
+ values[:preload_app] = !!value
126
+ end
127
+ end
128
+
129
+ def daemonize(value = :missing)
130
+ if value == :missing
131
+ values[:daemonize]
132
+ else
133
+ values[:daemonize] = !!value
134
+ end
135
+ end
136
+
137
+ def log_level(value = :missing)
138
+ if value == :missing
139
+ values[:log_level] ||= :info
140
+ else
141
+ values[:log_level] = symbolize(value)
142
+ end
143
+ end
144
+
145
+ def logger(value = :missing)
146
+ if value == :missing
147
+ values[:logger] ||= build_default_logger
148
+ else
149
+ values[:logger] = value
150
+ end
151
+ end
152
+
153
+ def logfile(value = :missing)
154
+ if value == :missing
155
+ values[:logfile] ||= daemonize ? "/var/log/#{app_id}.log" : nil
156
+ else
157
+ values[:logfile] = filename(value)
158
+ end
159
+ end
160
+
161
+ def pidfile(value = :missing)
162
+ if value == :missing
163
+ values[:pidfile] ||= daemonize ? "/var/run/#{app_id}.pid" : nil
164
+ else
165
+ values[:pidfile] = filename(value)
166
+ end
167
+ end
168
+
169
+ def before_fork(server=nil, &block)
170
+ if block
171
+ values[:before_fork] = block
172
+ elsif values[:before_fork].respond_to?(:call)
173
+ values[:before_fork].call(server)
174
+ end
175
+ end
176
+
177
+ def after_fork(server=nil, worker=nil, &block)
178
+ if block
179
+ values[:after_fork] = block
180
+ elsif values[:after_fork].respond_to?(:call)
181
+ values[:after_fork].call(server, worker)
182
+ end
183
+ end
184
+
185
+ #--------------------------------------------------------------------------
186
+
187
+ def rack_env
188
+ @rack_env ||= {
189
+ 'rack.version' => Rack::VERSION,
190
+ 'rack.logger' => logger,
191
+ 'rack.errors' => $stderr,
192
+ 'rack.multithread' => false,
193
+ 'rack.multiprocess' => true,
194
+ 'rack.run_once' => false,
195
+ 'rack.url_scheme' => 'http',
196
+ 'SERVER_NAME' => app_id
197
+ }
198
+ end
199
+
200
+ #--------------------------------------------------------------------------
201
+
202
+ private
203
+
204
+ attr_reader :values
205
+
206
+ def filename(path, relative_to = nil)
207
+ File.expand_path(path, relative_to)
208
+ end
209
+
210
+ def symbolize(s)
211
+ s.to_s.downcase.to_sym
212
+ end
213
+
214
+ def validate(options = {})
215
+
216
+ raise ArgumentError, "must provide EITHER a :queue OR an :exchange to subscribe to" if queue.nil? && exchange.nil?
217
+ raise ArgumentError, "missing app_id" if app_id.to_s.empty?
218
+ raise ArgumentError, "invalid workers" unless workers.is_a?(Fixnum)
219
+ raise ArgumentError, "invalid min_workers" unless min_workers.is_a?(Fixnum)
220
+ raise ArgumentError, "invalid max_workers" unless max_workers.is_a?(Fixnum)
221
+ raise ArgumentError, "invalid workers < min_workers" if workers < min_workers
222
+ raise ArgumentError, "invalid workers > max_workers" if workers > max_workers
223
+ raise ArgumentError, "invalid min_workers > max_workers" if min_workers > max_workers
224
+ raise ArgumentError, "invalid logger" unless [:fatal, :error, :warn, :info, :debug].all?{|method| logger.respond_to?(method)}
225
+ raise ArgumentError, "missing pidfile - required for daemon" if daemonize && pidfile.to_s.empty?
226
+ raise ArgumentError, "missing logfile - required for daemon" if daemonize && logfile.to_s.empty?
227
+
228
+ unless options[:skip_filesystem_checks]
229
+ raise ArgumentError, "missing rack config file #{rack_file}" unless File.readable?(rack_file)
230
+ raise ArgumentError, "pidfile not writable" if pidfile && !File.writable?(File.dirname(pidfile))
231
+ raise ArgumentError, "logfile not writable" if logfile && !File.writable?(File.dirname(logfile))
232
+ end
233
+
234
+ end
235
+
236
+ def build_default_logger
237
+ logger = Logger.new($stderr)
238
+ class << logger
239
+ attr_accessor :master_pid # track the master_pid (might change if we daemonize) in order to differentiate between "SERVER" vs "worker" in log entry preamble
240
+ end
241
+ logger.master_pid = $$
242
+ logger.formatter = proc do |severity, datetime, progname, msg|
243
+ "[#{Process.pid}:#{$$ == logger.master_pid ? "SERVER" : "worker"}] #{datetime} #{msg}\n"
244
+ end
245
+ logger.level = case log_level.to_s.downcase.to_sym
246
+ when :fatal then Logger::FATAL
247
+ when :error then Logger::ERROR
248
+ when :warn then Logger::WARN
249
+ when :info then Logger::INFO
250
+ when :debug then Logger::DEBUG
251
+ else
252
+ Logger::INFO
253
+ end
254
+ logger
255
+ end
256
+
257
+ #--------------------------------------------------------------------------
258
+
259
+ end # class Config
260
+ end # module RackRabbit