eventq 4.0.0 → 4.1.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
  SHA256:
3
- metadata.gz: 6b478bb783830a9a72ec5aa1790edcce6e21dd1ca336ce0610841bdfba96072b
4
- data.tar.gz: b75542f44bb89e77b9b4bd3175b872508c707efa17a30a288fea4e83cdea5b25
3
+ metadata.gz: 830a90e777c5896d7d7d7f7d02bf400ec53b2e46d37235956f489719742724f1
4
+ data.tar.gz: 9d874da06b6190367a382ddac2cfc0dafe937bb622136474bf2430d32805af34
5
5
  SHA512:
6
- metadata.gz: 8b823fbb7482330f56a50721a375ea8c6a92feaaff45dd1540454c02b0cc981aa4ecf78c76b219816498c58cb5e4df80432a03c955397dd1b461824e09d1d789
7
- data.tar.gz: b335b2ecd2c8c1ce7758890816b5f9947c8d32dabd25a62e7e395e1f79c19cf3dce34aac5196117623833695e813650148745d01557466fbb731103abde31b84
6
+ metadata.gz: 5ef60aecf44e22fe3ed07efd0e7d86205e3191c75c020e9a722715334598d284445fee1548cab1873868128f28556bc30fe9f5810628cb388d17add6c20bc4d9
7
+ data.tar.gz: 2157ca99b534f9d34156f444da0343814bc7f415649b7aeda6ae9d6c4e97d3b2ce131549bff9ebf57973a6d859fa95d03b89c2dbbd22d18dc3c4c84c04c8a370
data/README.md CHANGED
@@ -54,6 +54,7 @@ A subscription queue should be defined to receive any events raised for the subs
54
54
 
55
55
  - **allow_retry** [Bool] [Optional] [Default=false] This determines if the queue should allow processing failures to be retried.
56
56
  - **allow_retry_back_off** [Bool] [Optional] [Default=false] This is used to specify if failed messages that retry should incrementally backoff.
57
+ - **allow_exponential_back_off** [Bool] [Optional] [Default=false] This is used to specify if failed messages that retry should expontentially backoff.
57
58
  - **retry_back_off_grace** [Int] [Optional] [Default=0] This is the number of times to allow retries without applying retry back off if enabled.
58
59
  - **dlq** [EventQ::Queue] [Optional] [Default=nil] A queue that will receive the messages which were not successfully processed after maximum number of receives by consumers. This is created at the same time as the parent queue.
59
60
  - **max_retry_attempts** [Int] [Optional] [Default=5] This is used to specify the max number of times an event should be allowed to retry before failing.
@@ -63,6 +64,7 @@ A subscription queue should be defined to receive any events raised for the subs
63
64
  - **require_signature** [Bool] [Optional] [Default=false] This is used to specify if messages within this queue must be signed.
64
65
  - **retry_delay** [Int] [Optional] [Default=30000] This is used to specify the time delay in milliseconds before a failed message is re-added to the subscription queue.
65
66
  - **retry_back_off_weight** [Int] [Optional] [Default=1] Additional multiplier for the timeout backoff. Normally used when `retry_delay` is too small (eg: 30ms) in order to get meaningful backoff values.
67
+ - **retry_jitter_ratio** [Int] [Optional] [Default=0] Amount of randomness for retry delays in percent to avoid a bulk of retries hitting again at the same time. 0% means no randomness, while 100% means full randomness. With full randomness, a random number between 0 and the calculated retry delay will be chosen for the delay.
66
68
 
67
69
  **Example**
68
70
 
@@ -72,7 +74,7 @@ class DataChangeAddressQueue < Queue
72
74
  def initialize
73
75
  @name = 'Data.Change.Address'
74
76
  @allow_retry = true
75
- @retry_delay = 20000
77
+ @retry_delay = 20_000
76
78
  @max_retry_attempts = 3
77
79
  end
78
80
  end
@@ -109,10 +111,10 @@ This method is called to unsubscribe a queue.
109
111
 
110
112
  **Example**
111
113
 
112
- #create an instant of the queue definition
114
+ #create an instance of the queue definition
113
115
  queue = DateChangeAddressQueue.new
114
116
 
115
- #subscribe the queue definition to an event type
117
+ #unsubscribe the queue definition
116
118
  subscription_manager.unsubscribe(queue)
117
119
 
118
120
 
@@ -330,8 +332,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
330
332
 
331
333
  Run the setup script of eventq to build the environment. This will create the `eventq` image.
332
334
 
333
- $ cd script
334
- $ ./setup.sh
335
+ $ ./script/setup.sh
335
336
 
336
337
  ### Running the tests
337
338
 
@@ -345,13 +346,11 @@ You will also need to comment out the AWS_* environment variables in the `docker
345
346
 
346
347
  Run the whole test suite:
347
348
 
348
- $ cd script
349
- $ ./test.sh
349
+ $ ./script/test.sh
350
350
 
351
351
  You can run the specs that don't depend on an AWS account with:
352
352
 
353
- $ cd script
354
- $ ./test.sh --tag ~integration
353
+ $ ./script/test.sh --tag ~integration
355
354
 
356
355
  ## Contributing
357
356
 
@@ -5,7 +5,7 @@ module EventQ
5
5
  # Class responsible to know how to calculate message Visibility Timeout for Amazon SQS
6
6
  class CalculateVisibilityTimeout
7
7
  def initialize(max_timeout:, logger: EventQ.logger)
8
- @max_timeout = max_timeout
8
+ @max_timeout = seconds_to_ms(max_timeout)
9
9
  @logger = logger
10
10
  end
11
11
 
@@ -14,28 +14,33 @@ module EventQ
14
14
  # @param retry_attempts [Integer] Current retry
15
15
  # @param queue_settings [Hash] Queue settings
16
16
  # @option allow_retry_back_off [Bool] Enables/Disables backoff strategy
17
+ # @option allow_exponential_back_off [Bool] Enables/Disables exponential backoff strategy
17
18
  # @option max_retry_delay [Integer] Maximum amount of time a retry will take in ms
18
19
  # @option retry_back_off_grace [Integer] Amount of retries to wait before starting to backoff
19
20
  # @option retry_back_off_weight [Integer] Multiplier for the backoff retry
21
+ # @option retry_jitter_ratio [Integer] Ratio of how much jitter to apply to the backoff retry
20
22
  # @option retry_delay [Integer] Amount of time to wait until retry in ms
21
23
  # @return [Integer] the calculated visibility timeout in seconds
22
24
  def call(retry_attempts:, queue_settings:)
23
- @retry_attempts = retry_attempts
25
+ @retry_attempts = retry_attempts
24
26
 
25
- @allow_retry_back_off = queue_settings.fetch(:allow_retry_back_off)
26
- @max_retry_delay = queue_settings.fetch(:max_retry_delay)
27
- @retry_back_off_grace = queue_settings.fetch(:retry_back_off_grace)
28
- @retry_back_off_weight= queue_settings.fetch(:retry_back_off_weight)
29
- @retry_delay = queue_settings.fetch(:retry_delay)
27
+ @allow_retry_back_off = queue_settings.fetch(:allow_retry_back_off)
28
+ @allow_exponential_back_off = queue_settings.fetch(:allow_exponential_back_off)
29
+ @max_retry_delay = queue_settings.fetch(:max_retry_delay)
30
+ @retry_back_off_grace = queue_settings.fetch(:retry_back_off_grace)
31
+ @retry_back_off_weight = queue_settings.fetch(:retry_back_off_weight)
32
+ @retry_jitter_ratio = queue_settings.fetch(:retry_jitter_ratio)
33
+ @retry_delay = queue_settings.fetch(:retry_delay)
30
34
 
31
- if @allow_retry_back_off && retry_past_grace_period?
32
- visibility_timeout = timeout_with_back_off
33
- visibility_timeout = check_for_max_timeout(visibility_timeout)
35
+ visibility_timeout = if @allow_retry_back_off && retry_past_grace_period?
36
+ timeout_with_back_off
34
37
  else
35
- visibility_timeout = timeout_without_back_off
38
+ timeout_without_back_off
36
39
  end
37
40
 
38
- visibility_timeout
41
+ visibility_timeout = apply_jitter(visibility_timeout) if @retry_jitter_ratio > 0
42
+
43
+ ms_to_seconds(visibility_timeout)
39
44
  end
40
45
 
41
46
  private
@@ -47,34 +52,57 @@ module EventQ
47
52
  end
48
53
 
49
54
  def timeout_without_back_off
50
- ms_to_seconds(@retry_delay)
55
+ @retry_delay
51
56
  end
52
57
 
53
58
  def timeout_with_back_off
54
59
  factor = @retry_attempts - @retry_back_off_grace
60
+ weighted_retry_delay = @retry_delay * @retry_back_off_weight
55
61
 
56
- visibility_timeout = ms_to_seconds(@retry_delay * factor * @retry_back_off_weight)
57
- max_retry_delay = ms_to_seconds(@max_retry_delay)
58
-
59
- if visibility_timeout > max_retry_delay
60
- logger.debug { "[#{self.class}] - Max message back off retry delay reached: #{max_retry_delay}" }
61
- visibility_timeout = max_retry_delay
62
+ visibility_timeout = if @allow_exponential_back_off
63
+ weighted_retry_delay * 2 ** (factor - 1)
64
+ else
65
+ weighted_retry_delay * factor
62
66
  end
63
67
 
64
- visibility_timeout
68
+ visibility_timeout = check_for_max_retry_delay(visibility_timeout)
69
+ check_for_max_timeout(visibility_timeout)
70
+ end
71
+
72
+ def apply_jitter(visibility_timeout)
73
+ ratio = @retry_jitter_ratio / 100.0
74
+ min_visibility_timeout = (visibility_timeout * (1 - ratio)).to_i
75
+ rand(min_visibility_timeout..visibility_timeout)
65
76
  end
66
77
 
67
78
  def ms_to_seconds(value)
68
79
  value / 1000
69
80
  end
70
81
 
82
+ def seconds_to_ms(value)
83
+ value * 1000
84
+ end
85
+
86
+ def check_for_max_retry_delay(visibility_timeout)
87
+ return visibility_timeout if visibility_timeout <= @max_retry_delay
88
+
89
+ logger.debug do
90
+ "[#{self.class}] - Max message back off retry delay reached: #{ms_to_seconds(@max_retry_delay)}"
91
+ end
92
+
93
+ @max_retry_delay
94
+ end
95
+
71
96
  def check_for_max_timeout(visibility_timeout)
72
- if visibility_timeout > @max_timeout
73
- logger.debug { "[#{self.class}] - AWS max visibility timeout of 12 hours has been exceeded. Setting message retry delay to 12 hours." }
74
- visibility_timeout = @max_timeout
97
+ return visibility_timeout if visibility_timeout <= @max_timeout
98
+
99
+ logger.debug do
100
+ "[#{self.class}] - AWS max visibility timeout of 12 hours has been exceeded. "\
101
+ "Setting message retry delay to 12 hours."
75
102
  end
76
- visibility_timeout
103
+
104
+ @max_timeout
77
105
  end
78
106
  end
79
107
  end
80
- end
108
+ end
@@ -119,16 +119,7 @@ module EventQ
119
119
 
120
120
  EventQ.logger.warn("[#{self.class}] - Message Id: #{args.id}. Rejected requesting retry. Attempts: #{retry_attempts}")
121
121
 
122
- visibility_timeout = @calculate_visibility_timeout.call(
123
- retry_attempts: retry_attempts,
124
- queue_settings: {
125
- allow_retry_back_off: queue.allow_retry_back_off,
126
- max_retry_delay: queue.max_retry_delay,
127
- retry_back_off_grace: queue.retry_back_off_grace,
128
- retry_back_off_weight: queue.retry_back_off_weight,
129
- retry_delay: queue.retry_delay
130
- }
131
- )
122
+ visibility_timeout = calculate_visibility_timeout(retry_attempts, queue)
132
123
 
133
124
  EventQ.logger.debug { "[#{self.class}] - Sending message for retry. Message TTL: #{visibility_timeout}" }
134
125
  poller.change_message_visibility_timeout(msg, visibility_timeout)
@@ -136,6 +127,21 @@ module EventQ
136
127
  context.call_on_retry_block(message)
137
128
  end
138
129
  end
130
+
131
+ def calculate_visibility_timeout(retry_attempts, queue)
132
+ @calculate_visibility_timeout.call(
133
+ retry_attempts: retry_attempts,
134
+ queue_settings: {
135
+ allow_retry_back_off: queue.allow_retry_back_off,
136
+ allow_exponential_back_off: queue.allow_exponential_back_off,
137
+ max_retry_delay: queue.max_retry_delay,
138
+ retry_back_off_grace: queue.retry_back_off_grace,
139
+ retry_back_off_weight: queue.retry_back_off_weight,
140
+ retry_jitter_ratio: queue.retry_jitter_ratio,
141
+ retry_delay: queue.retry_delay
142
+ }
143
+ )
144
+ end
139
145
  end
140
146
  end
141
147
  end
@@ -2,6 +2,7 @@ module EventQ
2
2
  class Queue
3
3
  attr_accessor :allow_retry
4
4
  attr_accessor :allow_retry_back_off
5
+ attr_accessor :allow_exponential_back_off
5
6
  attr_accessor :dlq
6
7
  attr_accessor :max_retry_attempts
7
8
  attr_accessor :max_retry_delay
@@ -11,6 +12,7 @@ module EventQ
11
12
  attr_accessor :retry_delay
12
13
  attr_accessor :retry_back_off_grace
13
14
  attr_accessor :retry_back_off_weight
15
+ attr_accessor :retry_jitter_ratio
14
16
  # Character delimiter between namespace and queue name. Default = '-'
15
17
  attr_accessor :namespace_delimiter
16
18
  # Flag to control that the queue runs in isolation of auto creating the topic it belongs to
@@ -20,6 +22,8 @@ module EventQ
20
22
  @allow_retry = false
21
23
  # Default retry back off settings
22
24
  @allow_retry_back_off = false
25
+ # Default exponential back off settings
26
+ @allow_exponential_back_off = false
23
27
  # Default max receive count is 30
24
28
  @max_receive_count = 30
25
29
  # Default max retry attempts is 5
@@ -34,6 +38,8 @@ module EventQ
34
38
  @retry_back_off_grace = 0
35
39
  # Multiplier for the backoff retry in case retry_delay is too small
36
40
  @retry_back_off_weight = 1
41
+ # Ratio of how much jitter to apply to the retry delay
42
+ @retry_jitter_ratio = 0
37
43
  @isolated = false
38
44
  end
39
45
  end
@@ -78,16 +78,7 @@ module EventQ
78
78
  message.retry_attempts += 1
79
79
  retry_attempts = message.retry_attempts - queue.retry_back_off_grace
80
80
  retry_attempts = 1 if retry_attempts < 1
81
-
82
- if queue.allow_retry_back_off == true
83
- message_ttl = retry_attempts * queue.retry_delay
84
- if (retry_attempts * queue.retry_delay) > queue.max_retry_delay
85
- EventQ.logger.debug { "[#{self.class}] - Max message back off retry delay reached." }
86
- message_ttl = queue.max_retry_delay
87
- end
88
- else
89
- message_ttl = queue.retry_delay
90
- end
81
+ message_ttl = retry_delay(queue, retry_attempts)
91
82
 
92
83
  EventQ.logger.debug { "[#{self.class}] - Sending message for retry. Message TTL: #{message_ttl}" }
93
84
  retry_exchange.publish(serialize_message(message), :expiration => message_ttl)
@@ -128,6 +119,31 @@ module EventQ
128
119
  raise "Unrecognized status: #{status}"
129
120
  end
130
121
  end
122
+
123
+ def retry_delay(queue, retry_attempts)
124
+ return queue.retry_delay unless queue.allow_retry_back_off
125
+
126
+ message_ttl = if queue.allow_exponential_back_off
127
+ queue.retry_delay * 2 ** (retry_attempts - 1)
128
+ else
129
+ queue.retry_delay * retry_attempts
130
+ end
131
+
132
+ if message_ttl > queue.max_retry_delay
133
+ EventQ.logger.debug { "[#{self.class}] - Max message back off retry delay reached." }
134
+ message_ttl = queue.max_retry_delay
135
+ end
136
+
137
+ apply_jitter(queue.retry_jitter_ratio, message_ttl)
138
+ end
139
+
140
+ def apply_jitter(retry_jitter_ratio, message_ttl)
141
+ return message_ttl if retry_jitter_ratio == 0
142
+
143
+ ratio = retry_jitter_ratio / 100.0
144
+ min_message_ttl = (message_ttl * (1 - ratio)).to_i
145
+ rand(min_message_ttl..message_ttl)
146
+ end
131
147
  end
132
148
  end
133
149
  end
data/lib/eventq.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'securerandom'
2
4
  require 'redlock'
3
5
  require 'class_kit'
@@ -22,6 +24,3 @@ require_relative 'eventq/eventq_base/nonce_manager'
22
24
  require_relative 'eventq/eventq_base/signature_providers'
23
25
  require_relative 'eventq/eventq_base/exceptions'
24
26
  require_relative 'eventq/queue_worker'
25
-
26
-
27
-
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eventq
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SageOne
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-08 00:00:00.000000000 Z
11
+ date: 2024-01-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '4'
19
+ version: '6'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '4'
26
+ version: '6'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '10.0'
75
+ version: '13'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '10.0'
82
+ version: '13'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: rspec
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -123,33 +123,47 @@ dependencies:
123
123
  - !ruby/object:Gem::Version
124
124
  version: 0.18.0
125
125
  - !ruby/object:Gem::Dependency
126
- name: aws-sdk-sqs
126
+ name: aws-sdk-core
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - "~>"
129
+ - - ">="
130
130
  - !ruby/object:Gem::Version
131
- version: '1'
131
+ version: '0'
132
132
  type: :runtime
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
- - - "~>"
136
+ - - ">="
137
137
  - !ruby/object:Gem::Version
138
- version: '1'
138
+ version: '0'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: aws-sdk-sns
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
- - - "~>"
143
+ - - ">="
144
144
  - !ruby/object:Gem::Version
145
- version: '1'
145
+ version: '0'
146
146
  type: :runtime
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
- - - "~>"
150
+ - - ">="
151
151
  - !ruby/object:Gem::Version
152
- version: '1'
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: aws-sdk-sqs
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
153
167
  - !ruby/object:Gem::Dependency
154
168
  name: bunny
155
169
  requirement: !ruby/object:Gem::Requirement