beetle 3.0.0.rc5 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2ae9b83119623fc200b35d2533bb1044cbad99b2b137235fc2bc03709584f244
4
- data.tar.gz: a027e1eb96f2a87d0326091ecdf9e01bd0be7990479e24bcda32b3615d8638bb
3
+ metadata.gz: 8a90e706d339d2a9e458c0b82113b58d5be98243fa9c9adbeaafb12fadeeb3d1
4
+ data.tar.gz: d89db5e989cfa51558ebda9db9c6acd10170c27fda0135378157e252570be626
5
5
  SHA512:
6
- metadata.gz: 045d2183037e6506e96b39df9b1a8b2e61e1bb6d9048270003903d4fd1dde6b2ff52be6cb095148f5b612d5cbb94ed7ffe55f913ba93c47034227f9fad61038a
7
- data.tar.gz: 0ed9d1bc8fc16815bb4bf396aa47d3d47b52c9473d9a9696c53bfd5ae3076d646aaaf47c2177919ea1bd23cb5b49e3e3c919c44955502f663c1792b086f3644b
6
+ metadata.gz: ee25e895ba9f3f38afe6d4985297435d93cab432b0dd8d483b8890a25b58f89d9993fac429c0d710bfeed49568dd1a28c948a0241297c1352ad2200987e0aebc
7
+ data.tar.gz: d1dff384a6bd1da983b2c400e524970d086759ae7c24106089ef0e36a167b7cb9b8769f6938b96b9a5b78435abb945cddba432b14755716f67c96e508783ef51
data/RELEASE_NOTES.rdoc CHANGED
@@ -1,36 +1,30 @@
1
1
  = Release Notes
2
2
 
3
- == Version 3.0.0.rc5
4
- * ake sure to clean dedup store when ack_count is larger than 2
3
+ == Version 3.0.0
5
4
 
6
- == Version 3.0.0.rc4
7
- * fixed incorrect calculation of expiry time in garbage collector
8
-
9
-
10
- == Version 3.0.0.rc3
11
- * store redis gc stats in redis and display them in the configuration
12
- server web UI
13
-
14
- == Version 3.0.0.rc2
5
+ * provide client method to setup queues and queue policies.
6
+ Setting up queue policies on demand in publisher and subscriber is OK
7
+ for a small number of queues, publishers and subscribers. But the HTTP
8
+ API of RabbitMQ doesn't scale all that well, so that a large number
9
+ of HTTP calls to set up queue policies can in fact crash a server.
10
+ * allow queue mode specification and dead lettering specification on a
11
+ per queue basis.
15
12
  * change policy setup to be asynchronous, on demand in publisher and
16
13
  consumer. Users have to run a processors to listen to messages and
17
14
  call setup_queue_policies! with the parses JSON paylod of the
18
15
  message.
19
- * added dump_expiries command to beetle to dump dediplication store
20
- expiry times.
21
- * added delete_queue_keys command to beetle to allow deletion of
22
- excess dedup store entries for a given queue.
16
+ * store redis gc stats in redis and display them in the configuration
17
+ server web UI
23
18
  * config server: store current master server in consul, if consul
24
19
  config has been provided.
25
20
  * config server: server correctly updates configured client ids when
26
21
  they change in consul.
27
-
28
- == Version 3.0.0.rc1
29
- * provide client method to setup queues and queue policies.
30
- Setting up queue policies on demand in publisher and subscriber is OK
31
- for a small number of queues, publishers and subscribers. But the HTTP
32
- API of RabbitMQ doesn't scale all that well, so that a large number
33
- of HTTP calls to set up queue policies can in fact crash a server.
22
+ * don't create dead letter queues when using the trace functionality
23
+ * make sure to clean dedup store when ack_count is larger than 2
24
+ * added dump_expiries command to beetle to dump dediplication store
25
+ expiry times.
26
+ * added delete_queue_keys command to beetle to allow deletion of
27
+ excess dedup store entries for a given queue.
34
28
 
35
29
  == Version 2.3.2
36
30
  * config server: fixed a race condition when accessing server state.
data/Rakefile CHANGED
@@ -85,7 +85,7 @@ end
85
85
  namespace :consul do
86
86
  desc "start consul agent in development mode"
87
87
  task :start do
88
- system "consul agent -dev -node machine"
88
+ exec "consul agent -dev -node machine"
89
89
  end
90
90
  end
91
91
 
@@ -0,0 +1,68 @@
1
+ # throttling.rb
2
+ # this example shows you how throttling the publisher works
3
+ #
4
+ # ! check the examples/README.rdoc for information on starting your redis/rabbit !
5
+ #
6
+ # start it with ruby throttling.rb
7
+
8
+ require "rubygems"
9
+ require File.expand_path("../lib/beetle", File.dirname(__FILE__))
10
+
11
+ # set Beetle log level to info, less noisy than debug
12
+ Beetle.config.logger.level = Logger::INFO
13
+ # override default of 60 seconds
14
+ Beetle.config.throttling_refresh_interval = 5
15
+
16
+ # setup clients
17
+ client = Beetle::Client.new
18
+ client.register_queue(:test)
19
+ client.register_message(:test)
20
+ client.throttle(:test => 50)
21
+
22
+ consumer = Beetle::Client.new
23
+ consumer.register_queue(:test)
24
+ consumer.register_message(:test)
25
+
26
+ # purge the test queue
27
+ client.purge(:test)
28
+
29
+ # empty the dedup store
30
+ client.deduplication_store.flushdb
31
+
32
+ # register our handler to the message, check out the message.rb for more stuff you can get from the message object
33
+ messages_received = 0
34
+ consumer.register_handler(:test) do |message|
35
+ sleep 0.2
36
+ messages_received += 1
37
+ puts "Received #{messages_received} messages"
38
+ end
39
+
40
+ interrupted = false
41
+
42
+ t = Thread.new do
43
+ c = 0
44
+ loop do
45
+ break if c == 200 || interrupted
46
+ 100.times do |i|
47
+ client.publish(:test, (c+=1).to_s)
48
+ puts "Published message #{c}"
49
+ sleep 0.1
50
+ end
51
+ end
52
+ end
53
+
54
+ trap('INT') do
55
+ interrupted = true
56
+ consumer.stop_listening
57
+ end
58
+
59
+ # start listening
60
+ # this starts the event machine event loop using EM.run
61
+ # the block passed to listen will be yielded as the last step of the setup process
62
+ consumer.listen do
63
+ EM.add_periodic_timer(1) do
64
+ consumer.stop_listening if messages_received >= 200
65
+ end
66
+ end
67
+
68
+ t.join
data/lib/beetle.rb CHANGED
@@ -50,7 +50,7 @@ module Beetle
50
50
  # use ruby's autoload mechanism for loading beetle classes
51
51
  lib_dir = File.expand_path(File.dirname(__FILE__) + '/beetle/')
52
52
  Dir["#{lib_dir}/*.rb"].each do |libfile|
53
- autoload File.basename(libfile)[/^(.*)\.rb$/, 1].classify, libfile
53
+ autoload File.basename(libfile)[/^(.*)\.rb$/, 1].camelize, libfile
54
54
  end
55
55
 
56
56
  require "#{lib_dir}/redis_ext"
data/lib/beetle/base.rb CHANGED
@@ -82,6 +82,7 @@ module Beetle
82
82
  end
83
83
  return {
84
84
  :queue_name => target_queue,
85
+ :bindings => @client.bindings[target_queue],
85
86
  :dead_letter_queue_name => dead_letter_queue_name,
86
87
  :message_ttl => @client.config.dead_lettering_msg_ttl,
87
88
  }.merge(policy_options)
data/lib/beetle/client.rb CHANGED
@@ -55,7 +55,7 @@ module Beetle
55
55
  @messages = {}
56
56
  @bindings = {}
57
57
  @deduplication_store = DeduplicationStore.new(config)
58
- @dead_lettering = DeadLettering.new(config)
58
+ @queue_properties = QueueProperties.new(config)
59
59
  load_brokers_from_config
60
60
  register_exchange(config.beetle_policy_exchange_name)
61
61
  # make sure dead lettering is false for the policy update queue
@@ -258,6 +258,27 @@ module Beetle
258
258
  subscriber.resume_listening(queues)
259
259
  end
260
260
 
261
+ # throttle publishing of messages based on queue lengths.
262
+ #
263
+ # client.throttle(:foo => 10000, :bar => 10_000)
264
+ #
265
+ # throttle publisher to 1 message per second as long as queue foo has more
266
+ # than 10000 entries or queue bar has more than 100000 entries across all
267
+ # servers. Queue lenghts are periodically determined during publishing. You
268
+ # only want to use this feature if you plan to publish a huge amount of
269
+ # messages to slow consumers so as to not overload the broker or the redis
270
+ # deduplication store. You'll want to use this feature when running background
271
+ # jobs that publish huge amounts of messages to avoid overloading brokers
272
+ # and the message deduplication store.
273
+ def throttle(throttling_options)
274
+ publisher.throttle(throttling_options.stringify_keys)
275
+ end
276
+
277
+ # is the publisher currently throttled?
278
+ def throttled?
279
+ publisher.throttled?
280
+ end
281
+
261
282
  # traces queues without consuming them. useful for debugging message flow.
262
283
  def trace(queue_names=self.queues.keys, tracer=nil, &block)
263
284
  queues_to_trace = self.queues.slice(*queue_names)
@@ -275,7 +296,9 @@ module Beetle
275
296
  puts "DATA: #{msg.data}"
276
297
  end
277
298
  register_handler(queue_names){|msg| tracer.call msg }
299
+ @subscriber.tracing = true
278
300
  listen_queues(queue_names, &block)
301
+ @subscriber.tracing = false
279
302
  end
280
303
 
281
304
  # evaluate the ruby files matching the given +glob+ pattern in the context of the client instance.
@@ -299,8 +322,8 @@ module Beetle
299
322
  @subscriber = nil
300
323
  end
301
324
 
302
- def set_queue_policies!(message_payload)
303
- @dead_lettering.set_queue_policies!(message_payload)
325
+ def update_queue_properties!(message_payload)
326
+ @queue_properties.update_queue_properties!(message_payload)
304
327
  end
305
328
 
306
329
  private
@@ -350,7 +373,7 @@ module Beetle
350
373
  end
351
374
 
352
375
  def queue_name_for_tracing(queue)
353
- "trace-#{queue}-#{Beetle.hostname}-#{$$}"
376
+ "trace-#{queue}-#{Beetle.hostname}"
354
377
  end
355
378
 
356
379
  def load_brokers_from_config
@@ -93,7 +93,7 @@ module Beetle
93
93
  attr_accessor :dead_lettering_msg_ttl
94
94
 
95
95
  # Read timeout for http requests to create dead letter bindings
96
- attr_accessor :dead_lettering_read_timeout
96
+ attr_accessor :rabbitmq_api_read_timeout
97
97
 
98
98
  # Returns the port on which the Rabbit API is hosted
99
99
  attr_accessor :api_port
@@ -112,6 +112,9 @@ module Beetle
112
112
  # decreased parallelism.
113
113
  attr_accessor :prefetch_count
114
114
 
115
+ # refresh interval for determining queue lenghts for throttling.
116
+ attr_accessor :throttling_refresh_interval
117
+
115
118
  # directory to store large intermediate files (defaults '/tmp')
116
119
  attr_accessor :tmpdir
117
120
 
@@ -156,13 +159,14 @@ module Beetle
156
159
  self.prefetch_count = 1
157
160
 
158
161
  self.dead_lettering_enabled = false
159
- self.dead_lettering_msg_ttl = 1000 #1 second
160
- self.dead_lettering_read_timeout = 3 #3 seconds
162
+ self.dead_lettering_msg_ttl = 1000 # 1 second
163
+ self.rabbitmq_api_read_timeout = 5 # 5 seconds
161
164
 
162
165
  self.lazy_queues_enabled = false
166
+ self.throttling_refresh_interval = 60 # seconds
163
167
 
164
168
  self.publishing_timeout = 0
165
- self.publisher_connect_timeout = 5
169
+ self.publisher_connect_timeout = 5 # seconds
166
170
  self.tmpdir = "/tmp"
167
171
 
168
172
  self.log_file = STDOUT
@@ -9,9 +9,24 @@ module Beetle
9
9
  @exchanges_with_bound_queues = {}
10
10
  @dead_servers = {}
11
11
  @bunnies = {}
12
+ @throttling_options = {}
13
+ @next_throttle_refresh = Time.now
14
+ @throttled = false
12
15
  at_exit { stop }
13
16
  end
14
17
 
18
+ def throttled?
19
+ @throttled
20
+ end
21
+
22
+ def throttling?
23
+ !@throttling_options.empty?
24
+ end
25
+
26
+ def throttling_status
27
+ @throttled ? 'throttled' : 'unthrottled'
28
+ end
29
+
15
30
  # list of exceptions potentially raised by bunny
16
31
  # these need to be lazy, because qrack exceptions are only defined after a connection has been established
17
32
  def bunny_exceptions
@@ -29,6 +44,7 @@ module Beetle
29
44
  exchange_name = opts.delete(:exchange)
30
45
  opts.delete(:queue)
31
46
  recycle_dead_servers unless @dead_servers.empty?
47
+ throttle!
32
48
  if opts[:redundant]
33
49
  publish_with_redundancy(exchange_name, message_name, data, opts)
34
50
  else
@@ -135,6 +151,16 @@ module Beetle
135
151
  [status, result]
136
152
  end
137
153
 
154
+ def throttle(queue_options)
155
+ @throttling_options = queue_options
156
+ end
157
+
158
+ def throttle!
159
+ return unless throttling?
160
+ refresh_throttling!
161
+ sleep 1 if throttled?
162
+ end
163
+
138
164
  def purge(queue_names) #:nodoc:
139
165
  each_server do
140
166
  queue_names.each do |name|
@@ -251,5 +277,30 @@ module Beetle
251
277
  @exchanges[@server] = {}
252
278
  @queues[@server] = {}
253
279
  end
280
+
281
+ def refresh_throttling!
282
+ t = Time.now
283
+ return if t < @next_throttle_refresh
284
+ @next_throttle_refresh = t + @client.config.throttling_refresh_interval
285
+ old_throttled = @throttled
286
+ @throttled = false
287
+ @throttling_options.each do |queue_name, max_length|
288
+ begin
289
+ len = 0
290
+ each_server do
291
+ len += queue(queue_name).status[:message_count]
292
+ end
293
+ # logger.debug "Beetle: queue '#{queue_name}' has size #{len}"
294
+ if len > max_length
295
+ @throttled = true
296
+ break
297
+ end
298
+ rescue => e
299
+ logger.warn "Beetle: could not fetch queue length for queue '#{queue_name}': #{e}"
300
+ end
301
+ end
302
+ logger.info "Beetle: publisher #{throttling_status}" if @throttled != old_throttled
303
+ end
304
+
254
305
  end
255
306
  end
@@ -2,7 +2,7 @@ require 'net/http'
2
2
  require 'json'
3
3
 
4
4
  module Beetle
5
- class DeadLettering
5
+ class QueueProperties
6
6
  class FailedRabbitRequest < StandardError; end
7
7
 
8
8
  attr_reader :config
@@ -11,8 +11,12 @@ module Beetle
11
11
  @config = config
12
12
  end
13
13
 
14
- def set_queue_policies!(options)
15
- # logger.debug "Setting queue policies: #{options.inspect}"
14
+ def vhost
15
+ CGI.escape(@config.vhost)
16
+ end
17
+
18
+ def update_queue_properties!(options)
19
+ logger.info "Updating queue properties: #{options.inspect}"
16
20
  options = options.symbolize_keys
17
21
  server = options[:server]
18
22
  target_queue = options[:queue_name]
@@ -21,6 +25,7 @@ module Beetle
21
25
 
22
26
  target_queue_options = policy_options.merge(:routing_key => dead_letter_queue_name)
23
27
  set_queue_policy!(server, target_queue, target_queue_options)
28
+ remove_obsolete_bindings(server, target_queue, options[:bindings]) if options.has_key?(:bindings)
24
29
 
25
30
  dead_letter_queue_options = policy_options.merge(:routing_key => target_queue, :message_ttl => options[:message_ttl])
26
31
  set_queue_policy!(server, dead_letter_queue_name, dead_letter_queue_options)
@@ -34,7 +39,6 @@ module Beetle
34
39
 
35
40
  return unless options[:dead_lettering] || options[:lazy]
36
41
 
37
- vhost = CGI.escape(config.vhost)
38
42
  # no need to worry that the server has the port 5672. Net:HTTP will take care of this. See below.
39
43
  request_url = URI("http://#{server}/api/policies/#{vhost}/#{queue_name}_policy")
40
44
  request = Net::HTTP::Put.new(request_url)
@@ -68,11 +72,63 @@ module Beetle
68
72
  :ok
69
73
  end
70
74
 
75
+ def remove_obsolete_bindings(server, queue_name, bindings)
76
+ logger.debug "Removing obsolete bindings"
77
+ raise ArgumentError.new("server missing") if server.blank?
78
+ raise ArgumentError.new("queue name missing") if queue_name.blank?
79
+ raise ArgumentError.new("bindings missing") if bindings.nil?
80
+
81
+ desired_bindings = bindings.each_with_object({}) do |b, desired|
82
+ desired[[b[:exchange], b[:key]]] = b.except(:exchange, :key)
83
+ end
84
+
85
+ server_bindings = retrieve_bindings(server, queue_name)
86
+ server_bindings.each do |b|
87
+ next unless b["destination_type"] == "queue" || b["destination"] == queue_name
88
+ next if b["source"] == ""
89
+ source_route = b.values_at("source", "routing_key")
90
+ unless desired_bindings.has_key?(source_route)
91
+ logger.info "Removing obsolete binding: #{b.inspect}"
92
+ remove_binding(server, queue_name, b["source"], b["properties_key"])
93
+ end
94
+ end
95
+ end
96
+
97
+ def retrieve_bindings(server, queue_name)
98
+ request_url = URI("http://#{server}/api/queues/#{vhost}/#{queue_name}/bindings")
99
+ request = Net::HTTP::Get.new(request_url)
100
+
101
+ response = run_rabbit_http_request(request_url, request) do |http|
102
+ http.request(request)
103
+ end
104
+
105
+ unless response.code == "200"
106
+ log_error("Failed to retrieve bindings for queue #{queue_name}", response)
107
+ raise FailedRabbitRequest.new("Could not retrieve queue bindings")
108
+ end
109
+
110
+ JSON.parse(response.body)
111
+ end
112
+
113
+ def remove_binding(server, queue_name, exchange, properties_key)
114
+ request_url = URI("http://#{server}/api/bindings/#{vhost}/e/#{exchange}/q/#{queue_name}/#{properties_key}")
115
+ request = Net::HTTP::Delete.new(request_url)
116
+
117
+ response = run_rabbit_http_request(request_url, request) do |http|
118
+ http.request(request)
119
+ end
120
+
121
+ unless %w(200 201 204).include?(response.code)
122
+ log_error("Failed to remove obsolete binding for queue #{queue_name}", response)
123
+ raise FailedRabbitRequest.new("Could not retrieve queue bindings")
124
+ end
125
+ end
126
+
71
127
  def run_rabbit_http_request(uri, request, &block)
72
128
  request.basic_auth(config.user, config.password)
73
129
  request["Content-Type"] = "application/json"
74
130
  http = Net::HTTP.new(uri.hostname, config.api_port)
75
- http.read_timeout = config.dead_lettering_read_timeout
131
+ http.read_timeout = config.rabbitmq_api_read_timeout
76
132
  # don't do this in production:
77
133
  # http.set_debug_output(logger.instance_eval{ @logdev.dev })
78
134
  http.start do |instance|
@@ -4,6 +4,11 @@ module Beetle
4
4
  # Manages subscriptions and message processing on the receiver side of things.
5
5
  class Subscriber < Base
6
6
 
7
+ attr_accessor :tracing
8
+ def tracing?
9
+ @tracing
10
+ end
11
+
7
12
  # create a new subscriber instance
8
13
  def initialize(client, options = {}) #:nodoc:
9
14
  super
@@ -14,6 +19,7 @@ module Beetle
14
19
  @subscriptions = {}
15
20
  @listened_queues = []
16
21
  @channels_closed = false
22
+ @tracing = false
17
23
  end
18
24
 
19
25
  # the client calls this method to subscribe to a list of queues.
@@ -206,8 +212,11 @@ module Beetle
206
212
 
207
213
  def bind_queue!(queue_name, creation_keys, exchange_name, binding_keys)
208
214
  queue = channel.queue(queue_name, creation_keys)
209
- policy_options = bind_dead_letter_queue!(channel, queue_name, creation_keys)
210
- publish_policy_options(policy_options)
215
+ unless tracing?
216
+ # we don't want to create dead-letter queues for tracing
217
+ policy_options = bind_dead_letter_queue!(channel, queue_name, creation_keys)
218
+ publish_policy_options(policy_options)
219
+ end
211
220
  exchange = exchange(exchange_name)
212
221
  queue.bind(exchange, binding_keys)
213
222
  queue
@@ -1,3 +1,3 @@
1
1
  module Beetle
2
- VERSION = "3.0.0.rc5"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -68,6 +68,7 @@ module Beetle
68
68
  channel = stub('channel')
69
69
  expected_options = {
70
70
  :queue_name => "QUEUE_NAME",
71
+ :bindings=>[{:exchange=>"QUEUE_NAME", :key=>"QUEUE_NAME"}],
71
72
  :dead_letter_queue_name=>"QUEUE_NAME_dead_letter",
72
73
  :message_ttl => 1000,
73
74
  :dead_lettering => false,
@@ -84,6 +85,7 @@ module Beetle
84
85
 
85
86
  expected_options = {
86
87
  :queue_name => "QUEUE_NAME",
88
+ :bindings=>[{:exchange=>"QUEUE_NAME", :key=>"QUEUE_NAME"}],
87
89
  :dead_letter_queue_name=>"QUEUE_NAME_dead_letter",
88
90
  :message_ttl => 1000,
89
91
  :dead_lettering => true,
@@ -356,10 +356,10 @@ module Beetle
356
356
  client.stop_listening
357
357
  end
358
358
 
359
- test "should delegate set_queue_policies! to the dead lettering instance" do
359
+ test "should delegate update_queue_properties! to the queue policies instance" do
360
360
  client = Client.new
361
- client.instance_variable_get(:@dead_lettering).expects(:set_queue_policies!)
362
- client.set_queue_policies!({})
361
+ client.instance_variable_get(:@queue_properties).expects(:update_queue_properties!)
362
+ client.update_queue_properties!({})
363
363
  end
364
364
 
365
365
  test "should delegate pause_listening to the subscriber instance" do
@@ -430,6 +430,18 @@ module Beetle
430
430
  client.trace(["test"])
431
431
  end
432
432
 
433
+ test "delegates setting throttling options to the publisher instance and stringifies the queue names" do
434
+ client = Client.new
435
+ client.send(:publisher).expects(:throttle).with("test" => 1)
436
+ client.throttle(:test => 1)
437
+ end
438
+
439
+ test "delegates querying the throttling status to the publisher instance" do
440
+ client = Client.new
441
+ client.send(:publisher).expects(:throttled?)
442
+ client.throttled?
443
+ end
444
+
433
445
  def message_stub_for_tracing
434
446
  header_stub = stub_everything("header")
435
447
  header_stub.stubs(:method).returns(stub_everything("method"))
@@ -80,6 +80,7 @@ module Beetle
80
80
  @pub.send(:mark_server_dead)
81
81
  publishing = sequence('publishing')
82
82
  @pub.expects(:recycle_dead_servers).in_sequence(publishing)
83
+ @pub.expects(:throttle!).in_sequence(publishing)
83
84
  @pub.expects(:publish_with_failover).with("mama-exchange", "mama", @data, @opts).in_sequence(publishing)
84
85
  @pub.publish("mama", @data)
85
86
  end
@@ -89,6 +90,7 @@ module Beetle
89
90
  @pub.send(:mark_server_dead)
90
91
  publishing = sequence('publishing')
91
92
  @pub.expects(:recycle_dead_servers).in_sequence(publishing)
93
+ @pub.expects(:throttle!).in_sequence(publishing)
92
94
  @pub.expects(:publish_with_redundancy).with("mama-exchange", "mama", @data, @opts.merge(:redundant => true)).in_sequence(publishing)
93
95
  @pub.publish("mama", @data, :redundant => true)
94
96
  end
@@ -339,6 +341,57 @@ module Beetle
339
341
  @pub.setup_queues_and_policies(["queue"])
340
342
  end
341
343
 
344
+ test "reports whether it has been throttled" do
345
+ assert !@pub.throttled?
346
+ @pub.instance_variable_set :@throttled, true
347
+ assert @pub.throttled?
348
+ end
349
+
350
+ test "sets throttling options" do
351
+ h = { "x" => 1, "y" => 2}
352
+ @pub.throttle(h)
353
+ assert_equal h, @pub.instance_variable_get(:@throttling_options)
354
+ end
355
+
356
+ test "throttle! sleeps appropriately when refreshing throttling" do
357
+ @pub.expects(:throttling?).returns(true).twice
358
+ @pub.expects(:refresh_throttling!).twice
359
+ @pub.expects(:throttled?).returns(true)
360
+ @pub.expects(:sleep).with(1)
361
+ @pub.throttle!
362
+ @pub.expects(:throttled?).returns(false)
363
+ @pub.expects(:sleep).never
364
+ @pub.throttle!
365
+ end
366
+
367
+ test "returns a throttling status" do
368
+ assert_equal 'unthrottled', @pub.throttling_status
369
+ @pub.instance_variable_set :@throttled, true
370
+ assert_equal 'throttled', @pub.throttling_status
371
+ end
372
+
373
+ test "refresh_throttling! does not recompute values when refresh interval has not passed" do
374
+ @pub.instance_variable_set :@next_throttle_refresh, Time.now + 1
375
+ options = {}
376
+ @pub.throttle(options)
377
+ options.expects(:each).never
378
+ @pub.__send__ :refresh_throttling!
379
+ end
380
+
381
+ test "refresh_throttling! throttles when queue length exceeds limit" do
382
+ assert !@pub.throttled?
383
+ @pub.instance_variable_set :@next_throttle_refresh, Time.now - 1
384
+ options = { "test" => 100 }
385
+ @pub.throttle(options)
386
+ @pub.expects(:each_server).yields
387
+ q = mock("queue")
388
+ q.expects(:status).returns(:message_count => 500)
389
+ @pub.expects(:queue).returns(q)
390
+ @pub.logger.expects(:info)
391
+ @pub.__send__ :refresh_throttling!
392
+ assert @pub.throttled?
393
+ end
394
+
342
395
  end
343
396
 
344
397
  class PublisherExchangeManagementTest < Minitest::Test
@@ -0,0 +1,210 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ module Beetle
4
+ class SetDeadLetterPolicyTest < Minitest::Test
5
+ def setup
6
+ @server = "localhost:15672"
7
+ @queue_name = "QUEUE_NAME"
8
+ @config = Configuration.new
9
+ @config.logger = Logger.new("/dev/null")
10
+ @queue_properties = QueueProperties.new(@config)
11
+ end
12
+
13
+ test "raises exception when queue name wasn't specified" do
14
+ assert_raises ArgumentError do
15
+ @queue_properties.set_queue_policy!(@server, "")
16
+ end
17
+ end
18
+
19
+ test "raises exception when no server was specified" do
20
+ assert_raises ArgumentError do
21
+ @queue_properties.set_queue_policy!("", @queue_name)
22
+ end
23
+ end
24
+
25
+ test "update_queue_properties! calls set_queue_policy for both target queue and dead letter queue" do
26
+ options = {
27
+ :server => "server", :lazy => true, :dead_lettering => true,
28
+ :queue_name => "QUEUE_NAME", :dead_letter_queue_name => "QUEUE_NAME_dead_letter",
29
+ :message_ttl => 10000
30
+ }
31
+ @queue_properties.expects(:set_queue_policy!).with("server", "QUEUE_NAME",
32
+ :lazy => true, :dead_lettering => true,
33
+ :routing_key => "QUEUE_NAME_dead_letter")
34
+ @queue_properties.expects(:set_queue_policy!).with("server", "QUEUE_NAME_dead_letter",
35
+ :lazy => true, :dead_lettering => true,
36
+ :routing_key => "QUEUE_NAME",
37
+ :message_ttl => 10000)
38
+ @queue_properties.update_queue_properties!(options)
39
+ end
40
+
41
+ test "creates a policy by posting to the rabbitmq if dead lettering is enabled" do
42
+ stub_request(:put, "http://localhost:15672/api/policies/%2F/QUEUE_NAME_policy")
43
+ .with(basic_auth: ['guest', 'guest'])
44
+ .with(:body => {
45
+ "pattern" => "^QUEUE_NAME$",
46
+ "priority" => 1,
47
+ "apply-to" => "queues",
48
+ "definition" => {
49
+ "dead-letter-routing-key" => "QUEUE_NAME_dead_letter",
50
+ "dead-letter-exchange" => ""
51
+ }}.to_json)
52
+ .to_return(:status => 204)
53
+
54
+ @queue_properties.set_queue_policy!(@server, @queue_name, :lazy => false, :dead_lettering => true, :routing_key => "QUEUE_NAME_dead_letter")
55
+ end
56
+
57
+ test "creates a policy by posting to the rabbitmq if lazy queues are enabled" do
58
+ stub_request(:put, "http://localhost:15672/api/policies/%2F/QUEUE_NAME_policy")
59
+ .with(basic_auth: ['guest', 'guest'])
60
+ .with(:body => {
61
+ "pattern" => "^QUEUE_NAME$",
62
+ "priority" => 1,
63
+ "apply-to" => "queues",
64
+ "definition" => {
65
+ "queue-mode" => "lazy"
66
+ }}.to_json)
67
+ .to_return(:status => 204)
68
+
69
+ @queue_properties.set_queue_policy!(@server, @queue_name, :lazy => true, :dead_lettering => false)
70
+ end
71
+
72
+ test "raises exception when policy couldn't successfully be created" do
73
+ stub_request(:put, "http://localhost:15672/api/policies/%2F/QUEUE_NAME_policy")
74
+ .with(basic_auth: ['guest', 'guest'])
75
+ .to_return(:status => [405])
76
+
77
+ assert_raises QueueProperties::FailedRabbitRequest do
78
+ @queue_properties.set_queue_policy!(@server, @queue_name, :lazy => true, :dead_lettering => true)
79
+ end
80
+ end
81
+
82
+ test "can optionally specify a message ttl" do
83
+ stub_request(:put, "http://localhost:15672/api/policies/%2F/QUEUE_NAME_policy")
84
+ .with(basic_auth: ['guest', 'guest'])
85
+ .with(:body => {
86
+ "pattern" => "^QUEUE_NAME$",
87
+ "priority" => 1,
88
+ "apply-to" => "queues",
89
+ "definition" => {
90
+ "dead-letter-routing-key" => "QUEUE_NAME_dead_letter",
91
+ "dead-letter-exchange" => "",
92
+ "message-ttl" => 10000
93
+ }}.to_json)
94
+ .to_return(:status => 204)
95
+
96
+ @queue_properties.set_queue_policy!(@server, @queue_name, :dead_lettering => true, :message_ttl => 10000, :routing_key => "QUEUE_NAME_dead_letter")
97
+ end
98
+
99
+ test "properly encodes the vhost from the configuration" do
100
+ stub_request(:put, "http://localhost:15672/api/policies/foo%2F/QUEUE_NAME_policy")
101
+ .with(basic_auth: ['guest', 'guest'])
102
+ .with(:body => {
103
+ "pattern" => "^QUEUE_NAME$",
104
+ "priority" => 1,
105
+ "apply-to" => "queues",
106
+ "definition" => {
107
+ "dead-letter-routing-key" => "QUEUE_NAME_dead_letter",
108
+ "dead-letter-exchange" => ""
109
+ }}.to_json)
110
+ .to_return(:status => 204)
111
+
112
+ @config.vhost = "foo/"
113
+
114
+ @queue_properties.set_queue_policy!(@server, @queue_name, :dead_lettering => true, :routing_key => "QUEUE_NAME_dead_letter")
115
+ end
116
+
117
+ test "update_queue_properties! calls remove_obsolete_bindings if bindings are part of the options hash" do
118
+ bindings = [{:exchange => "foo", :key => "a.b.c"}]
119
+ options = {
120
+ :server => "server", :lazy => true, :dead_lettering => true,
121
+ :queue_name => "QUEUE_NAME", :dead_letter_queue_name => "QUEUE_NAME_dead_letter",
122
+ :message_ttl => 10000,
123
+ :bindings => bindings
124
+ }
125
+ @queue_properties.expects(:set_queue_policy!).twice
126
+ @queue_properties.expects(:remove_obsolete_bindings).with("server", "QUEUE_NAME", bindings)
127
+ @queue_properties.update_queue_properties!(options)
128
+ end
129
+
130
+ test "remove_obsolete_bindings removes an obsolete binding but does not remove the default binding" do
131
+ bindings = [{:exchange => "foo", :key => "QUEUE_NAME"}, {:exchange => "foo", :key => "a.b.c"}]
132
+ stub_request(:get, "http://localhost:15672/api/queues/%2F/QUEUE_NAME/bindings")
133
+ .with(basic_auth: ['guest', 'guest'])
134
+ .to_return(:status => 200,
135
+ :body =>[
136
+ {
137
+ "destination_type" => "queue",
138
+ "source" => "",
139
+ "routing_key" => "QUEUE_NAME",
140
+ "destination" => "QUEUE_NAME",
141
+ "vhost" => "/",
142
+ "properties_key" => "QUEUE_NAME",
143
+ "arguments" => {}
144
+ },
145
+ {
146
+ "destination_type" => "queue",
147
+ "source" => "foo",
148
+ "routing_key" => "QUEUE_NAME",
149
+ "destination" => "QUEUE_NAME",
150
+ "vhost" => "/",
151
+ "properties_key" => "QUEUE_NAME",
152
+ "arguments" => {}
153
+ },
154
+ {
155
+ "destination_type" => "queue",
156
+ "source" => "foo",
157
+ "routing_key" => "a.b.c",
158
+ "destination" => "QUEUE_NAME",
159
+ "vhost" => "/",
160
+ "properties_key" => "a.b.c",
161
+ "arguments" => {}
162
+ },
163
+ {
164
+ "destination_type" => "queue",
165
+ "source" => "foofoo",
166
+ "routing_key" => "x.y.z",
167
+ "destination" => "QUEUE_NAME",
168
+ "vhost" => "/",
169
+ "properties_key" => "x.y.z",
170
+ "arguments" => {}
171
+ }
172
+ ].to_json)
173
+
174
+ stub_request(:delete, "http://localhost:15672/api/bindings/%2F/e/foofoo/q/QUEUE_NAME/x.y.z")
175
+ .with(basic_auth: ['guest', 'guest'])
176
+ .to_return(:status => 200)
177
+
178
+ @queue_properties.remove_obsolete_bindings(@server, "QUEUE_NAME", bindings)
179
+ end
180
+
181
+ test "raises an error when bindings cannot be retrieved" do
182
+ stub_request(:get, "http://localhost:15672/api/queues/%2F/QUEUE_NAME/bindings")
183
+ .with(basic_auth: ['guest', 'guest'])
184
+ .to_return(:status => 500)
185
+ assert_raises(Beetle::QueueProperties::FailedRabbitRequest) { @queue_properties.remove_obsolete_bindings(@server, "QUEUE_NAME", []) }
186
+ end
187
+
188
+ test "raises an error when bindings cannot be deleted" do
189
+ stub_request(:get, "http://localhost:15672/api/queues/%2F/QUEUE_NAME/bindings")
190
+ .with(basic_auth: ['guest', 'guest'])
191
+ .to_return(:status => 200, :body => [
192
+ {
193
+ "destination_type" => "queue",
194
+ "source" => "foofoo",
195
+ "routing_key" => "x.y.z",
196
+ "destination" => "QUEUE_NAME",
197
+ "vhost" => "/",
198
+ "properties_key" => "x.y.z",
199
+ "arguments" => {}
200
+ }
201
+ ].to_json)
202
+ stub_request(:delete, "http://localhost:15672/api/bindings/%2F/e/foofoo/q/QUEUE_NAME/x.y.z")
203
+ .with(basic_auth: ['guest', 'guest'])
204
+ .to_return(:status => 500)
205
+
206
+ assert_raises(Beetle::QueueProperties::FailedRabbitRequest) { @queue_properties.remove_obsolete_bindings(@server, "QUEUE_NAME", []) }
207
+ end
208
+
209
+ end
210
+ end
@@ -246,7 +246,7 @@ module Beetle
246
246
  @client.config.dead_lettering_enabled = false
247
247
  end
248
248
 
249
- test "should call reject on the message header when processing the handler returns true on reject? if dead_lettering has been enabled" do
249
+ test "should call reject on the message header when processing the handler returns true on reject? if dead lettering has been enabled" do
250
250
  header = header_with_params({})
251
251
  result = mock("result")
252
252
  result.expects(:reject?).returns(true)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: beetle
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0.rc5
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Kaes
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2019-05-21 00:00:00.000000000 Z
15
+ date: 2019-05-27 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: bunny
@@ -327,6 +327,7 @@ files:
327
327
  - examples/redundant.rb
328
328
  - examples/rpc.rb
329
329
  - examples/simple.rb
330
+ - examples/throttling.rb
330
331
  - features/redis_auto_failover.feature
331
332
  - features/step_definitions/redis_auto_failover_steps.rb
332
333
  - features/support/beetle_handler
@@ -340,12 +341,12 @@ files:
340
341
  - lib/beetle/base.rb
341
342
  - lib/beetle/client.rb
342
343
  - lib/beetle/configuration.rb
343
- - lib/beetle/dead_lettering.rb
344
344
  - lib/beetle/deduplication_store.rb
345
345
  - lib/beetle/handler.rb
346
346
  - lib/beetle/logging.rb
347
347
  - lib/beetle/message.rb
348
348
  - lib/beetle/publisher.rb
349
+ - lib/beetle/queue_properties.rb
349
350
  - lib/beetle/r_c.rb
350
351
  - lib/beetle/redis_ext.rb
351
352
  - lib/beetle/subscriber.rb
@@ -357,12 +358,12 @@ files:
357
358
  - test/beetle/beetle_test.rb
358
359
  - test/beetle/client_test.rb
359
360
  - test/beetle/configuration_test.rb
360
- - test/beetle/dead_lettering_test.rb
361
361
  - test/beetle/deduplication_store_test.rb
362
362
  - test/beetle/handler_test.rb
363
363
  - test/beetle/message/settings_test.rb
364
364
  - test/beetle/message_test.rb
365
365
  - test/beetle/publisher_test.rb
366
+ - test/beetle/queue_properties_test.rb
366
367
  - test/beetle/r_c_test.rb
367
368
  - test/beetle/redis_ext_test.rb
368
369
  - test/beetle/subscriber_test.rb
@@ -397,13 +398,13 @@ test_files:
397
398
  - test/beetle/client_test.rb
398
399
  - test/beetle/amqp_gem_behavior_test.rb
399
400
  - test/beetle/deduplication_store_test.rb
401
+ - test/beetle/queue_properties_test.rb
400
402
  - test/beetle/handler_test.rb
401
403
  - test/beetle/beetle_test.rb
402
404
  - test/beetle/configuration_test.rb
403
405
  - test/beetle/subscriber_test.rb
404
406
  - test/beetle/message/settings_test.rb
405
407
  - test/beetle/redis_ext_test.rb
406
- - test/beetle/dead_lettering_test.rb
407
408
  - test/beetle/message_test.rb
408
409
  - test/beetle/publisher_test.rb
409
410
  - test/beetle/r_c_test.rb
@@ -1,117 +0,0 @@
1
- require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
-
3
- module Beetle
4
- class SetDeadLetterPolicyTest < Minitest::Test
5
- def setup
6
- @server = "localhost:15672"
7
- @queue_name = "QUEUE_NAME"
8
- @config = Configuration.new
9
- @config.logger = Logger.new("/dev/null")
10
- @dead_lettering = DeadLettering.new(@config)
11
- end
12
-
13
- test "raises exception when queue name wasn't specified" do
14
- assert_raises ArgumentError do
15
- @dead_lettering.set_queue_policy!(@server, "")
16
- end
17
- end
18
-
19
- test "raises exception when no server was specified" do
20
- assert_raises ArgumentError do
21
- @dead_lettering.set_queue_policy!("", @queue_name)
22
- end
23
- end
24
-
25
- test "set_queue_policies! calls set_queue_policy for both target queue and dead letter queue" do
26
- options = {
27
- :server => "server", :lazy => true, :dead_lettering => true,
28
- :queue_name => "QUEUE_NAME", :dead_letter_queue_name => "QUEUE_NAME_dead_letter",
29
- :message_ttl => 10000
30
- }
31
- @dead_lettering.expects(:set_queue_policy!).with("server", "QUEUE_NAME",
32
- :lazy => true, :dead_lettering => true,
33
- :routing_key => "QUEUE_NAME_dead_letter")
34
- @dead_lettering.expects(:set_queue_policy!).with("server", "QUEUE_NAME_dead_letter",
35
- :lazy => true, :dead_lettering => true,
36
- :routing_key => "QUEUE_NAME",
37
- :message_ttl => 10000)
38
- @dead_lettering.set_queue_policies!(options)
39
- end
40
-
41
- test "creates a policy by posting to the rabbitmq if dead lettering is enabled" do
42
- stub_request(:put, "http://localhost:15672/api/policies/%2F/QUEUE_NAME_policy")
43
- .with(basic_auth: ['guest', 'guest'])
44
- .with(:body => {
45
- "pattern" => "^QUEUE_NAME$",
46
- "priority" => 1,
47
- "apply-to" => "queues",
48
- "definition" => {
49
- "dead-letter-routing-key" => "QUEUE_NAME_dead_letter",
50
- "dead-letter-exchange" => ""
51
- }}.to_json)
52
- .to_return(:status => 204)
53
-
54
- @dead_lettering.set_queue_policy!(@server, @queue_name, :lazy => false, :dead_lettering => true, :routing_key => "QUEUE_NAME_dead_letter")
55
- end
56
-
57
- test "creates a policy by posting to the rabbitmq if lazy queues are enabled" do
58
- stub_request(:put, "http://localhost:15672/api/policies/%2F/QUEUE_NAME_policy")
59
- .with(basic_auth: ['guest', 'guest'])
60
- .with(:body => {
61
- "pattern" => "^QUEUE_NAME$",
62
- "priority" => 1,
63
- "apply-to" => "queues",
64
- "definition" => {
65
- "queue-mode" => "lazy"
66
- }}.to_json)
67
- .to_return(:status => 204)
68
-
69
- @dead_lettering.set_queue_policy!(@server, @queue_name, :lazy => true, :dead_lettering => false)
70
- end
71
-
72
- test "raises exception when policy couldn't successfully be created" do
73
- stub_request(:put, "http://localhost:15672/api/policies/%2F/QUEUE_NAME_policy")
74
- .with(basic_auth: ['guest', 'guest'])
75
- .to_return(:status => [405])
76
-
77
- assert_raises DeadLettering::FailedRabbitRequest do
78
- @dead_lettering.set_queue_policy!(@server, @queue_name, :lazy => true, :dead_lettering => true)
79
- end
80
- end
81
-
82
- test "can optionally specify a message ttl" do
83
- stub_request(:put, "http://localhost:15672/api/policies/%2F/QUEUE_NAME_policy")
84
- .with(basic_auth: ['guest', 'guest'])
85
- .with(:body => {
86
- "pattern" => "^QUEUE_NAME$",
87
- "priority" => 1,
88
- "apply-to" => "queues",
89
- "definition" => {
90
- "dead-letter-routing-key" => "QUEUE_NAME_dead_letter",
91
- "dead-letter-exchange" => "",
92
- "message-ttl" => 10000
93
- }}.to_json)
94
- .to_return(:status => 204)
95
-
96
- @dead_lettering.set_queue_policy!(@server, @queue_name, :dead_lettering => true, :message_ttl => 10000, :routing_key => "QUEUE_NAME_dead_letter")
97
- end
98
-
99
- test "properly encodes the vhost from the configuration" do
100
- stub_request(:put, "http://localhost:15672/api/policies/foo%2F/QUEUE_NAME_policy")
101
- .with(basic_auth: ['guest', 'guest'])
102
- .with(:body => {
103
- "pattern" => "^QUEUE_NAME$",
104
- "priority" => 1,
105
- "apply-to" => "queues",
106
- "definition" => {
107
- "dead-letter-routing-key" => "QUEUE_NAME_dead_letter",
108
- "dead-letter-exchange" => ""
109
- }}.to_json)
110
- .to_return(:status => 204)
111
-
112
- @config.vhost = "foo/"
113
-
114
- @dead_lettering.set_queue_policy!(@server, @queue_name, :dead_lettering => true, :routing_key => "QUEUE_NAME_dead_letter")
115
- end
116
- end
117
- end