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 +4 -4
- data/README.md +69 -5
- data/lib/eventq/eventq_base/nonce_manager.rb +67 -15
- data/lib/eventq/queue_worker.rb +1 -1
- data/lib/eventq.rb +0 -1
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c0bbacb646ab33aa08c10738ceeba304c17c0c5f80024b0b8c5e22cb4a11e41a
|
4
|
+
data.tar.gz: d05f206a2231a46750ab172426725dc7a7347acfd084c6371ea888955182042c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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 =
|
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
|
-
|
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.
|
23
|
-
|
24
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
39
|
-
|
63
|
+
return true if !configured?
|
64
|
+
|
65
|
+
with_redis_connection do |conn|
|
66
|
+
conn.expire(nonce, lifespan)
|
40
67
|
end
|
41
|
-
|
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
|
-
|
46
|
-
|
74
|
+
return true if !configured?
|
75
|
+
|
76
|
+
with_redis_connection do |conn|
|
77
|
+
conn.del(nonce)
|
47
78
|
end
|
48
|
-
|
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
|
data/lib/eventq/queue_worker.rb
CHANGED
@@ -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.
|
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
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.
|
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:
|
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:
|
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.
|
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: []
|