beetle 0.3.14 → 0.4.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
  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