beetle 0.3.14 → 0.4.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
  SHA1:
3
- metadata.gz: f9af32086ee6f83824426aadee6665688b19de6f
4
- data.tar.gz: f557cf6385121b1a6bcae4605d69b391d88e009b
3
+ metadata.gz: 4440f43f6c12064520bcf671c092da94206d83c4
4
+ data.tar.gz: 8f23d433a36db5942fe08b15a2024f8d979cc28b
5
5
  SHA512:
6
- metadata.gz: 7dae19283de63eed536e58ff2378f48b0ee151dad24502668b9e6f062a801090c926736c546cb5a3ff377568ceb2dae671418c79329c1fbdd441b7d0706848be
7
- data.tar.gz: c007937c6d910c75b16fad962e2cdf4bf8f11455ee5f68dd64004892740580fbc002fe4396aa4d4e3f1b103699d834de070b747ea3a4a467901224260fcb9f84
6
+ metadata.gz: 975fc5dbb57fdf533628a11ae7f77518945e548e7f4c041961663928efa438329d178b3fc7724af56f890542144578b4201318708352ab23418c44baa440e91b
7
+ data.tar.gz: 2b641c7cb51d17243454f5a52ace536825dda13f211f1e37660cb7d6f014d4cf6ba1e5d695de6ea126e2af795c78212d8afc3726c427d40361fcfc02553a3406
@@ -99,7 +99,8 @@ For tests, you'll need
99
99
 
100
100
  {Stefan Kaes}[http://github.com/skaes],
101
101
  {Pascal Friederich}[http://github.com/paukul],
102
- {Ali Jelveh}[http://github.com/dudemeister] and
102
+ {Ali Jelveh}[http://github.com/dudemeister],
103
+ {Bjoern Rochel}[http://github.com/bjro] and
103
104
  {Sebastian Roebke}[http://github.com/boosty].
104
105
 
105
106
  You can find out more about our work on our {dev blog}[http://devblog.xing.com].
@@ -1,5 +1,8 @@
1
1
  = Release Notes
2
2
 
3
+ == Version 0.4.0
4
+ * Added optional dead lettering feature to mimic RabbitMQ 2.x requeueing behaviour on RabbitMQ 3.x
5
+
3
6
  == Version 0.3.14
4
7
  * switched message id generation to use v4 uuids
5
8
 
@@ -32,4 +32,6 @@ Gem::Specification.new do |s|
32
32
  s.add_runtime_dependency("activesupport", [">= 2.3.4"])
33
33
  s.add_runtime_dependency("eventmachine_httpserver", [">= 0.2.1"])
34
34
  s.add_runtime_dependency("daemons", [">= 1.0.10"])
35
+
36
+ s.add_development_dependency("webmock", [">= 1.21.0"])
35
37
  end
@@ -15,8 +15,11 @@ Beetle.config.logger.level = Logger::INFO
15
15
 
16
16
  # setup client
17
17
  $client = Beetle::Client.new
18
- $client.register_queue(:test)
19
- $client.register_message(:test)
18
+ $client.config.dead_lettering_enabled = true
19
+ $client.configure(:key => "my.test.message") do
20
+ message(:test)
21
+ queue(:test)
22
+ end
20
23
 
21
24
  # purge the test queue
22
25
  $client.purge(:test)
@@ -35,6 +38,12 @@ class Handler < Beetle::Handler
35
38
 
36
39
  # called when the handler receives the message - fail everytime
37
40
  def process
41
+ logger.info "received message with routing key: #{message.routing_key}"
42
+ death = message.header.attributes[:headers]["x-death"]
43
+ if death
44
+ logger.info "X-DEATH: Message is coming back from dead letter queue (#{death.first["count"]})"
45
+ death.each {|d| logger.debug d}
46
+ end
38
47
  raise "failed #{$exceptions += 1} times"
39
48
  end
40
49
 
@@ -12,6 +12,7 @@ module Beetle
12
12
  @server = @servers[rand @servers.size]
13
13
  @exchanges = {}
14
14
  @queues = {}
15
+ @dead_lettering = DeadLettering.new(@client.config)
15
16
  end
16
17
 
17
18
  private
@@ -47,6 +47,31 @@ module Beetle
47
47
  attr_accessor :user
48
48
  # the password to use when connectiong to the AMQP servers (defaults to <tt>"guest"</tt>)
49
49
  attr_accessor :password
50
+ # the maximum permissible size of a frame (in bytes). Defaults to 128 KB
51
+ attr_accessor :frame_max
52
+
53
+ # In contrast to RabbitMQ 2.x, RabbitMQ 3.x preserves message order when requeing a message. This can lead to
54
+ # throughput degradation (when rejected messages block the processing of other messages
55
+ # at the head of the queue) in some cases.
56
+ #
57
+ # This setting enables the creation of dead letter queues that mimic the old beetle behaviour on RabbitMQ 3.x.
58
+ # Instead of rejecting messages with "requeue => true", beetle will setup dead letter queues for all queues, will
59
+ # reject messages with "requeue => false", where messages are temporarily moved to the side and are republished to
60
+ # the end of the original queue when they expire in the dead letter queue.
61
+ #
62
+ # By default this is turned off and needs to be explicitly enabled.
63
+ attr_accessor :dead_lettering_enabled
64
+ alias_method :dead_lettering_enabled?, :dead_lettering_enabled
65
+
66
+ # the time a message spends in the dead letter queue if dead lettering is enabled, before it is returned
67
+ # to the original queue
68
+ attr_accessor :dead_lettering_msg_ttl
69
+
70
+ # Read timeout for http requests to create dead letter bindings
71
+ attr_accessor :dead_lettering_read_timeout
72
+
73
+ # Returns the port on which the Rabbit API is hosted
74
+ attr_accessor :api_port
50
75
 
51
76
  # the socket timeout in seconds for message publishing (defaults to <tt>0</tt>).
52
77
  # consider this a highly experimental feature for now.
@@ -85,6 +110,12 @@ module Beetle
85
110
  self.vhost = "/"
86
111
  self.user = "guest"
87
112
  self.password = "guest"
113
+ self.api_port = 15672
114
+ self.frame_max = 131072
115
+
116
+ self.dead_lettering_enabled = false
117
+ self.dead_lettering_msg_ttl = 1000 #1 second
118
+ self.dead_lettering_read_timeout = 3 #3 seconds
88
119
 
89
120
  self.publishing_timeout = 0
90
121
  self.tmpdir = "/tmp"
@@ -0,0 +1,94 @@
1
+ require 'net/http'
2
+ require 'json'
3
+
4
+ module Beetle
5
+ class DeadLettering
6
+ class FailedRabbitRequest < StandardError; end
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ end
11
+
12
+ def bind_dead_letter_queues!(channel, servers, target_queue, creation_keys = {})
13
+ return unless @config.dead_lettering_enabled?
14
+
15
+ dead_letter_queue_name = dead_letter_queue_name(target_queue)
16
+
17
+ logger.debug("Beetle: creating dead letter queue #{dead_letter_queue_name} with opts: #{creation_keys.inspect}")
18
+ dead_letter_queue = channel.queue(dead_letter_queue_name, creation_keys)
19
+
20
+ logger.debug("Beetle: setting #{dead_letter_queue_name} as dead letter queue of #{target_queue} on all servers")
21
+ set_dead_letter_policies!(servers, target_queue)
22
+
23
+ logger.debug("Beetle: setting #{target_queue} as dead letter queue of #{dead_letter_queue_name} on all servers")
24
+ set_dead_letter_policies!(
25
+ servers,
26
+ dead_letter_queue_name,
27
+ :message_ttl => @config.dead_lettering_msg_ttl,
28
+ :routing_key => target_queue
29
+ )
30
+ end
31
+
32
+ def set_dead_letter_policies!(servers, queue_name, options={})
33
+ servers.each { |server| set_dead_letter_policy!(server, queue_name, options) }
34
+ end
35
+
36
+ def set_dead_letter_policy!(server, queue_name, options={})
37
+ raise ArgumentError.new("server missing") if server.blank?
38
+ raise ArgumentError.new("queue name missing") if queue_name.blank?
39
+
40
+ vhost = CGI.escape(@config.vhost)
41
+ request_url = URI("http://#{server}/api/policies/#{vhost}/#{queue_name}_policy")
42
+ request = Net::HTTP::Put.new(request_url)
43
+
44
+ request_body = {
45
+ "pattern" => "^#{queue_name}$",
46
+ "priority" => 1,
47
+ "apply-to" => "queues",
48
+ "definition" => {
49
+ "dead-letter-routing-key" => dead_letter_routing_key(queue_name, options),
50
+ "dead-letter-exchange" => ""
51
+ }
52
+ }
53
+
54
+ request_body["definition"].merge!("message-ttl" => options[:message_ttl]) if options[:message_ttl]
55
+
56
+ response = run_rabbit_http_request(request_url, request) do |http|
57
+ http.request(request, request_body.to_json)
58
+ end
59
+
60
+ if response.code != "204"
61
+ log_error("Failed to create policy for queue #{queue_name}", response)
62
+ raise FailedRabbitRequest.new("Could not create policy")
63
+ end
64
+
65
+ :ok
66
+ end
67
+
68
+ def dead_letter_routing_key(queue_name, options)
69
+ options.fetch(:routing_key) { dead_letter_queue_name(queue_name) }
70
+ end
71
+
72
+ def dead_letter_queue_name(queue_name)
73
+ "#{queue_name}_dead_letter"
74
+ end
75
+
76
+ def run_rabbit_http_request(uri, request, &block)
77
+ request.basic_auth(@config.user, @config.password)
78
+ request["Content-Type"] = "application/json"
79
+ response = Net::HTTP.start(uri.hostname, @config.api_port, :read_timeout => @config.dead_lettering_read_timeout) do |http|
80
+ block.call(http) if block_given?
81
+ end
82
+ end
83
+
84
+ def log_error(msg, response)
85
+ logger.error(msg)
86
+ logger.error("Response code was #{response.code}")
87
+ logger.error(response.body)
88
+ end
89
+
90
+ def logger
91
+ @config.logger
92
+ end
93
+ end
94
+ end
@@ -103,7 +103,11 @@ module Beetle
103
103
 
104
104
  # the routing key
105
105
  def routing_key
106
- header.routing_key
106
+ @routing_key ||= if x_death = header.attributes[:headers]["x-death"]
107
+ x_death.last["routing-keys"].first
108
+ else
109
+ header.routing_key
110
+ end
107
111
  end
108
112
  alias_method :key, :routing_key
109
113
 
@@ -149,9 +149,15 @@ module Beetle
149
149
  end
150
150
 
151
151
  def new_bunny
152
- b = Bunny.new(:host => current_host, :port => current_port, :logging => !!@options[:logging],
153
- :user => Beetle.config.user, :pass => Beetle.config.password, :vhost => Beetle.config.vhost,
154
- :socket_timeout => Beetle.config.publishing_timeout)
152
+ b = Bunny.new(
153
+ :host => current_host,
154
+ :port => current_port,
155
+ :logging => !!@options[:logging],
156
+ :user => @client.config.user,
157
+ :pass => @client.config.password,
158
+ :vhost => @client.config.vhost,
159
+ :frame_max => @client.config.frame_max,
160
+ :socket_timeout => @client.config.publishing_timeout)
155
161
  b.start
156
162
  b
157
163
  end
@@ -195,10 +201,11 @@ module Beetle
195
201
  @exchanges_with_bound_queues[exchange_name] = true
196
202
  end
197
203
 
198
- # TODO: Refactor, fethch the keys and stuff itself
204
+ # TODO: Refactor, fetch the keys and stuff itself
199
205
  def bind_queue!(queue_name, creation_keys, exchange_name, binding_keys)
200
206
  logger.debug("Beetle: creating queue with opts: #{creation_keys.inspect}")
201
207
  queue = bunny.queue(queue_name, creation_keys)
208
+ @dead_lettering.bind_dead_letter_queues!(bunny, @client.servers, queue_name, creation_keys)
202
209
  logger.debug("Beetle: binding queue #{queue_name} to #{exchange_name} with opts: #{binding_keys.inspect}")
203
210
  queue.bind(exchange(exchange_name), binding_keys)
204
211
  queue
@@ -144,8 +144,12 @@ module Beetle
144
144
  m = Message.new(amqp_queue_name, header, data, message_options)
145
145
  result = m.process(processor)
146
146
  if result.reject?
147
- sleep 1
148
- header.reject(:requeue => true)
147
+ if @client.config.dead_lettering_enabled?
148
+ header.reject(:requeue => false)
149
+ else
150
+ sleep 1
151
+ header.reject(:requeue => true)
152
+ end
149
153
  elsif reply_to = header.attributes[:reply_to]
150
154
  # logger.info "Beetle: sending reply to queue #{reply_to}"
151
155
  # require 'ruby-debug'
@@ -177,6 +181,8 @@ module Beetle
177
181
 
178
182
  def bind_queue!(queue_name, creation_keys, exchange_name, binding_keys)
179
183
  queue = channel.queue(queue_name, creation_keys)
184
+ target_servers = @client.servers + @client.additional_subscription_servers
185
+ @dead_lettering.bind_dead_letter_queues!(channel, target_servers, queue_name, creation_keys)
180
186
  exchange = exchange(exchange_name)
181
187
  queue.bind(exchange, binding_keys)
182
188
  queue
@@ -1,3 +1,3 @@
1
1
  module Beetle
2
- VERSION = "0.3.14"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -0,0 +1,130 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ module Beetle
4
+ class SetDeadLetteringsTest < MiniTest::Unit::TestCase
5
+ def setup
6
+ @dead_lettering = DeadLettering.new(Configuration.new)
7
+ end
8
+
9
+ test "creates a dead letter queue for each server" do
10
+ servers = %w(a b)
11
+
12
+ @dead_lettering.expects(:set_dead_letter_policy!).
13
+ with("a", "QUEUE_NAME", :message_ttl => 10000)
14
+ @dead_lettering.expects(:set_dead_letter_policy!).
15
+ with("b", "QUEUE_NAME", :message_ttl => 10000)
16
+
17
+ @dead_lettering.set_dead_letter_policies!(servers, "QUEUE_NAME", :message_ttl => 10000)
18
+ end
19
+ end
20
+
21
+ class SetDeadLetterPolicyTest < MiniTest::Unit::TestCase
22
+ def setup
23
+ @server = "localhost:15672"
24
+ @queue_name = "QUEUE_NAME"
25
+ @config = Configuration.new
26
+ @config.logger = Logger.new("/dev/null")
27
+ @dead_lettering = DeadLettering.new(@config)
28
+ end
29
+
30
+ test "raises exception when queue name wasn't specified" do
31
+ assert_raises ArgumentError do
32
+ @dead_lettering.set_dead_letter_policy!(@server, "")
33
+ end
34
+ end
35
+
36
+ test "raises exception when no server was specified" do
37
+ assert_raises ArgumentError do
38
+ @dead_lettering.set_dead_letter_policy!("", @queue_name)
39
+ end
40
+ end
41
+
42
+ test "creates a policy by posting to the rabbitmq" do
43
+ stub_request(:put, "http://guest:guest@localhost:15672/api/policies/%2F/QUEUE_NAME_policy").
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_dead_letter_policy!(@server, @queue_name)
55
+ end
56
+
57
+ test "raises exception when policy couldn't successfully be created" do
58
+ stub_request(:put, "http://guest:guest@localhost:15672/api/policies/%2F/QUEUE_NAME_policy").
59
+ to_return(:status => [405])
60
+
61
+ assert_raises DeadLettering::FailedRabbitRequest do
62
+ @dead_lettering.set_dead_letter_policy!(@server, @queue_name)
63
+ end
64
+ end
65
+
66
+ test "can optionally specify a message ttl" do
67
+ stub_request(:put, "http://guest:guest@localhost:15672/api/policies/%2F/QUEUE_NAME_policy").
68
+ with(:body => {
69
+ "pattern" => "^QUEUE_NAME$",
70
+ "priority" => 1,
71
+ "apply-to" => "queues",
72
+ "definition" => {
73
+ "dead-letter-routing-key" => "QUEUE_NAME_dead_letter",
74
+ "dead-letter-exchange" => "",
75
+ "message-ttl" => 10000
76
+ }}.to_json).
77
+ to_return(:status => 204)
78
+
79
+ @dead_lettering.set_dead_letter_policy!(@server, @queue_name, :message_ttl => 10000)
80
+ end
81
+
82
+ test "properly encodes the vhost from the configuration" do
83
+ stub_request(:put, "http://guest:guest@localhost:15672/api/policies/foo%2F/QUEUE_NAME_policy").
84
+ with(:body => {
85
+ "pattern" => "^QUEUE_NAME$",
86
+ "priority" => 1,
87
+ "apply-to" => "queues",
88
+ "definition" => {
89
+ "dead-letter-routing-key" => "QUEUE_NAME_dead_letter",
90
+ "dead-letter-exchange" => ""
91
+ }}.to_json).
92
+ to_return(:status => 204)
93
+
94
+ @config.vhost = "foo/"
95
+
96
+ @dead_lettering.set_dead_letter_policy!(@server, @queue_name)
97
+ end
98
+ end
99
+
100
+ class BindDeadLetterQueuesTest < MiniTest::Unit::TestCase
101
+ def setup
102
+ @queue_name = "QUEUE_NAME"
103
+ @config = Configuration.new
104
+ @config.logger = Logger.new("/dev/null")
105
+ @dead_lettering = DeadLettering.new(@config)
106
+ @servers = ["localhost:55672"]
107
+ end
108
+
109
+ test "is turned off by default" do
110
+ channel = stub('channel')
111
+ @dead_lettering.expects(:set_dead_letter_policies!).never
112
+ @dead_lettering.bind_dead_letter_queues!(channel, @servers, @queue_name)
113
+ end
114
+
115
+ test "creates and connects the dead letter queue via policies when enabled" do
116
+ @config.dead_lettering_enabled = true
117
+
118
+ channel = stub('channel')
119
+
120
+ channel.expects(:queue).with("#{@queue_name}_dead_letter", {})
121
+ @dead_lettering.expects(:set_dead_letter_policies!).with(@servers, @queue_name)
122
+ @dead_lettering.expects(:set_dead_letter_policies!).with(@servers, "#{@queue_name}_dead_letter",
123
+ :routing_key => @queue_name,
124
+ :message_ttl => 1000
125
+ )
126
+
127
+ @dead_lettering.bind_dead_letter_queues!(channel, @servers, @queue_name)
128
+ end
129
+ end
130
+ end
@@ -19,7 +19,12 @@ module Beetle
19
19
  m = mock("dummy")
20
20
  expected_bunny_options = {
21
21
  :host => @pub.send(:current_host), :port => @pub.send(:current_port),
22
- :logging => false, :user => "guest", :pass => "guest", :vhost => "/", :socket_timeout => 0
22
+ :logging => false,
23
+ :user => "guest",
24
+ :pass => "guest",
25
+ :vhost => "/",
26
+ :socket_timeout => 0,
27
+ :frame_max => 131072
23
28
  }
24
29
  Bunny.expects(:new).with(expected_bunny_options).returns(m)
25
30
  m.expects(:start)
@@ -230,7 +235,7 @@ module Beetle
230
235
  q.expects(:bind).with(:the_exchange, {:key => "haha.#"})
231
236
  m = mock("Bunny")
232
237
  m.expects(:queue).with("some_queue", :durable => true, :passive => false, :auto_delete => false, :exclusive => false, :arguments => {"foo" => "fighter"}).returns(q)
233
- @pub.expects(:bunny).returns(m)
238
+ @pub.expects(:bunny).returns(m).twice
234
239
 
235
240
  @pub.send(:queue, "some_queue")
236
241
  assert_equal q, @pub.send(:queues)["some_queue"]
@@ -147,7 +147,7 @@ module Beetle
147
147
  q.expects(:bind).with(:the_exchange, {:key => "haha.#"})
148
148
  m = mock("MQ")
149
149
  m.expects(:queue).with("some_queue", :durable => true, :passive => false, :auto_delete => false, :exclusive => false, :arguments => {"schmu" => 5}).returns(q)
150
- @sub.expects(:channel).returns(m)
150
+ @sub.expects(:channel).returns(m).twice
151
151
 
152
152
  @sub.send(:queue, "some_queue")
153
153
  assert_equal q, @sub.send(:queues)["some_queue"]
@@ -15,6 +15,8 @@ require File.expand_path(File.dirname(__FILE__) + '/../lib/beetle')
15
15
  class MiniTest::Unit::TestCase
16
16
  require "active_support/testing/declarative"
17
17
  extend ActiveSupport::Testing::Declarative
18
+ require "webmock"
19
+ include WebMock::API
18
20
  def assert_nothing_raised(*)
19
21
  yield
20
22
  end
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: 0.3.14
4
+ version: 0.4.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: 2015-05-19 00:00:00.000000000 Z
15
+ date: 2015-07-14 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: uuid4r
@@ -166,6 +166,20 @@ dependencies:
166
166
  - - ">="
167
167
  - !ruby/object:Gem::Version
168
168
  version: 1.0.10
169
+ - !ruby/object:Gem::Dependency
170
+ name: webmock
171
+ requirement: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: 1.21.0
176
+ type: :development
177
+ prerelease: false
178
+ version_requirements: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ version: 1.21.0
169
183
  description: A highly available, reliable messaging infrastructure
170
184
  email: opensource@xing.com
171
185
  executables:
@@ -215,6 +229,7 @@ files:
215
229
  - lib/beetle/commands/configuration_server.rb
216
230
  - lib/beetle/commands/garbage_collect_deduplication_store.rb
217
231
  - lib/beetle/configuration.rb
232
+ - lib/beetle/dead_lettering.rb
218
233
  - lib/beetle/deduplication_store.rb
219
234
  - lib/beetle/handler.rb
220
235
  - lib/beetle/logging.rb
@@ -236,6 +251,7 @@ files:
236
251
  - test/beetle/beetle_test.rb
237
252
  - test/beetle/client_test.rb
238
253
  - test/beetle/configuration_test.rb
254
+ - test/beetle/dead_lettering_test.rb
239
255
  - test/beetle/deduplication_store_test.rb
240
256
  - test/beetle/handler_test.rb
241
257
  - test/beetle/message_test.rb
@@ -278,6 +294,7 @@ test_files:
278
294
  - test/beetle/beetle_test.rb
279
295
  - test/beetle/client_test.rb
280
296
  - test/beetle/configuration_test.rb
297
+ - test/beetle/dead_lettering_test.rb
281
298
  - test/beetle/deduplication_store_test.rb
282
299
  - test/beetle/handler_test.rb
283
300
  - test/beetle/message_test.rb