eventq 4.1.0 → 4.2.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: 830a90e777c5896d7d7d7f7d02bf400ec53b2e46d37235956f489719742724f1
4
- data.tar.gz: 9d874da06b6190367a382ddac2cfc0dafe937bb622136474bf2430d32805af34
3
+ metadata.gz: c0bbacb646ab33aa08c10738ceeba304c17c0c5f80024b0b8c5e22cb4a11e41a
4
+ data.tar.gz: d05f206a2231a46750ab172426725dc7a7347acfd084c6371ea888955182042c
5
5
  SHA512:
6
- metadata.gz: 5ef60aecf44e22fe3ed07efd0e7d86205e3191c75c020e9a722715334598d284445fee1548cab1873868128f28556bc30fe9f5810628cb388d17add6c20bc4d9
7
- data.tar.gz: 2157ca99b534f9d34156f444da0343814bc7f415649b7aeda6ae9d6c4e97d3b2ce131549bff9ebf57973a6d859fa95d03b89c2dbbd22d18dc3c4c84c04c8a370
6
+ metadata.gz: 8daf3d93b434bae5ca09c3d2c51e9058df10f3355efbfcf116bd44eb52fd4769209f90a5638204de5e9eff93b418f07596bd746d33db5510a5c5a3471fea1f7f
7
+ data.tar.gz: 2cf23e913f766e2f2dff17cb1ea47dfbb5b6c1f476728113225cf50801ade03dfcacdfec810a44a8ac2c3c50d84b3af2fc91bbbda1dda7ea5ea2eb324040e7c2
data/README.md CHANGED
@@ -69,17 +69,72 @@ A subscription queue should be defined to receive any events raised for the subs
69
69
  **Example**
70
70
 
71
71
  ```ruby
72
- # Create a queue that allows retries and accepts a maximum of 3 retries with a 20 second delay between retries.
72
+ # Create a queue that allows retries and accepts a maximum of 5 retries with a 20 second delay between retries.
73
73
  class DataChangeAddressQueue < Queue
74
74
  def initialize
75
75
  @name = 'Data.Change.Address'
76
76
  @allow_retry = true
77
77
  @retry_delay = 20_000
78
- @max_retry_attempts = 3
78
+ @max_retry_attempts = 5
79
79
  end
80
80
  end
81
81
  ```
82
82
 
83
+ **Retry Strategies**
84
+
85
+ In distributed systems, it is expected for some events to fail.
86
+ Thankfully, those events can be put "on hold" and will be processed again after a given waiting time.
87
+ The attributes affecting your retry strategy the most are:
88
+ * `retry_delay` (base duration that events are waiting before being reprocessed)
89
+ * `max_receive_count` and `max_retry_attempts` (limiting how often an event can be seen / processed)
90
+ * `allow_retry`, `allow_retry_back_off` and `allow_exponential_back_off` (defining if retries are allowed and how duration between retries should be calculated)
91
+
92
+ If only `retry_delay` is set to `true`, while `allow_retry_back_off` and `allow_exponential_back_off` remain `false`, the duration between retries will be `retry_delay` each time ("fixed back off").
93
+ So there is a fixed duration between events, like in the example for `DataChangeAddressQueue` above.
94
+ With the configuration of that class, the event will be retried 5 times, with at least 20 seconds between retries.
95
+ Therefore we can calculate that the final retry will have happened after `retry_duration * max_retry_attempts`, which results in 100 seconds here.
96
+
97
+ If also `allow_retry_back_off` is set to `true`, the duration between retries will scale with the number of retries ("incremental back off").
98
+ So the first retry will happen after `retry_duration`, the second after `2 * retry_duration`, the third after `3 * retry_duration` and so on.
99
+ So the retries will be spread out further apart each time.
100
+ The last retry will be processed after `(max_retry_attempts * (max_retry_attempts + 1))/2 * retry_duration`.
101
+ So in the example above, it would result in 300 seconds until the last retry.
102
+
103
+ If also `allow_exponential_back_off` is set to `true`, the duration between retries will double each time ("exponential back off").
104
+ So the first retry will happen after `retry_duration`, the second after `2 * retry_duration`, the third after `4 * retry_duration` and so on.
105
+ The last retry will be processed after `(2^max_retry_attempts - 1) * retry_duration`.
106
+ So in the example above, it would result in 620 seconds until the last retry.
107
+
108
+ You can run experiments on your retry configuration using [plot_visibility_timeout.rb](https://github.com/Sage/eventq/blob/master/utilities/plot_visibility_timeout.rb), which will output the retry duration on each retry given your settings.
109
+
110
+
111
+ ![Graph comparing back off strategies](images/back-off-strategy.png)
112
+
113
+ **Randomness**
114
+
115
+ By default, there will be no randomness in your retry strategy.
116
+ However, that means that with a fixed 20 second back off, many events overloading your service will all come back after exactly 20 seconds, overloading it again.
117
+ Therefore it can be useful to introduce randomness to your retry duration, so the events that initially hit the queue at the same time, are spread out when scheduling them for retry.
118
+
119
+ The attribute `retry_jitter_ratio` allows you to configure how much randomness ("jitter") is allowed for the retry duration.
120
+ Let's assume we have a `retry_duration = 20_000` (20 seconds).
121
+ Then the `retry_jitter_ratio` would have the following effect:
122
+ * 0 means no randomness, so retry duration of 20 seconds is used every time
123
+ * 20 means 20% randomness, so the duration will be randomly chosen between 80% to 100% of the value, i.e. between 16 to 20 seconds
124
+ * 50 means 50% randomness, i.e. between 10 to 20 seconds
125
+ * 80 means 80% randomness, i.e. between 4 to 20 seconds
126
+ * 100 means 100% randomness, i.e. between 0 to 20 seconds
127
+
128
+ In the graphs below you can see how adding 50% randomness can help avoid overloading the service.
129
+ In the first graph ("Fixed Retry Duration"), all failures are hitting the queue again after exactly 20 seconds.
130
+ This leads to only a couple of events to succeed, as the others fail due to too many concurrent requests running into locks etc.
131
+ However, in the second graph ("Randomised Retry Duration"), the events are randomnly spread out over the next 10 to 20 seconds.
132
+ This means less events hit the service concurrently, allowing it to succesfully process more events and processing all of the events in a shorter duration, reducing the overall load on the service.
133
+
134
+ ![Graph showing that events overload the service repeatedly with fixed retry duration](images/fixed-retry-duration.png)
135
+
136
+ ![Graph showing that events are spread out on retries when randomising retry duration](images/randomised-retry-duration.png)
137
+
83
138
  ### SubscriptionManager
84
139
 
85
140
  In order to receive events within a subscription queue it must subscribe to the type of the event it should receive.
@@ -323,10 +378,11 @@ This method is called to verify connection to an event_type (topic/exchange).
323
378
 
324
379
  ## Development
325
380
 
326
- After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
327
-
328
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in the file, `EVENTQ_VERSION`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
381
+ ### Setup
329
382
 
383
+ After checking out the repo, run `bin/setup` to install dependencies.
384
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
385
+ To install this gem onto your local machine, run `bundle exec rake install`.
330
386
 
331
387
  ### Preparing the Docker images
332
388
 
@@ -352,6 +408,14 @@ You can run the specs that don't depend on an AWS account with:
352
408
 
353
409
  $ ./script/test.sh --tag ~integration
354
410
 
411
+ ### Release new version
412
+
413
+ To release a new version, first update the version number in the file [`EVENTQ_VERSION`](https://github.com/Sage/eventq/blob/master/EVENTQ_VERSION).
414
+ With that change merged to `master`, just [draft a new release](https://github.com/Sage/eventq/releases/new) with the same version you specified in `EVENTQ_VERSION`.
415
+ Use "Generate Release Notes" to generate details for this release.
416
+
417
+ This will create a git tag for the version and triggers the GitHub [Workflow to publish the new gem](https://github.com/Sage/eventq/actions/workflows/publish.yml) (defined in [publish.yml](https://github.com/Sage/eventq/blob/master/.github/workflows/publish.yml)) to [rubygems.org](https://rubygems.org).
418
+
355
419
  ## Contributing
356
420
 
357
421
  Bug reports and pull requests are welcome on GitHub at https://github.com/sage/eventq. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
@@ -1,10 +1,24 @@
1
1
  module EventQ
2
+ class NonceManagerNotConfiguredError < StandardError; end
3
+
2
4
  class NonceManager
3
5
 
4
- def self.configure(server:,timeout:10000,lifespan:3600)
6
+ def self.configure(server:,timeout:10000,lifespan:3600, pool_size: 5, pool_timeout: 5)
5
7
  @server_url = server
6
8
  @timeout = timeout
7
9
  @lifespan = lifespan
10
+ @pool_size = pool_size
11
+ @pool_timeout = pool_timeout
12
+
13
+ @redis_pool = begin
14
+ require 'connection_pool'
15
+ require 'redis'
16
+
17
+ ConnectionPool.new(size: @pool_size, timeout: @pool_timeout) do
18
+ Redis.new(url: @server_url)
19
+ end
20
+ end
21
+ @configured = true
8
22
  end
9
23
 
10
24
  def self.server_url
@@ -19,39 +33,77 @@ module EventQ
19
33
  @lifespan
20
34
  end
21
35
 
22
- def self.is_allowed?(nonce)
23
- if @server_url == nil
24
- return true
36
+ def self.pool_size
37
+ @pool_size
38
+ end
39
+
40
+ def self.pool_timeout
41
+ @pool_timeout
42
+ end
43
+
44
+ def self.lock(nonce)
45
+ # act as if successfully locked if not nonce manager configured - makes it a no-op
46
+ return true if !configured?
47
+
48
+ successfully_locked = false
49
+ with_redis_connection do |conn|
50
+ successfully_locked = conn.set(nonce, 1, ex: lifespan, nx: true)
25
51
  end
26
52
 
27
- require 'redlock'
28
- lock = Redlock::Client.new([ @server_url ]).lock(nonce, @timeout)
29
- if lock == false
53
+ if !successfully_locked
30
54
  EventQ.log(:info, "[#{self.class}] - Message has already been processed: #{nonce}")
31
- return false
32
55
  end
33
56
 
34
- return true
57
+ successfully_locked
35
58
  end
36
59
 
60
+ # if the message was successfully procesed, lock for another lifespan length
61
+ # so it isn't reprocessed
37
62
  def self.complete(nonce)
38
- if @server_url != nil
39
- Redis.new(url: @server_url).expire(nonce, @lifespan)
63
+ return true if !configured?
64
+
65
+ with_redis_connection do |conn|
66
+ conn.expire(nonce, lifespan)
40
67
  end
41
- return true
68
+
69
+ true
42
70
  end
43
71
 
72
+ # if it failed, unlock immediately so that retries can kick in
44
73
  def self.failed(nonce)
45
- if @server_url != nil
46
- Redis.new(url: @server_url).del(nonce)
74
+ return true if !configured?
75
+
76
+ with_redis_connection do |conn|
77
+ conn.del(nonce)
47
78
  end
48
- return true
79
+
80
+ true
49
81
  end
50
82
 
51
83
  def self.reset
52
84
  @server_url = nil
53
85
  @timeout = nil
54
86
  @lifespan = nil
87
+ @pool_size = nil
88
+ @pool_timeout = nil
89
+ @configured = false
90
+ @redis_pool.reload(&:close)
91
+ end
92
+
93
+ def self.configured?
94
+ @configured == true
95
+ end
96
+
97
+ private
98
+
99
+ def self.with_redis_connection
100
+ if !configured?
101
+ raise NonceManagerNotConfiguredError, 'Unable to checkout redis connection from pool, nonce manager has not been configured. Call .configure on NonceManager.'
102
+ end
103
+
104
+ @redis_pool.with do |conn|
105
+ yield conn
106
+ end
55
107
  end
56
108
  end
57
109
  end
@@ -127,7 +127,7 @@ module EventQ
127
127
 
128
128
  EventQ.logger.debug("[#{self.class}] - Message received. Id: #{message.id}. Retry Attempts: #{retry_attempts}")
129
129
 
130
- if (!EventQ::NonceManager.is_allowed?(message.id))
130
+ if (!EventQ::NonceManager.lock(message.id))
131
131
  EventQ.logger.warn("[#{self.class}] - Duplicate Message received. Id: #{message.id}. Ignoring message.")
132
132
  status = :duplicate
133
133
  return status, message_args
data/lib/eventq.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'securerandom'
4
- require 'redlock'
5
4
  require 'class_kit'
6
5
  require 'hash_kit'
7
6
  require 'oj'
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.1.0
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SageOne
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-12 00:00:00.000000000 Z
11
+ date: 2025-01-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -235,7 +235,7 @@ dependencies:
235
235
  - !ruby/object:Gem::Version
236
236
  version: '0'
237
237
  - !ruby/object:Gem::Dependency
238
- name: redlock
238
+ name: connection_pool
239
239
  requirement: !ruby/object:Gem::Requirement
240
240
  requirements:
241
241
  - - ">="
@@ -309,7 +309,7 @@ homepage: https://github.com/sage/eventq
309
309
  licenses:
310
310
  - MIT
311
311
  metadata: {}
312
- post_install_message:
312
+ post_install_message:
313
313
  rdoc_options: []
314
314
  require_paths:
315
315
  - lib
@@ -324,8 +324,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
324
324
  - !ruby/object:Gem::Version
325
325
  version: '0'
326
326
  requirements: []
327
- rubygems_version: 3.3.5
328
- signing_key:
327
+ rubygems_version: 3.4.20
328
+ signing_key:
329
329
  specification_version: 4
330
330
  summary: EventQ is a pub/sub system that uses async notifications and message queues
331
331
  test_files: []