beetle 3.0.0.rc5 → 3.0.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.
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