beetle 0.2.11 → 0.3.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1,8 +1,9 @@
1
1
  require 'rake'
2
2
  require 'rake/testtask'
3
3
  require 'rcov/rcovtask'
4
+ # rake 0.9.2 hack to supress deprecation warnings caused by cucumber
5
+ include Rake::DSL if RAKEVERSION >= "0.9"
4
6
  require 'cucumber/rake/task'
5
- require 'active_support'
6
7
 
7
8
  # 1.8/1.9 compatible way of loading lib/beetle.rb
8
9
  $:.unshift 'lib'
data/beetle.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "beetle"
3
- s.version = "0.2.11"
4
- s.required_rubygems_version = ">= 1.3.1"
3
+ s.version = "0.3.0.rc.1"
4
+ s.required_rubygems_version = ">= 1.3.7"
5
5
  s.authors = ["Stefan Kaes", "Pascal Friederich", "Ali Jelveh", "Sebastian Roebke"]
6
6
  s.date = Time.now.strftime('%Y-%m-%d')
7
7
  s.default_executable = "beetle"
@@ -28,15 +28,17 @@ Gem::Specification.new do |s|
28
28
  INFO
29
29
 
30
30
  s.specification_version = 3
31
- s.add_runtime_dependency("uuid4r", [">= 0.1.1"])
32
- s.add_runtime_dependency("bunny", ["= 0.6.0"])
33
- s.add_runtime_dependency("bunny-ext", [">= 0.6.5"])
31
+ s.add_runtime_dependency("uuid4r", [">= 0.1.2"])
32
+ s.add_runtime_dependency("bunny", ["~> 0.7.1"])
34
33
  s.add_runtime_dependency("redis", ["= 2.0.4"])
35
- s.add_runtime_dependency("amqp", [">= 0.6.7"])
36
- s.add_runtime_dependency("activesupport", ["~> 2.3.4"])
34
+ s.add_runtime_dependency("amqp", ["~> 0.6.7"])
35
+ s.add_runtime_dependency("activesupport", [">= 2.3.4"])
37
36
  s.add_runtime_dependency("daemons", [">= 1.0.10"])
37
+ s.add_development_dependency("rake", [">= 0.8.7"])
38
38
  s.add_development_dependency("mocha", [">= 0"])
39
39
  s.add_development_dependency("rcov", [">= 0"])
40
+ s.add_development_dependency("redgreen", [">= 0"])
41
+ s.add_development_dependency("wirble", [">= 0"])
40
42
  s.add_development_dependency("cucumber", [">= 0.7.2"])
41
43
  s.add_development_dependency("daemon_controller", [">= 0"])
42
44
  end
data/examples/attempts.rb CHANGED
@@ -47,7 +47,7 @@ class Handler < Beetle::Handler
47
47
  # we're stopping the event loop so this script stops after that
48
48
  def failure(result)
49
49
  super
50
- $client.stop_listening
50
+ EM.add_timer(1){$client.stop_listening}
51
51
  end
52
52
  end
53
53
 
@@ -62,5 +62,5 @@ $client.listen
62
62
 
63
63
  # error handling, if everything went right this shouldn't happen.
64
64
  if $exceptions != $max_exceptions + 1
65
- raise "something is fishy. Failed #{$exceptions} times"
65
+ raise "Something is fishy. Failed #{$exceptions} times"
66
66
  end
@@ -0,0 +1,75 @@
1
+ # attempts.rb
2
+ # this example shows you how to use the exception limiting feature of beetle
3
+ # it allows you to control the number of retries your handler will go through
4
+ # with one message before giving up on it
5
+ #
6
+ # ! check the examples/README.rdoc for information on starting your redis/rabbit !
7
+ #
8
+ # start it with ruby attempts.rb
9
+
10
+ require "rubygems"
11
+ require File.expand_path("../lib/beetle", File.dirname(__FILE__))
12
+
13
+ # set Beetle log level to info, less noisy than debug
14
+ Beetle.config.logger.level = Logger::INFO
15
+
16
+ # setup client
17
+ $client = Beetle::Client.new
18
+ $client.register_queue(:test)
19
+ $client.register_message(:test)
20
+
21
+ # purge the test queue
22
+ $client.purge(:test)
23
+
24
+ # initially, our service is online
25
+ $online = true
26
+
27
+ # declare a handler class for message processing
28
+ # in this example we've not only overwritten the process method but also the
29
+ # error and failure methods of the handler baseclass
30
+ class Handler < Beetle::Handler
31
+
32
+ # called when the handler receives the message - fail everytime
33
+ def process
34
+ puts "received message #{message.data} online=#{$online}"
35
+ unless $online
36
+ $client.pause_listening(:test)
37
+ raise "offline"
38
+ end
39
+ end
40
+
41
+ # called when handler process raised an exception
42
+ def error(exception)
43
+ puts "execution failed: #{exception}"
44
+ end
45
+
46
+ end
47
+
48
+ # publish a decent amount of messages
49
+ # 1000.times do |i|
50
+ # $client.publish(:test, i)
51
+ # end
52
+ # $client.stop_publishing
53
+
54
+ # register our handler to the message, configure it to our max_exceptions limit, we configure a delay of 0 to have it not wait before retrying
55
+ $client.register_handler(:test, Handler, :exceptions => 1, :delay => 0)
56
+
57
+ # and start our listening loop...
58
+ $client.listen do
59
+ n = 0
60
+ ptimer = EM.add_periodic_timer(0.1) do
61
+ data = (n+=1)
62
+ puts "publishing message #{data}"
63
+ $client.publish(:test, data)
64
+ end
65
+
66
+ EM.add_periodic_timer(2) do
67
+ $online = !$online
68
+ $client.resume_listening(:test) if $online
69
+ end
70
+
71
+ EM.add_timer(10) do
72
+ $client.pause_listening
73
+ EM.add_timer(1) { $client.stop_listening }
74
+ end
75
+ end
data/examples/simple.rb CHANGED
@@ -5,7 +5,7 @@
5
5
  #
6
6
  # ! check the examples/README.rdoc for information on starting your redis/rabbit !
7
7
  #
8
- # start it with ruby multiple_exchanges.rb
8
+ # start it with ruby simple.rb
9
9
 
10
10
  require "rubygems"
11
11
  require File.expand_path("../lib/beetle", File.dirname(__FILE__))
@@ -27,8 +27,8 @@ client.deduplication_store.flushdb
27
27
  # register our handler to the message, check out the message.rb for more stuff you can get from the message object
28
28
  client.register_handler(:test) {|message| puts "got message: #{message.data}"}
29
29
 
30
- # publish our message
31
- client.publish(:test, 'bam')
30
+ # publish our message (NOTE: empty message bodies don't work, most likely due to bugs in bunny/amqp)
31
+ puts client.publish(:test, 'bam')
32
32
 
33
33
  # start listening
34
34
  # this starts the event machine event loop using EM.run
@@ -0,0 +1,32 @@
1
+ # attempts.rb
2
+ # this example shows you how to use the exception limiting feature of beetle
3
+ # it allows you to control the number of retries your handler will go through
4
+ # with one message before giving up on it
5
+ #
6
+ # ! check the examples/README.rdoc for information on starting your redis/rabbit !
7
+ #
8
+ # start it with ruby attempts.rb
9
+
10
+ require "rubygems"
11
+ require File.expand_path("../lib/beetle", File.dirname(__FILE__))
12
+ require "eventmachine"
13
+
14
+ # set Beetle log level to info, less noisy than debug
15
+ Beetle.config.logger.level = Logger::INFO
16
+
17
+ # setup client
18
+ client = Beetle::Client.new
19
+ client.register_message(:test)
20
+
21
+ n = 0
22
+ EM.run do
23
+ EM.add_periodic_timer(0.1) do
24
+ data = (n+=1)
25
+ client.logger.info "publishing #{data}"
26
+ client.publish(:test, data)
27
+ end
28
+ trap("INT") do
29
+ client.stop_publishing
30
+ EM.stop_event_loop
31
+ end
32
+ end
@@ -40,7 +40,9 @@ def first_redis_configuration_client_pid
40
40
  end
41
41
 
42
42
  def system_notification_log_path
43
- tmp_path + "/system_notifications.log"
43
+ log_path = tmp_path + "/system_notifications.log"
44
+ `touch #{log_path}` unless File.exists?(log_path)
45
+ log_path
44
46
  end
45
47
 
46
48
  def tmp_path
@@ -185,5 +185,5 @@ glueoutputbuf yes
185
185
  # WARNING: object sharing is experimental, don't enable this feature
186
186
  # in production before of Redis 1.0-stable. Still please try this feature in
187
187
  # your development environment so that we can test it better.
188
- shareobjects no
189
- shareobjectspoolsize 1024
188
+ # shareobjects no
189
+ # shareobjectspoolsize 1024
data/lib/beetle/base.rb CHANGED
@@ -53,7 +53,7 @@ module Beetle
53
53
  queues[name] ||=
54
54
  begin
55
55
  opts = @client.queues[name]
56
- error("You are trying to bind a queue #{name} which is not configured!") unless opts
56
+ raise UnknownQueue.new("You are trying to bind a queue #{name} which is not configured!") unless opts
57
57
  logger.debug("Beetle: binding queue #{name} with internal name #{opts[:amqp_name]} on server #{@server}")
58
58
  queue_name = opts[:amqp_name]
59
59
  creation_options = opts.slice(*QUEUE_CREATION_KEYS)
data/lib/beetle/client.rb CHANGED
@@ -140,8 +140,7 @@ module Beetle
140
140
  # For details on handler classes see class Beetle::Handler
141
141
 
142
142
  def register_handler(queues, *args, &block)
143
- queues = Array(queues).map(&:to_s)
144
- queues.each {|q| raise UnknownQueue.new(q) unless self.queues.include?(q)}
143
+ queues = determine_queue_names(Array(queues))
145
144
  opts = args.last.is_a?(Hash) ? args.pop : {}
146
145
  handler = args.shift
147
146
  raise ArgumentError.new("too many arguments for handler registration") unless args.empty?
@@ -167,9 +166,9 @@ module Beetle
167
166
  end
168
167
 
169
168
  # publishes a message. the given options hash is merged with options given on message registration.
169
+ # WARNING: empty message bodies can lead to problems.
170
170
  def publish(message_name, data=nil, opts={})
171
- message_name = message_name.to_s
172
- raise UnknownMessage.new("unknown message #{message_name}") unless messages.include?(message_name)
171
+ message_name = validated_message_name(message_name)
173
172
  publisher.publish(message_name, data, opts)
174
173
  end
175
174
 
@@ -177,24 +176,28 @@ module Beetle
177
176
  #
178
177
  # unexpected behavior can ensue if the message gets routed to more than one recipient, so be careful.
179
178
  def rpc(message_name, data=nil, opts={})
180
- message_name = message_name.to_s
181
- raise UnknownMessage.new("unknown message #{message_name}") unless messages.include?(message_name)
179
+ message_name = validated_message_name(message_name)
182
180
  publisher.rpc(message_name, data, opts)
183
181
  end
184
182
 
185
- # purges the given queue on all configured servers
186
- def purge(queue_name)
187
- queue_name = queue_name.to_s
188
- raise UnknownQueue.new("unknown queue #{queue_name}") unless queues.include?(queue_name)
189
- publisher.purge(queue_name)
183
+ # purges the given queues on all configured servers
184
+ def purge(*queues)
185
+ queues = determine_queue_names(queues)
186
+ publisher.purge(queues)
190
187
  end
191
188
 
192
- # start listening to a list of messages (default to all registered messages).
189
+ # start listening to all registered queues. Calls #listen_queues internally
193
190
  # runs the given block before entering the eventmachine loop.
194
- def listen(messages=self.messages.keys, &block)
195
- messages = messages.map(&:to_s)
196
- messages.each{|m| raise UnknownMessage.new("unknown message #{m}") unless self.messages.include?(m)}
197
- subscriber.listen(messages, &block)
191
+ def listen(_deprecated_messages=nil, &block)
192
+ raise Error.new("Beetle::Client#listen no longer works with arguments. Please use #listen_queues(['queue1', 'queue2']) instead") if _deprecated_messages
193
+ listen_queues(&block)
194
+ end
195
+
196
+ # start listening to a list of queues (default to all registered queues).
197
+ # runs the given block before entering the eventmachine loop.
198
+ def listen_queues(*queues, &block)
199
+ queues = determine_queue_names(queues)
200
+ subscriber.listen_queues(queues, &block)
198
201
  end
199
202
 
200
203
  # stops the eventmachine loop
@@ -207,19 +210,32 @@ module Beetle
207
210
  publisher.stop
208
211
  end
209
212
 
210
- # traces messages without consuming them. useful for debugging message flow.
211
- def trace(messages=self.messages.keys, &block)
212
- queues.each do |name, opts|
213
+ # pause listening on a list of queues
214
+ def pause_listening(*queues)
215
+ queues = determine_queue_names(queues)
216
+ subscriber.pause_listening(queues)
217
+ end
218
+
219
+ # resume listening on a list of queues
220
+ def resume_listening(*queues)
221
+ queues = determine_queue_names(queues)
222
+ subscriber.resume_listening(queues)
223
+ end
224
+
225
+ # traces queues without consuming them. useful for debugging message flow.
226
+ def trace(queue_names=self.queues.keys, &block)
227
+ queues_to_trace = self.queues.slice(*queue_names)
228
+ queues_to_trace.each do |name, opts|
213
229
  opts.merge! :durable => false, :auto_delete => true, :amqp_name => queue_name_for_tracing(opts[:amqp_name])
214
230
  end
215
- register_handler(queues.keys) do |msg|
231
+ register_handler(queue_names) do |msg|
216
232
  puts "-----===== new message =====-----"
217
233
  puts "SERVER: #{msg.server}"
218
234
  puts "HEADER: #{msg.header.inspect}"
219
235
  puts "MSGID: #{msg.msg_id}"
220
236
  puts "DATA: #{msg.data}"
221
237
  end
222
- listen(messages, &block)
238
+ listen_queues(queue_names, &block)
223
239
  end
224
240
 
225
241
  # evaluate the ruby files matching the given +glob+ pattern in the context of the client instance.
@@ -232,6 +248,26 @@ module Beetle
232
248
 
233
249
  private
234
250
 
251
+ def determine_queue_names(queues)
252
+ if queues.empty?
253
+ self.queues.keys
254
+ else
255
+ queues.flatten.map{|q| validated_queue_name(q)}
256
+ end
257
+ end
258
+
259
+ def validated_queue_name(queue_name)
260
+ queue_name = queue_name.to_s
261
+ raise UnknownQueue.new("unknown queue #{queue_name}") unless queues.include?(queue_name)
262
+ queue_name
263
+ end
264
+
265
+ def validated_message_name(message_name)
266
+ message_name = message_name.to_s
267
+ raise UnknownMessage.new("unknown message #{message_name}") unless messages.include?(message_name)
268
+ message_name
269
+ end
270
+
235
271
  class Configurator #:nodoc:all
236
272
  def initialize(client, options={})
237
273
  @client = client
@@ -11,13 +11,16 @@ module Beetle
11
11
  # the Message instance which caused the handler to be created
12
12
  attr_reader :message
13
13
 
14
- def self.create(block_or_handler, opts={}) #:nodoc:
15
- if block_or_handler.is_a? Handler
16
- block_or_handler
17
- elsif block_or_handler.is_a?(Class) && block_or_handler.ancestors.include?(Handler)
18
- block_or_handler.new
14
+ def self.create(handler, opts={}) #:nodoc:
15
+ if handler.is_a? Handler
16
+ # a handler instance
17
+ handler
18
+ elsif handler.is_a?(Class) && handler.ancestors.include?(Handler)
19
+ # handler class
20
+ handler.new
19
21
  else
20
- new(block_or_handler, opts)
22
+ # presumably something which responds to call
23
+ new(handler, opts)
21
24
  end
22
25
  end
23
26
 
@@ -70,6 +73,13 @@ module Beetle
70
73
  Beetle::reraise_expectation_errors!
71
74
  end
72
75
 
76
+ # should not be overriden in subclasses
77
+ def processing_completed
78
+ completed
79
+ rescue Exception
80
+ Beetle::reraise_expectation_errors!
81
+ end
82
+
73
83
  # called when handler execution raised an exception and no error callback was
74
84
  # specified when the handler instance was created
75
85
  def error(exception)
@@ -83,6 +93,12 @@ module Beetle
83
93
  logger.error "Beetle: handler has finally failed"
84
94
  end
85
95
 
96
+ # called after all normal processing has been completed. flushes the loggger, if it responds to flush.
97
+ def completed
98
+ logger.debug "Beetle: message processing completed"
99
+ logger.flush if logger.respond_to?(:flush)
100
+ end
101
+
86
102
  # returns the configured Beetle logger
87
103
  def self.logger
88
104
  Beetle.config.logger
@@ -16,7 +16,7 @@ module Beetle
16
16
  Bunny::ConnectionError, Bunny::ForcedChannelCloseError, Bunny::ForcedConnectionCloseError,
17
17
  Bunny::MessageError, Bunny::ProtocolError, Bunny::ServerDownError, Bunny::UnsubscribeError,
18
18
  Bunny::AcknowledgementError, Qrack::BufferOverflowError, Qrack::InvalidTypeError,
19
- Errno::EHOSTUNREACH, Errno::ECONNRESET
19
+ Errno::EHOSTUNREACH, Errno::ECONNRESET, Timeout::Error
20
20
  ]
21
21
  end
22
22
 
@@ -130,8 +130,12 @@ module Beetle
130
130
  [status, result]
131
131
  end
132
132
 
133
- def purge(queue_name) #:nodoc:
134
- each_server { queue(queue_name).purge rescue nil }
133
+ def purge(queue_names) #:nodoc:
134
+ each_server do
135
+ queue_names.each do |name|
136
+ queue(name).purge rescue nil
137
+ end
138
+ end
135
139
  end
136
140
 
137
141
  def stop #:nodoc:
@@ -210,7 +214,7 @@ module Beetle
210
214
  end
211
215
  end
212
216
  rescue Exception => e
213
- logger.error "Beetle: error closing down bunny #{e}"
217
+ logger.warn "Beetle: error closing down bunny #{e}"
214
218
  Beetle::reraise_expectation_errors!
215
219
  ensure
216
220
  @bunnies[@server] = nil
@@ -12,34 +12,46 @@ module Beetle
12
12
  @handlers = {}
13
13
  @amqp_connections = {}
14
14
  @mqs = {}
15
+ @subscriptions = {}
15
16
  end
16
17
 
17
- # the client calls this method to subscribe to all queues on all servers which have
18
- # handlers registered for the given list of messages. this method does the following
19
- # things:
18
+ # the client calls this method to subscribe to a list of queues.
19
+ # this method does the following things:
20
20
  #
21
- # * creates all exchanges which have been registered for the given messages
22
- # * creates and binds queues which have been registered for the exchanges
21
+ # * creates all exchanges which have been registered for the given queues
22
+ # * creates and binds each listed queue queues
23
23
  # * subscribes the handlers for all these queues
24
24
  #
25
25
  # yields before entering the eventmachine loop (if a block was given)
26
- def listen(messages) #:nodoc:
26
+ def listen_queues(queues) #:nodoc:
27
27
  EM.run do
28
- exchanges = exchanges_for_messages(messages)
28
+ exchanges = exchanges_for_queues(queues)
29
29
  create_exchanges(exchanges)
30
- queues = queues_for_exchanges(exchanges)
31
30
  bind_queues(queues)
32
31
  subscribe_queues(queues)
33
32
  yield if block_given?
34
33
  end
35
34
  end
36
35
 
36
+ def pause_listening(queues)
37
+ each_server do
38
+ queues.each { |name| pause(name) if has_subscription?(name) }
39
+ end
40
+ end
41
+
42
+ def resume_listening(queues)
43
+ each_server do
44
+ queues.each { |name| resume(name) if has_subscription?(name) }
45
+ end
46
+ end
47
+
37
48
  # closes all AMQP connections and stops the eventmachine loop
38
49
  def stop! #:nodoc:
39
50
  if @amqp_connections.empty?
40
51
  EM.stop_event_loop
41
52
  else
42
53
  server, connection = @amqp_connections.shift
54
+ logger.debug "Beetle: closing connection to #{server}"
43
55
  connection.close { stop! }
44
56
  end
45
57
  end
@@ -53,8 +65,8 @@ module Beetle
53
65
 
54
66
  private
55
67
 
56
- def exchanges_for_messages(messages)
57
- @client.messages.slice(*messages).map{|_, opts| opts[:exchange]}.uniq
68
+ def exchanges_for_queues(queues)
69
+ @client.bindings.slice(*queues).map{|_, opts| opts.map{|opt| opt[:exchange]}}.flatten.uniq
58
70
  end
59
71
 
60
72
  def queues_for_exchanges(exchanges)
@@ -88,24 +100,46 @@ module Beetle
88
100
  @mqs[server] ||= MQ.new(amqp_connection).prefetch(1)
89
101
  end
90
102
 
103
+ def subscriptions(server=@server)
104
+ @subscriptions[server] ||= {}
105
+ end
106
+
107
+ def has_subscription?(name)
108
+ subscriptions.include?(name)
109
+ end
110
+
91
111
  def subscribe(queue_name)
92
112
  error("no handler for queue #{queue_name}") unless @handlers.include?(queue_name)
93
113
  opts, handler = @handlers[queue_name]
94
114
  queue_opts = @client.queues[queue_name][:amqp_name]
95
115
  amqp_queue_name = queue_opts
96
116
  callback = create_subscription_callback(queue_name, amqp_queue_name, handler, opts)
117
+ keys = opts.slice(*SUBSCRIPTION_KEYS).merge(:key => "#", :ack => true)
97
118
  logger.debug "Beetle: subscribing to queue #{amqp_queue_name} with key # on server #{@server}"
98
119
  begin
99
- queues[queue_name].subscribe(opts.slice(*SUBSCRIPTION_KEYS).merge(:key => "#", :ack => true), &callback)
120
+ queues[queue_name].subscribe(keys, &callback)
121
+ subscriptions[queue_name] = [keys, callback]
100
122
  rescue MQ::Error
101
123
  error("Beetle: binding multiple handlers for the same queue isn't possible.")
102
124
  end
103
125
  end
104
126
 
127
+ def pause(queue_name)
128
+ return unless queues[queue_name].subscribed?
129
+ queues[queue_name].unsubscribe
130
+ end
131
+
132
+ def resume(queue_name)
133
+ return if queues[queue_name].subscribed?
134
+ keys, callback = subscriptions[queue_name]
135
+ queues[queue_name].subscribe(keys, &callback)
136
+ end
137
+
105
138
  def create_subscription_callback(queue_name, amqp_queue_name, handler, opts)
106
139
  server = @server
107
140
  lambda do |header, data|
108
141
  begin
142
+ # logger.debug "Beetle: received message"
109
143
  processor = Handler.create(handler, opts)
110
144
  message_options = opts.merge(:server => server, :store => @client.deduplication_store)
111
145
  m = Message.new(amqp_queue_name, header, data, message_options)
@@ -121,10 +155,14 @@ module Beetle
121
155
  exchange = MQ::Exchange.new(mq(server), :direct, "", :key => reply_to)
122
156
  exchange.publish(m.handler_result.to_s, :headers => {:status => status})
123
157
  end
158
+ # logger.debug "Beetle: processed message"
124
159
  rescue Exception
125
160
  Beetle::reraise_expectation_errors!
126
161
  # swallow all exceptions
127
162
  logger.error "Beetle: internal error during message processing: #{$!}: #{$!.backtrace.join("\n")}"
163
+ ensure
164
+ # processing_completed swallows all exceptions, so we don't need to protect this call
165
+ processor.processing_completed
128
166
  end
129
167
  end
130
168
  end
@@ -146,7 +184,7 @@ module Beetle
146
184
 
147
185
  def new_amqp_connection
148
186
  # FIXME: wtf, how to test that reconnection feature....
149
- con = AMQP.connect(:host => current_host, :port => current_port,
187
+ con = AMQP.connect(:host => current_host, :port => current_port, :logging => false,
150
188
  :user => Beetle.config.user, :pass => Beetle.config.password, :vhost => Beetle.config.vhost)
151
189
  con.instance_variable_set("@on_disconnect", proc{ con.__send__(:reconnect) })
152
190
  con
data/lib/beetle.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  $:.unshift(File.expand_path('..', __FILE__))
2
- require 'bunny-ext'
2
+ require 'bunny'
3
3
  require 'uuid4r'
4
4
  require 'active_support'
5
+ require 'active_support/core_ext'
5
6
  require 'redis'
7
+ require 'set'
6
8
 
7
9
  module Beetle
8
10
 
@@ -236,14 +236,14 @@ module Beetle
236
236
  test "should delegate queue purging to the publisher instance" do
237
237
  client = Client.new
238
238
  client.register_queue(:queue)
239
- client.send(:publisher).expects(:purge).with("queue").returns("ha!")
239
+ client.send(:publisher).expects(:purge).with(["queue"]).returns("ha!")
240
240
  assert_equal "ha!", client.purge("queue")
241
241
  end
242
242
 
243
243
  test "purging a queue should convert the queue name to a string" do
244
244
  client = Client.new
245
245
  client.register_queue(:queue)
246
- client.send(:publisher).expects(:purge).with("queue").returns("ha!")
246
+ client.send(:publisher).expects(:purge).with(["queue"]).returns("ha!")
247
247
  assert_equal "ha!", client.purge(:queue)
248
248
  end
249
249
 
@@ -251,6 +251,14 @@ module Beetle
251
251
  assert_raises(UnknownQueue) { Client.new.purge(:mumu) }
252
252
  end
253
253
 
254
+ test "should be possible to purge multiple queues with a single call" do
255
+ client = Client.new
256
+ client.register_queue(:queue1)
257
+ client.register_queue(:queue2)
258
+ client.send(:publisher).expects(:purge).with(["queue1","queue2"]).returns("ha!")
259
+ assert_equal "ha!", client.purge(:queue1, :queue2)
260
+ end
261
+
254
262
  test "should delegate rpc calls to the publisher instance" do
255
263
  client = Client.new
256
264
  client.register_message("deadletter")
@@ -261,18 +269,36 @@ module Beetle
261
269
 
262
270
  test "should delegate listening to the subscriber instance" do
263
271
  client = Client.new
264
- client.register_queue(:a)
265
- client.register_message(:a)
266
- client.register_queue(:b)
267
- client.register_message(:b)
268
- client.send(:subscriber).expects(:listen).with(["a", "b"]).yields
269
- x = 0
270
- client.listen([:a, "b"]) { x = 5 }
271
- assert_equal 5, x
272
+ client.register_queue("b_queue")
273
+ client.register_queue("a_queue")
274
+ client.send(:subscriber).expects(:listen_queues).with {|value| value.include?("a_queue") && value.include?("b_queue")}.yields
275
+ client.listen
276
+ end
277
+
278
+ test "trying to listen to a message is no longer supported and should raise an exception" do
279
+ assert_raises(Error) { Client.new.listen([:a])}
280
+ end
281
+
282
+ test "should delegate listening to queues to the subscriber instance" do
283
+ client = Client.new
284
+ client.register_queue(:test)
285
+ client.send(:subscriber).expects(:listen_queues).with(['test']).yields
286
+ client.listen_queues([:test])
272
287
  end
273
288
 
274
- test "trying to listen to an unknown message should raise an exception" do
275
- assert_raises(UnknownMessage) { Client.new.listen([:a])}
289
+ test "trying to listen to an unknown queue should raise an exception" do
290
+ client = Client.new
291
+ assert_raises(UnknownQueue) { Client.new.listen_queues([:foobar])}
292
+ end
293
+
294
+ test "trying to pause listening on an unknown queue should raise an exception" do
295
+ client = Client.new
296
+ assert_raises(UnknownQueue) { Client.new.pause_listening(:foobar)}
297
+ end
298
+
299
+ test "trying to resume listening on an unknown queue should raise an exception" do
300
+ client = Client.new
301
+ assert_raises(UnknownQueue) { Client.new.pause_listening(:foobar)}
276
302
  end
277
303
 
278
304
  test "should delegate stop_listening to the subscriber instance" do
@@ -281,6 +307,20 @@ module Beetle
281
307
  client.stop_listening
282
308
  end
283
309
 
310
+ test "should delegate pause_listening to the subscriber instance" do
311
+ client = Client.new
312
+ client.register_queue(:test)
313
+ client.send(:subscriber).expects(:pause_listening).with(%w(test))
314
+ client.pause_listening(:test)
315
+ end
316
+
317
+ test "should delegate resume_listening to the subscriber instance" do
318
+ client = Client.new
319
+ client.register_queue(:test)
320
+ client.send(:subscriber).expects(:resume_listening).with(%w(test))
321
+ client.resume_listening(:test)
322
+ end
323
+
284
324
  test "should delegate handler registration to the subscriber instance" do
285
325
  client = Client.new
286
326
  client.register_queue("huhu")
@@ -314,7 +354,7 @@ module Beetle
314
354
  client.register_queue("test")
315
355
  sub = client.send(:subscriber)
316
356
  sub.expects(:register_handler).with(client.queues.keys, {}, nil).yields(stub_everything("message"))
317
- sub.expects(:listen)
357
+ sub.expects(:listen_queues)
318
358
  client.stubs(:puts)
319
359
  client.trace
320
360
  test_queue_opts = client.queues["test"]
@@ -324,5 +364,16 @@ module Beetle
324
364
  assert !test_queue_opts[:durable]
325
365
  end
326
366
 
367
+ test "limiting tracing to some queues" do
368
+ client = Client.new
369
+ client.register_queue("test")
370
+ client.register_queue("irrelevant")
371
+ sub = client.send(:subscriber)
372
+ sub.expects(:register_handler).with(["test"], {}, nil).yields(stub_everything("message"))
373
+ sub.expects(:listen_queues).with(["test"])
374
+ client.stubs(:puts)
375
+ client.trace(["test"])
376
+ end
377
+
327
378
  end
328
379
  end
@@ -77,17 +77,18 @@ module Beetle
77
77
  assert_equal Beetle.config.logger, Handler.logger
78
78
  end
79
79
 
80
- test "default implementation of error and process and failure should not crash" do
80
+ test "default implementation of error and process and failure and completed should not crash" do
81
81
  handler = Handler.create(lambda {})
82
82
  handler.process
83
83
  handler.error('barfoo')
84
84
  handler.failure('razzmatazz')
85
+ handler.completed
85
86
  end
86
87
 
87
88
  test "should silently rescue exceptions in the process_exception call" do
88
89
  mock = mock('error handler')
89
90
  e = Exception.new
90
- mock.expects(:call).with('message', e).raises(RuntimeError)
91
+ mock.expects(:call).with('message', e).raises(Exception)
91
92
  handler = Handler.create(lambda {}, :errback => mock)
92
93
  handler.instance_variable_set(:@message, 'message')
93
94
  assert_nothing_raised {handler.process_exception(e)}
@@ -95,11 +96,17 @@ module Beetle
95
96
 
96
97
  test "should silently rescue exceptions in the process_failure call" do
97
98
  mock = mock('failure handler')
98
- mock.expects(:call).with('message', 1).raises(RuntimeError)
99
+ mock.expects(:call).with('message', 1).raises(Exception)
99
100
  handler = Handler.create(lambda {}, :failback => mock)
100
101
  handler.instance_variable_set(:@message, 'message')
101
102
  assert_nothing_raised {handler.process_failure(1)}
102
103
  end
103
104
 
105
+ test "should silently rescue exceptions in the processing_completed call" do
106
+ handler = Handler.create(lambda {})
107
+ handler.expects(:completed).raises(Exception)
108
+ assert_nothing_raised {handler.processing_completed}
109
+ end
110
+
104
111
  end
105
112
  end
@@ -274,7 +274,7 @@ module Beetle
274
274
  @pub.expects(:set_current_server).with("b").in_sequence(s)
275
275
  @pub.expects(:queue).with("queue").returns(queue).in_sequence(s)
276
276
  queue.expects(:purge).in_sequence(s)
277
- @pub.purge("queue")
277
+ @pub.purge(["queue"])
278
278
  end
279
279
  end
280
280
 
@@ -27,7 +27,7 @@ module Beetle
27
27
  m = mock("dummy")
28
28
  expected_amqp_options = {
29
29
  :host => @sub.send(:current_host), :port => @sub.send(:current_port),
30
- :user => "guest", :pass => "guest", :vhost => "/"
30
+ :user => "guest", :pass => "guest", :vhost => "/", :logging => false
31
31
  }
32
32
  AMQP.expects(:connect).with(expected_amqp_options).returns(m)
33
33
  # TODO: smarter way to test? what triggers the amqp_connection private method call?
@@ -56,6 +56,75 @@ module Beetle
56
56
 
57
57
  end
58
58
 
59
+ class SubscriberPauseAndResumeTest < Test::Unit::TestCase
60
+ def setup
61
+ @client = Client.new
62
+ @sub = @client.send(:subscriber)
63
+ @sub.servers << "localhost:7777"
64
+ @server1, @server2 = @sub.servers
65
+ @client.register_message(:a)
66
+ @client.register_queue(:a)
67
+ @client.register_handler(%W(a)){}
68
+ end
69
+
70
+ test "should pause on all servers when a handler has been registered" do
71
+ @sub.expects(:pause).with("a").times(2)
72
+ @sub.stubs(:has_subscription?).returns(true)
73
+ @sub.pause_listening(%w(a))
74
+ end
75
+
76
+ test "should resume on all servers when a handler has been registered" do
77
+ @sub.expects(:resume).with("a").times(2)
78
+ @sub.stubs(:has_subscription?).returns(true)
79
+ @sub.resume_listening(%w(a))
80
+ end
81
+
82
+ test "should pause on no servers when no handler has been registered" do
83
+ @sub.expects(:pause).never
84
+ @sub.stubs(:has_subscription?).returns(false)
85
+ @sub.pause_listening(%w(a))
86
+ end
87
+
88
+ test "should resume on no servers when no handler has been registered" do
89
+ @sub.expects(:resume).never
90
+ @sub.stubs(:has_subscription?).returns(false)
91
+ @sub.resume_listening(%w(a))
92
+ end
93
+
94
+ test "pausing a single queue should call amqp unsubscribe" do
95
+ q = mock("queue a")
96
+ q.expects(:subscribed?).returns(true)
97
+ q.expects(:unsubscribe)
98
+ @sub.stubs(:queues).returns({"a" =>q})
99
+ @sub.__send__(:pause, "a")
100
+ end
101
+
102
+ test "pausing a single queue which is already paused should not call amqp unsubscribe" do
103
+ q = mock("queue a")
104
+ q.expects(:subscribed?).returns(false)
105
+ q.expects(:unsubscribe).never
106
+ @sub.stubs(:queues).returns({"a" =>q})
107
+ @sub.__send__(:pause, "a")
108
+ end
109
+
110
+ test "resuming a single queue should call amqp subscribe" do
111
+ q = mock("queue a")
112
+ q.expects(:subscribed?).returns(false)
113
+ q.expects(:subscribe)
114
+ @sub.stubs(:queues).returns({"a" =>q})
115
+ @sub.__send__(:resume, "a")
116
+ end
117
+
118
+ test "resuming a single queue which is already subscribed should not call amqp subscribe" do
119
+ q = mock("queue a")
120
+ q.expects(:subscribed?).returns(true)
121
+ q.expects(:subscribe).never
122
+ @sub.stubs(:queues).returns({"a" =>q})
123
+ @sub.__send__(:resume, "a")
124
+ end
125
+
126
+ end
127
+
59
128
  class AdditionalSubscriptionServersTest < Test::Unit::TestCase
60
129
  def setup
61
130
  @config = Configuration.new
@@ -160,6 +229,8 @@ module Beetle
160
229
  @sub = client.send(:subscriber)
161
230
  @exception = Exception.new "murks"
162
231
  @handler = Handler.create(lambda{|*args| raise @exception})
232
+ # handler method 'processing_completed' should be called under all circumstances
233
+ @handler.expects(:processing_completed).once
163
234
  @callback = @sub.send(:create_subscription_callback, "my myessage", @queue, @handler, :exceptions => 1)
164
235
  end
165
236
 
@@ -225,7 +296,7 @@ module Beetle
225
296
  @sub.send(:subscribe_queues, %W(a b))
226
297
  end
227
298
 
228
- test "subscribe should subscribe with a subscription callback created from the registered block" do
299
+ test "subscribe should subscribe with a subscription callback created from the registered block and remember the subscription" do
229
300
  @client.register_queue(:some_queue, :exchange => "some_exchange", :key => "some_key")
230
301
  server = @sub.server
231
302
  header = header_with_params({})
@@ -239,24 +310,30 @@ module Beetle
239
310
  end
240
311
  @sub.register_handler("some_queue", &proc)
241
312
  q = mock("QUEUE")
242
- q.expects(:subscribe).with({:ack => true, :key => "#"}).yields(header, "foo")
243
- @sub.expects(:queues).returns({"some_queue" => q})
313
+ subscription_options = {:ack => true, :key => "#"}
314
+ q.expects(:subscribe).with(subscription_options).yields(header, "foo")
315
+ @sub.expects(:queues).returns({"some_queue" => q}).twice
244
316
  @sub.send(:subscribe, "some_queue")
245
317
  assert block_called
318
+ assert @sub.__send__(:has_subscription?, "some_queue")
319
+ q.expects(:subscribe).with(subscription_options).raises(MQ::Error)
320
+ assert_raises(Error) { @sub.send(:subscribe, "some_queue") }
246
321
  end
247
322
 
248
323
  test "subscribe should fail if no handler exists for given message" do
249
324
  assert_raises(Error){ @sub.send(:subscribe, "some_queue") }
250
325
  end
251
326
 
252
- test "listening should use eventmachine. create exchanges. bind queues. install subscribers. and yield." do
253
- @client.register_queue(:a)
254
- @client.register_message(:a)
327
+ test "listening on queues should use eventmachine. create exchanges. bind queues. install subscribers. and yield." do
328
+ @client.register_exchange(:an_exchange)
329
+ @client.register_queue(:a_queue, :exchange => :an_exchange)
330
+ @client.register_message(:a_message, :key => "foo", :exchange => :an_exchange)
331
+
255
332
  EM.expects(:run).yields
256
- @sub.expects(:create_exchanges).with(["a"])
257
- @sub.expects(:bind_queues).with(["a"])
258
- @sub.expects(:subscribe_queues).with(["a"])
259
- @sub.listen(["a"]) {}
333
+ @sub.expects(:create_exchanges).with(["an_exchange"])
334
+ @sub.expects(:bind_queues).with(["a_queue"])
335
+ @sub.expects(:subscribe_queues).with(["a_queue"])
336
+ @sub.listen_queues(["a_queue"]) {}
260
337
  end
261
338
  end
262
339
 
data/test/test_helper.rb CHANGED
@@ -1,10 +1,13 @@
1
1
  require 'rubygems'
2
- require 'active_support'
3
- require 'active_support/testing/declarative'
4
2
  require 'test/unit'
3
+ require 'mocha'
4
+ require 'active_support/testing/declarative'
5
+
6
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/beetle')
7
+
5
8
  begin
6
9
  require 'redgreen' unless ENV['TM_FILENAME']
7
- rescue MissingSourceFile
10
+ rescue LoadError => e
8
11
  end
9
12
 
10
13
  # we can remove this hack which is needed only for testing
@@ -17,8 +20,6 @@ rescue LoadError
17
20
  end
18
21
  end
19
22
 
20
- require 'mocha'
21
- require File.expand_path(File.dirname(__FILE__) + '/../lib/beetle')
22
23
 
23
24
  class Test::Unit::TestCase
24
25
  extend ActiveSupport::Testing::Declarative
metadata CHANGED
@@ -1,13 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: beetle
3
3
  version: !ruby/object:Gem::Version
4
- hash: 1
5
- prerelease: false
4
+ hash: 15424039
5
+ prerelease: 6
6
6
  segments:
7
7
  - 0
8
- - 2
9
- - 11
10
- version: 0.2.11
8
+ - 3
9
+ - 0
10
+ - rc
11
+ - 1
12
+ version: 0.3.0.rc.1
11
13
  platform: ruby
12
14
  authors:
13
15
  - Stefan Kaes
@@ -18,7 +20,7 @@ autorequire:
18
20
  bindir: bin
19
21
  cert_chain: []
20
22
 
21
- date: 2011-02-15 00:00:00 +01:00
23
+ date: 2011-08-27 00:00:00 +02:00
22
24
  default_executable: beetle
23
25
  dependencies:
24
26
  - !ruby/object:Gem::Dependency
@@ -29,12 +31,12 @@ dependencies:
29
31
  requirements:
30
32
  - - ">="
31
33
  - !ruby/object:Gem::Version
32
- hash: 25
34
+ hash: 31
33
35
  segments:
34
36
  - 0
35
37
  - 1
36
- - 1
37
- version: 0.1.1
38
+ - 2
39
+ version: 0.1.2
38
40
  type: :runtime
39
41
  version_requirements: *id001
40
42
  - !ruby/object:Gem::Dependency
@@ -43,36 +45,20 @@ dependencies:
43
45
  requirement: &id002 !ruby/object:Gem::Requirement
44
46
  none: false
45
47
  requirements:
46
- - - "="
48
+ - - ~>
47
49
  - !ruby/object:Gem::Version
48
- hash: 7
50
+ hash: 1
49
51
  segments:
50
52
  - 0
51
- - 6
52
- - 0
53
- version: 0.6.0
53
+ - 7
54
+ - 1
55
+ version: 0.7.1
54
56
  type: :runtime
55
57
  version_requirements: *id002
56
- - !ruby/object:Gem::Dependency
57
- name: bunny-ext
58
- prerelease: false
59
- requirement: &id003 !ruby/object:Gem::Requirement
60
- none: false
61
- requirements:
62
- - - ">="
63
- - !ruby/object:Gem::Version
64
- hash: 13
65
- segments:
66
- - 0
67
- - 6
68
- - 5
69
- version: 0.6.5
70
- type: :runtime
71
- version_requirements: *id003
72
58
  - !ruby/object:Gem::Dependency
73
59
  name: redis
74
60
  prerelease: false
75
- requirement: &id004 !ruby/object:Gem::Requirement
61
+ requirement: &id003 !ruby/object:Gem::Requirement
76
62
  none: false
77
63
  requirements:
78
64
  - - "="
@@ -84,14 +70,14 @@ dependencies:
84
70
  - 4
85
71
  version: 2.0.4
86
72
  type: :runtime
87
- version_requirements: *id004
73
+ version_requirements: *id003
88
74
  - !ruby/object:Gem::Dependency
89
75
  name: amqp
90
76
  prerelease: false
91
- requirement: &id005 !ruby/object:Gem::Requirement
77
+ requirement: &id004 !ruby/object:Gem::Requirement
92
78
  none: false
93
79
  requirements:
94
- - - ">="
80
+ - - ~>
95
81
  - !ruby/object:Gem::Version
96
82
  hash: 9
97
83
  segments:
@@ -100,14 +86,14 @@ dependencies:
100
86
  - 7
101
87
  version: 0.6.7
102
88
  type: :runtime
103
- version_requirements: *id005
89
+ version_requirements: *id004
104
90
  - !ruby/object:Gem::Dependency
105
91
  name: activesupport
106
92
  prerelease: false
107
- requirement: &id006 !ruby/object:Gem::Requirement
93
+ requirement: &id005 !ruby/object:Gem::Requirement
108
94
  none: false
109
95
  requirements:
110
- - - ~>
96
+ - - ">="
111
97
  - !ruby/object:Gem::Version
112
98
  hash: 11
113
99
  segments:
@@ -116,11 +102,11 @@ dependencies:
116
102
  - 4
117
103
  version: 2.3.4
118
104
  type: :runtime
119
- version_requirements: *id006
105
+ version_requirements: *id005
120
106
  - !ruby/object:Gem::Dependency
121
107
  name: daemons
122
108
  prerelease: false
123
- requirement: &id007 !ruby/object:Gem::Requirement
109
+ requirement: &id006 !ruby/object:Gem::Requirement
124
110
  none: false
125
111
  requirements:
126
112
  - - ">="
@@ -132,6 +118,22 @@ dependencies:
132
118
  - 10
133
119
  version: 1.0.10
134
120
  type: :runtime
121
+ version_requirements: *id006
122
+ - !ruby/object:Gem::Dependency
123
+ name: rake
124
+ prerelease: false
125
+ requirement: &id007 !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ hash: 49
131
+ segments:
132
+ - 0
133
+ - 8
134
+ - 7
135
+ version: 0.8.7
136
+ type: :development
135
137
  version_requirements: *id007
136
138
  - !ruby/object:Gem::Dependency
137
139
  name: mocha
@@ -162,9 +164,37 @@ dependencies:
162
164
  type: :development
163
165
  version_requirements: *id009
164
166
  - !ruby/object:Gem::Dependency
165
- name: cucumber
167
+ name: redgreen
166
168
  prerelease: false
167
169
  requirement: &id010 !ruby/object:Gem::Requirement
170
+ none: false
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ hash: 3
175
+ segments:
176
+ - 0
177
+ version: "0"
178
+ type: :development
179
+ version_requirements: *id010
180
+ - !ruby/object:Gem::Dependency
181
+ name: wirble
182
+ prerelease: false
183
+ requirement: &id011 !ruby/object:Gem::Requirement
184
+ none: false
185
+ requirements:
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ hash: 3
189
+ segments:
190
+ - 0
191
+ version: "0"
192
+ type: :development
193
+ version_requirements: *id011
194
+ - !ruby/object:Gem::Dependency
195
+ name: cucumber
196
+ prerelease: false
197
+ requirement: &id012 !ruby/object:Gem::Requirement
168
198
  none: false
169
199
  requirements:
170
200
  - - ">="
@@ -176,11 +206,11 @@ dependencies:
176
206
  - 2
177
207
  version: 0.7.2
178
208
  type: :development
179
- version_requirements: *id010
209
+ version_requirements: *id012
180
210
  - !ruby/object:Gem::Dependency
181
211
  name: daemon_controller
182
212
  prerelease: false
183
- requirement: &id011 !ruby/object:Gem::Requirement
213
+ requirement: &id013 !ruby/object:Gem::Requirement
184
214
  none: false
185
215
  requirements:
186
216
  - - ">="
@@ -190,7 +220,7 @@ dependencies:
190
220
  - 0
191
221
  version: "0"
192
222
  type: :development
193
- version_requirements: *id011
223
+ version_requirements: *id013
194
224
  description: A highly available, reliable messaging infrastructure
195
225
  email: developers@xing.com
196
226
  executables:
@@ -210,9 +240,11 @@ files:
210
240
  - examples/handling_exceptions.rb
211
241
  - examples/multiple_exchanges.rb
212
242
  - examples/multiple_queues.rb
243
+ - examples/pause_and_resume.rb
213
244
  - examples/redundant.rb
214
245
  - examples/rpc.rb
215
246
  - examples/simple.rb
247
+ - examples/test_publisher.rb
216
248
  - lib/beetle/base.rb
217
249
  - lib/beetle/client.rb
218
250
  - lib/beetle/commands/configuration_client.rb
@@ -290,16 +322,16 @@ required_rubygems_version: !ruby/object:Gem::Requirement
290
322
  requirements:
291
323
  - - ">="
292
324
  - !ruby/object:Gem::Version
293
- hash: 25
325
+ hash: 21
294
326
  segments:
295
327
  - 1
296
328
  - 3
297
- - 1
298
- version: 1.3.1
329
+ - 7
330
+ version: 1.3.7
299
331
  requirements: []
300
332
 
301
333
  rubyforge_project:
302
- rubygems_version: 1.3.7
334
+ rubygems_version: 1.6.2
303
335
  signing_key:
304
336
  specification_version: 3
305
337
  summary: High Availability AMQP Messaging with Redundant Queues