beetle 3.2.0 → 3.3.0.rc1

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: 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