beetle 3.2.0 → 3.3.0.rc1

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: cfe005056abcea7bb999f5c1ffbc3300882e6625633034e0cc4ad7e8fc2b4d18
4
- data.tar.gz: bc6a039a77d190c10878045420ce6fe57bb34d05cd343d71bbc8e9dc6832b74c
3
+ metadata.gz: df35415e0cf7a4d4915a56aa594fe4aa1e63be2d536eab5589a5b80c349fdb96
4
+ data.tar.gz: 6f621ab1501f71cdc7ee84d8989dd446ebd17676d53f9e23ba9d8278206908ed
5
5
  SHA512:
6
- metadata.gz: 284dc26f2ccdcf11df046f977e083ac35ff0f35d759da350508cdaffecdcf3b56579a06381818f613711b9b5b2c57ff7d044d106562b39530b983e4a4148268e
7
- data.tar.gz: 962ba73a9f2580748e53e846a36fefcbc1ee9a1054a88a8f60f3de77b3824ac547deaa2c69e94995155400e1aca1ce39cd06779efd6d8910420a99d7b682ec07
6
+ metadata.gz: 51e61e992d734f636b5a86b176ab1150530c8e7c18dbbaf8c4f8e685eaf48b2531133bb0770b9b8f50bea45e95588cad6b892866a6cd6644dc8d5a3e67e71b7d
7
+ data.tar.gz: 4ef44c5608cfe93adc0bf9fefea0cfdfda59f358323b5ed93158fb57e0122241c45a4deff76a8a098664a4e027ff1394e135541857a4682286ab135481c012a9
@@ -1,6 +1,14 @@
1
1
  = Release Notes
2
2
 
3
- == Version 3.1.1
3
+ == Version 3.3.0.rc1
4
+ * protect against duplicate handler execution by keeping the status of
5
+ handler exectution for both redundant and non-redundant messages in
6
+ the dedup store for a configurable time window. The config option
7
+ is named redis_status_key_expiry_interval. Note that this will
8
+ significantly increase the cpu load and memory usage of the Redis
9
+ server for applications using a large number of non-redundant messages.
10
+
11
+ == Version 3.2.0
4
12
  * added currently processed message to the handler pre-processing step
5
13
 
6
14
  == Version 3.1.0
data/Rakefile CHANGED
@@ -120,5 +120,5 @@ RDoc::Task.new do |rdoc|
120
120
  end
121
121
 
122
122
  task :clean do
123
- system('rm -f tmp/*.output tmp/*.log tmp/master/* tmp/slave/* tmp/*lock tmp/*pid test.log')
123
+ sh "rm -f tmp/*.output tmp/*.log tmp/master-dir/* tmp/slave-dir/* tmp/*lock tmp/*pid test.log"
124
124
  end
@@ -27,7 +27,7 @@ Feature: Redis auto failover
27
27
  Scenario: Successful single redis master switch with multiple failover sets
28
28
  Given a redis server "redis-3" exists as master
29
29
  And a redis server "redis-4" exists as slave of "redis-3"
30
- Given a redis configuration server using redis servers "a/redis-1,redis-2;b/redis-3,redis-4" with clients "rc-client-1,rc-client-2" exists
30
+ Given a redis configuration server using redis servers "system/redis-1,redis-2;system2/redis-3,redis-4" with clients "rc-client-1,rc-client-2" exists
31
31
  And a redis configuration client "rc-client-1" using redis servers "redis-1,redis-2" exists
32
32
  And a redis configuration client "rc-client-2" using redis servers "redis-1,redis-2" exists
33
33
  And a beetle handler using the redis-master file from "rc-client-1" exists
@@ -37,10 +37,10 @@ Feature: Redis auto failover
37
37
  And the role of redis server "redis-2" should be "master"
38
38
  And the redis master file of the redis configuration server should contain "redis-2"
39
39
  And the redis master file of the redis configuration server should contain "redis-3"
40
- And the redis master of "rc-client-1" in system "a" should be "redis-2"
41
- And the redis master of "rc-client-2" in system "a" should be "redis-2"
42
- And the redis master of "rc-client-1" in system "b" should be "redis-3"
43
- And the redis master of "rc-client-2" in system "b" should be "redis-3"
40
+ And the redis master of "rc-client-1" in system "system" should be "redis-2"
41
+ And the redis master of "rc-client-2" in system "system" should be "redis-2"
42
+ And the redis master of "rc-client-1" in system "system2" should be "redis-3"
43
+ And the redis master of "rc-client-2" in system "system2" should be "redis-3"
44
44
  And the redis master of the beetle handler should be "redis-2"
45
45
  And a system notification for switching from "redis-1" to "redis-2" should be sent
46
46
  Given a redis server "redis-1" exists as master
@@ -49,7 +49,7 @@ Feature: Redis auto failover
49
49
  Scenario: Successful double redis master switch with multiple failover sets
50
50
  Given a redis server "redis-3" exists as master
51
51
  And a redis server "redis-4" exists as slave of "redis-3"
52
- Given a redis configuration server using redis servers "a/redis-1,redis-2;b/redis-3,redis-4" with clients "rc-client-1,rc-client-2" exists
52
+ Given a redis configuration server using redis servers "system/redis-1,redis-2;system2/redis-3,redis-4" with clients "rc-client-1,rc-client-2" exists
53
53
  And a redis configuration client "rc-client-1" using redis servers "redis-1,redis-2" exists
54
54
  And a redis configuration client "rc-client-2" using redis servers "redis-1,redis-2" exists
55
55
  And a beetle handler using the redis-master file from "rc-client-1" exists
@@ -60,10 +60,10 @@ Feature: Redis auto failover
60
60
  And the role of redis server "redis-4" should be "master"
61
61
  And the redis master file of the redis configuration server should contain "redis-2"
62
62
  And the redis master file of the redis configuration server should contain "redis-4"
63
- And the redis master of "rc-client-1" in system "a" should be "redis-2"
64
- And the redis master of "rc-client-2" in system "a" should be "redis-2"
65
- And the redis master of "rc-client-1" in system "b" should be "redis-4"
66
- And the redis master of "rc-client-2" in system "b" should be "redis-4"
63
+ And the redis master of "rc-client-1" in system "system" should be "redis-2"
64
+ And the redis master of "rc-client-2" in system "system" should be "redis-2"
65
+ And the redis master of "rc-client-1" in system "system2" should be "redis-4"
66
+ And the redis master of "rc-client-2" in system "system2" should be "redis-4"
67
67
  And the redis master of the beetle handler should be "redis-2"
68
68
  Given a redis server "redis-1" exists as master
69
69
  Then the role of redis server "redis-1" should be "slave"
@@ -36,7 +36,7 @@ Daemons.run_proc("beetle_handler", :log_output => true, :dir_mode => :normal, :d
36
36
  config.handler(Beetle.config.beetle_policy_updates_queue_name) do |message|
37
37
  begin
38
38
  Beetle.config.logger.info "received beetle policy update message': #{message.data}"
39
- client.set_queue_policies!(JSON.parse(message.data))
39
+ client.update_queue_properties!(JSON.parse(message.data))
40
40
  rescue => e
41
41
  Beetle.config.logger.error("#{e}:#{e.backtrace.join("\n")}")
42
42
  end
@@ -16,6 +16,8 @@ module Beetle
16
16
  # default logger (defaults to <tt>Logger.new(log_file)</tt>)
17
17
  attr_accessor :logger
18
18
  # defaults to <tt>STDOUT</tt>
19
+ attr_accessor :redis_logger
20
+ # set this to a logger instance if you want redis operations to be logged. defaults to <tt>nil</tt>.
19
21
  attr_accessor :log_file
20
22
  # number of seconds after which keys are removed from the message deduplication store (defaults to <tt>1.hour</tt>)
21
23
  attr_accessor :gc_threshold
@@ -35,6 +37,15 @@ module Beetle
35
37
  # handler timeout.
36
38
  attr_accessor :redis_failover_timeout
37
39
 
40
+ # how long we want status keys to survive after we have seen the second message of a
41
+ # redundant message pair. Defaults to 5 minutes. Setting this to a high value (hours)
42
+ # will reduce the likelihood of executing handler logic more than once, but can cause
43
+ # a higher redis database size with all associated problems. A handler can be
44
+ # executed more than once if the ack to the RabbitMQ server gets lost between the consumer
45
+ # and the RabbitMQ instance. This happens extremely seldom in our environment, but we cannot
46
+ # rule it out and we also don't quite understand (yet) why it happens.
47
+ attr_accessor :redis_status_key_expiry_interval
48
+
38
49
  # how often heartbeat messages are exchanged between failover
39
50
  # daemons. defaults to 10 seconds.
40
51
  attr_accessor :redis_failover_client_heartbeat_interval
@@ -140,6 +151,7 @@ module Beetle
140
151
  self.redis_servers = ""
141
152
  self.redis_db = 4
142
153
  self.redis_failover_timeout = 180.seconds
154
+ self.redis_status_key_expiry_interval = 5.minutes
143
155
  self.redis_failover_client_heartbeat_interval = 10.seconds
144
156
  self.redis_failover_client_dead_interval = 60.seconds
145
157
 
@@ -30,7 +30,8 @@ module Beetle
30
30
  redis
31
31
  end
32
32
 
33
- # list of key suffixes to use for storing values in Redis.
33
+ # list of key suffixes to use for storing values in Redis. 'status'
34
+ # always needs to be the first element of the array.
34
35
  KEY_SUFFIXES = [:status, :ack_count, :timeout, :delay, :attempts, :exceptions, :mutex, :expires]
35
36
 
36
37
  # build a Redis key out of a message id and a given suffix
@@ -58,6 +59,19 @@ module Beetle
58
59
  with_failover { redis.setnx(key(msg_id, suffix), value) }
59
60
  end
60
61
 
62
+ # store completion status for given <tt>msg_id</tt> if it doesn't exist yet. Returns whether the
63
+ # operation was successful.
64
+ def setnx_completed!(msg_id)
65
+ with_failover do
66
+ redis.set(
67
+ key(msg_id, :status),
68
+ "completed",
69
+ :nx => true,
70
+ :ex => @config.redis_status_key_expiry_interval,
71
+ )
72
+ end
73
+ end
74
+
61
75
  # store some key/value pairs
62
76
  def mset(msg_id, values)
63
77
  values = values.inject([]){|a,(k,v)| a.concat([key(msg_id, k), v])}
@@ -93,7 +107,12 @@ module Beetle
93
107
 
94
108
  # delete all keys associated with the given <tt>msg_id</tt>.
95
109
  def del_keys(msg_id)
96
- with_failover { redis.del(*keys(msg_id)) }
110
+ keys = keys(msg_id)
111
+ status_key = keys.shift
112
+ with_failover do
113
+ redis.del(*keys)
114
+ redis.expire(status_key, @config.redis_status_key_expiry_interval)
115
+ end
97
116
  end
98
117
 
99
118
  # check whether key with given suffix exists for a given <tt>msg_id</tt>.
@@ -128,7 +147,7 @@ module Beetle
128
147
 
129
148
  # set current redis master instance (as specified in the Beetle::Configuration)
130
149
  def redis_master_from_server_string
131
- @current_master ||= Redis.from_server_string(@config.redis_server, :db => @config.redis_db)
150
+ @current_master ||= Redis.from_server_string(@config.redis_server, :db => @config.redis_db, :logger => @config.redis_logger)
132
151
  end
133
152
 
134
153
  # set current redis master from master file
@@ -146,7 +165,7 @@ module Beetle
146
165
  def set_current_redis_master_from_master_file
147
166
  @last_time_master_file_changed = File.mtime(@config.redis_server)
148
167
  server_string = extract_redis_master(read_master_file)
149
- @current_master = !server_string.blank? ? Redis.from_server_string(server_string, :db => @config.redis_db) : nil
168
+ @current_master = !server_string.blank? ? Redis.from_server_string(server_string, :db => @config.redis_db, :logger => @config.redis_logger) : nil
150
169
  end
151
170
 
152
171
  # extract redis master from file content and return the server for our system
@@ -265,7 +265,7 @@ module Beetle
265
265
  Beetle::reraise_expectation_errors!
266
266
  logger.error "Beetle: preprocessing error #{@pre_exception.class}(#{@pre_exception}) for #{msg_id}"
267
267
  end
268
- logger.debug "Beetle: processing message #{msg_id}(#{timestamp})"
268
+ logger.debug "Beetle: processing message #{msg_id}(#{timestamp}) redelivered: #{header.redelivered?}"
269
269
  begin
270
270
  result = process_internal(handler)
271
271
  handler.process_exception(@exception || @pre_exception) if (@exception || @pre_exception)
@@ -294,7 +294,11 @@ module Beetle
294
294
  RC::Ancient
295
295
  elsif simple?
296
296
  ack!
297
- run_handler(handler) == RC::HandlerCrash ? RC::AttemptsLimitReached : RC::OK
297
+ if @store.setnx_completed!(msg_id)
298
+ run_handler(handler) == RC::HandlerCrash ? RC::AttemptsLimitReached : RC::OK
299
+ else
300
+ RC::OK
301
+ end
298
302
  elsif !key_exists?
299
303
  run_handler!(handler)
300
304
  else
@@ -383,7 +387,7 @@ module Beetle
383
387
  #:doc:
384
388
  logger.debug "Beetle: ack! for message #{msg_id}"
385
389
  header.ack
386
- return if simple? # simple messages don't use the deduplication store
390
+ return if simple?
387
391
  if !redundant? || @store.incr(msg_id, :ack_count) >= 2
388
392
  # we test for >= 2 here, because we retry increments in the
389
393
  # dedup store so the counter could be greater than two
@@ -1,3 +1,3 @@
1
1
  module Beetle
2
- VERSION = "3.2.0"
2
+ VERSION = "3.3.0.rc1"
3
3
  end
@@ -115,7 +115,8 @@ module Beetle
115
115
 
116
116
  class KeyManagementTest < Minitest::Test
117
117
  def setup
118
- @store = DeduplicationStore.new
118
+ @config = Configuration.new
119
+ @store = DeduplicationStore.new(@config)
119
120
  @store.flushdb
120
121
  @null_handler = Handler.create(lambda {|*args|})
121
122
  end
@@ -128,33 +129,42 @@ module Beetle
128
129
  end
129
130
  end
130
131
 
131
- test "successful processing of a non redundant message should delete all keys from the database" do
132
+ test "successful processing of a non redundant message should delete all keys from the database (except the status key, which should be set to expire)" do
132
133
  header = header_with_params({})
133
134
  header.expects(:ack)
134
135
  message = Message.new("somequeue", header, 'foo', :store => @store)
136
+ message.stubs(:simple?).returns(false)
135
137
 
136
138
  assert !message.expired?
137
139
  assert !message.redundant?
138
140
 
139
141
  message.process(@null_handler)
140
-
141
- @store.keys(message.msg_id).each do |key|
142
+ keys = @store.keys(message.msg_id)
143
+ status_key = keys.shift
144
+ assert @store.redis.exists(status_key)
145
+ assert @store.redis.ttl(status_key) <= @config.redis_status_key_expiry_interval
146
+ keys.each do |key|
142
147
  assert !@store.redis.exists(key)
143
148
  end
144
149
  end
145
150
 
146
- test "succesful processing of a redundant message twice should delete all keys from the database" do
151
+ test "successful processing of a redundant message twice should delete all keys from the database (except the status key, which should be set to expire)" do
147
152
  header = header_with_params({:redundant => true})
148
153
  header.expects(:ack).twice
149
154
  message = Message.new("somequeue", header, 'foo', :store => @store)
150
155
 
151
156
  assert !message.expired?
152
157
  assert message.redundant?
158
+ assert !message.simple?
153
159
 
154
160
  message.process(@null_handler)
155
161
  message.process(@null_handler)
156
162
 
157
- @store.keys(message.msg_id).each do |key|
163
+ keys = @store.keys(message.msg_id)
164
+ status_key = keys.shift
165
+ assert @store.redis.exists(status_key)
166
+ assert @store.redis.ttl(status_key) <= @config.redis_status_key_expiry_interval
167
+ keys.each do |key|
158
168
  assert !@store.redis.exists(key)
159
169
  end
160
170
  end
@@ -294,10 +304,9 @@ module Beetle
294
304
  def setup
295
305
  @store = DeduplicationStore.new
296
306
  @store.flushdb
297
- @store.expects(:redis).never
298
307
  end
299
308
 
300
- test "when processing a simple message, ack should precede calling the handler" do
309
+ test "when processing a simple message, ack should follow calling the handler" do
301
310
  header = header_with_params({})
302
311
  message = Message.new("somequeue", header, 'foo', :attempts => 1, :store => @store)
303
312
 
@@ -35,6 +35,7 @@ def header_with_params(opts = {})
35
35
  beetle_headers = Beetle::Message.publishing_options(opts)
36
36
  header = mock("header")
37
37
  header.stubs(:attributes).returns(beetle_headers)
38
+ header.stubs(:redelivered?).returns(false)
38
39
  header
39
40
  end
40
41
 
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.2.0
4
+ version: 3.3.0.rc1
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-07-09 00:00:00.000000000 Z
15
+ date: 2019-07-17 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: bunny