eventsimple 1.3.3 → 1.4.0
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 +4 -4
- data/CHANGELOG.md +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +8 -4
- data/README.md +11 -14
- data/eventsimple.gemspec +1 -1
- data/lib/eventsimple/event.rb +3 -3
- data/lib/eventsimple/outbox/consumer.rb +30 -13
- data/lib/eventsimple/outbox/models/cursor.rb +5 -5
- data/lib/eventsimple/version.rb +1 -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: 124fba5b3a01b411d35423265f33d4de64cf5a5824cb57491e502454557de90c
|
4
|
+
data.tar.gz: 9352b0c064d692bfc5f3623bf3c3a487a9c430a08f3526da2cb03ecaf36bd40c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 669a73959571590b6cc4ac32835c761d2aeb93a3b4c57e6043d79b94f5d271f523020f839845ae9f8aef0274757bb8de837d59a1822f6b51790fd582ff1e344a
|
7
|
+
data.tar.gz: d0740368d330aa08bfbd8e65fb7253800b1c8f055c29d9d92202b08812a79d64b74a08c8d557ab563efad57a021830a9a70fc4986b4bfd6aa652cb5e526b56db
|
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
6
6
|
|
7
7
|
## Unreleased
|
8
8
|
|
9
|
+
## 1.4.0 - 2024-04-02
|
10
|
+
### Changed
|
11
|
+
- Production ready release of the outbox consumer
|
12
|
+
- Multiple consumers on an event stream are supported
|
13
|
+
- The outbox processor is instantiated once and takes the event as an argument.
|
14
|
+
- Added proper SIGTERM event handling and fixed shutdown behaviour.
|
15
|
+
|
9
16
|
## 1.3.3 - 2024-04-02
|
10
17
|
### Changed
|
11
18
|
- add `parent_record` configuration so it can be easily overwritten
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
eventsimple (1.
|
4
|
+
eventsimple (1.4.0)
|
5
5
|
dry-struct (~> 1.6)
|
6
6
|
dry-types (~> 1.7)
|
7
7
|
pg (~> 1.4)
|
8
8
|
rails (~> 7.0)
|
9
9
|
retriable (~> 3.1)
|
10
|
+
with_advisory_lock (>= 5.1)
|
10
11
|
|
11
12
|
GEM
|
12
13
|
remote: https://rubygems.org/
|
@@ -176,7 +177,7 @@ GEM
|
|
176
177
|
net-imap
|
177
178
|
net-pop
|
178
179
|
net-smtp
|
179
|
-
marcel (1.0.
|
180
|
+
marcel (1.0.4)
|
180
181
|
method_source (1.0.0)
|
181
182
|
mini_mime (1.1.5)
|
182
183
|
minitest (5.22.2)
|
@@ -189,7 +190,7 @@ GEM
|
|
189
190
|
net-protocol
|
190
191
|
net-protocol (0.2.2)
|
191
192
|
timeout
|
192
|
-
net-smtp (0.
|
193
|
+
net-smtp (0.5.0)
|
193
194
|
net-protocol
|
194
195
|
nio4r (2.7.0)
|
195
196
|
nokogiri (1.16.2-arm64-darwin)
|
@@ -205,7 +206,7 @@ GEM
|
|
205
206
|
parser (3.3.0.5)
|
206
207
|
ast (~> 2.4.1)
|
207
208
|
racc
|
208
|
-
pg (1.5.
|
209
|
+
pg (1.5.6)
|
209
210
|
polyglot (0.3.5)
|
210
211
|
pry (0.14.2)
|
211
212
|
coderay (~> 1.1)
|
@@ -348,6 +349,9 @@ GEM
|
|
348
349
|
websocket-driver (0.7.6)
|
349
350
|
websocket-extensions (>= 0.1.0)
|
350
351
|
websocket-extensions (0.1.5)
|
352
|
+
with_advisory_lock (5.1.0)
|
353
|
+
activerecord (>= 6.1)
|
354
|
+
zeitwerk (>= 2.6)
|
351
355
|
ws-style (7.4.3)
|
352
356
|
rubocop-rspec (>= 2.2.0)
|
353
357
|
rubocop-vendor (>= 0.11)
|
data/README.md
CHANGED
@@ -276,14 +276,11 @@ end
|
|
276
276
|
|
277
277
|
## Configuring an outbox consumer
|
278
278
|
|
279
|
-
For many use cases, async reactors are sufficient to handle workflows like making an API call or publishing to a message broker.
|
280
|
-
However since reactors use ActiveJob, order is not guaranteed.
|
279
|
+
For many use cases, async reactors are sufficient to handle workflows like making an API call or publishing to a message broker. However as reactors use ActiveJob, order is not guaranteed. For use cases requiring order, eventsimple provides an ordered outbox implementation.
|
281
280
|
|
282
|
-
|
281
|
+
**Caveat**: The current implementation leverages a single advisory lock to guarantee write order. This will impact write throughput on the model. On a db.rg6.large Aurora instance for example, write throughput is limited to ~300 events per second.
|
283
282
|
|
284
|
-
|
285
|
-
|
286
|
-
For more information on why an advisory lock is required:
|
283
|
+
For an explaination of why an advisory lock is required:
|
287
284
|
https://github.com/pawelpacana/account-basics
|
288
285
|
|
289
286
|
### Setup an ordered outbox
|
@@ -297,8 +294,6 @@ Generate migration to setup the outbox cursor table. This table is used to track
|
|
297
294
|
Create a consummer and processor class for the outbox.
|
298
295
|
Note: The presence of the consumer class moves all writes to the respective events table to be written using an advisory lock.
|
299
296
|
|
300
|
-
Only a single outbox consumer per events table is supported. **DO NOT** create multiple consumers for the same events table.
|
301
|
-
|
302
297
|
```ruby
|
303
298
|
require 'eventsimple/outbox/consumer'
|
304
299
|
|
@@ -306,6 +301,7 @@ module UserComponent
|
|
306
301
|
class Consumer
|
307
302
|
extend Eventsimple::Outbox::Consumer
|
308
303
|
|
304
|
+
identitfier 'UserComponent::Consumer'
|
309
305
|
consumes_event UserEvent
|
310
306
|
processor EventProcessor
|
311
307
|
end
|
@@ -315,12 +311,7 @@ end
|
|
315
311
|
```ruby
|
316
312
|
module UserComponent
|
317
313
|
class EventProcessor
|
318
|
-
def
|
319
|
-
@event = event
|
320
|
-
end
|
321
|
-
attr_reader :event
|
322
|
-
|
323
|
-
def call
|
314
|
+
def call(event)
|
324
315
|
Rails.logger.info("PROCESSING EVENT: #{event.id}")
|
325
316
|
end
|
326
317
|
end
|
@@ -339,6 +330,12 @@ Create a rake task to run the consumer
|
|
339
330
|
end
|
340
331
|
```
|
341
332
|
|
333
|
+
To set the cursor position to the latest event:
|
334
|
+
|
335
|
+
```ruby
|
336
|
+
Eventsimple::Outbox::Cursor.set('UserComponent::Consumer', UserEvent.last.id)
|
337
|
+
```
|
338
|
+
|
342
339
|
## Helper methods
|
343
340
|
Some convenience methods are provided to help with common use cases.
|
344
341
|
|
data/eventsimple.gemspec
CHANGED
@@ -28,9 +28,9 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.add_runtime_dependency 'pg', '~> 1.4'
|
29
29
|
spec.add_runtime_dependency 'rails', '~> 7.0'
|
30
30
|
spec.add_runtime_dependency 'retriable', '~> 3.1'
|
31
|
+
spec.add_runtime_dependency 'with_advisory_lock', '>= 5.1'
|
31
32
|
|
32
33
|
spec.add_development_dependency 'bundle-audit'
|
33
|
-
spec.add_development_dependency 'factory_bot_rails'
|
34
34
|
spec.add_development_dependency 'fuubar'
|
35
35
|
spec.add_development_dependency 'git'
|
36
36
|
spec.add_development_dependency 'guard-rspec'
|
data/lib/eventsimple/event.rb
CHANGED
@@ -14,8 +14,8 @@ module Eventsimple
|
|
14
14
|
class_attribute :_aggregate_id
|
15
15
|
self._aggregate_id = aggregate_id
|
16
16
|
|
17
|
-
class_attribute :
|
18
|
-
class_attribute :
|
17
|
+
class_attribute :_outbox_enabled
|
18
|
+
class_attribute :_consumer_group_size
|
19
19
|
|
20
20
|
class_attribute :_on_invalid_transition
|
21
21
|
self._on_invalid_transition = ->(error) { raise error }
|
@@ -161,7 +161,7 @@ module Eventsimple
|
|
161
161
|
end
|
162
162
|
|
163
163
|
def with_locks(&block)
|
164
|
-
if
|
164
|
+
if _outbox_enabled
|
165
165
|
base_class.with_advisory_lock(base_class.name, { transaction: true }, &block)
|
166
166
|
else
|
167
167
|
yield
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'with_advisory_lock'
|
3
4
|
require 'eventsimple/outbox/models/cursor'
|
4
5
|
|
5
6
|
module Eventsimple
|
@@ -9,39 +10,55 @@ module Eventsimple
|
|
9
10
|
klass.class_exec do
|
10
11
|
class_attribute :_event_klass
|
11
12
|
class_attribute :_processor_klass
|
13
|
+
class_attribute :_processor
|
12
14
|
class_attribute :stop_consumer, default: false
|
13
|
-
|
14
|
-
Signal.trap('SIGINT') do
|
15
|
-
self.stop_consumer = true
|
16
|
-
$stdout.puts('SIGINT received, stopping consumer')
|
17
|
-
end
|
15
|
+
class_attribute :_identifier, default: name.to_s
|
18
16
|
end
|
19
17
|
end
|
20
18
|
|
21
|
-
def
|
22
|
-
|
23
|
-
|
19
|
+
def identifier(name = nil)
|
20
|
+
self._identifier = name
|
21
|
+
end
|
22
|
+
|
23
|
+
def consumes_event(event_klass, group_size: 1)
|
24
|
+
event_klass._outbox_enabled = true
|
25
|
+
event_klass._consumer_group_size = group_size
|
24
26
|
|
25
27
|
self._event_klass = event_klass
|
26
28
|
end
|
27
29
|
|
28
30
|
def processor(processor_klass)
|
29
31
|
self._processor_klass = processor_klass
|
32
|
+
self._processor = processor_klass.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def start(group_number: 0) # rubocop:disable Metrics/AbcSize
|
36
|
+
Signal.trap('INT') do
|
37
|
+
self.stop_consumer = true
|
38
|
+
$stdout.puts('INT received, stopping consumer')
|
39
|
+
end
|
40
|
+
Signal.trap('TERM') do
|
41
|
+
self.stop_consumer = true
|
42
|
+
$stdout.puts('TERM received, stopping consumer')
|
43
|
+
end
|
44
|
+
|
45
|
+
run_consumer(group_number: group_number)
|
30
46
|
end
|
31
47
|
|
32
|
-
def
|
33
|
-
cursor = Outbox::Cursor.fetch(
|
48
|
+
def run_consumer(group_number:)
|
49
|
+
cursor = Outbox::Cursor.fetch(_identifier, group_number: group_number)
|
34
50
|
|
35
51
|
until stop_consumer
|
36
52
|
_event_klass.unscoped.in_batches(start: cursor + 1, load: true).each do |batch|
|
37
53
|
batch.each do |event|
|
38
|
-
|
54
|
+
_processor.call(event)
|
39
55
|
|
56
|
+
cursor = event.id
|
40
57
|
break if stop_consumer
|
41
58
|
end
|
42
59
|
|
43
|
-
cursor
|
44
|
-
|
60
|
+
Outbox::Cursor.set(_identifier, cursor, group_number: group_number)
|
61
|
+
break if stop_consumer
|
45
62
|
end
|
46
63
|
|
47
64
|
sleep(1)
|
@@ -5,19 +5,19 @@ module Eventsimple
|
|
5
5
|
class Cursor < Eventsimple.configuration.parent_record_klass
|
6
6
|
self.table_name = 'eventsimple_outbox_cursors'
|
7
7
|
|
8
|
-
def self.fetch(
|
9
|
-
existing = find_by(
|
8
|
+
def self.fetch(identifier, group_number: 0)
|
9
|
+
existing = find_by(identifier: identifier.to_s, group_number: group_number)
|
10
10
|
existing ? existing.cursor : 0
|
11
11
|
end
|
12
12
|
|
13
|
-
def self.set(
|
13
|
+
def self.set(identifier, cursor, group_number: 0)
|
14
14
|
upsert(
|
15
15
|
{
|
16
|
-
|
16
|
+
identifier: identifier.to_s,
|
17
17
|
group_number: group_number,
|
18
18
|
cursor: cursor,
|
19
19
|
},
|
20
|
-
unique_by: [:
|
20
|
+
unique_by: [:identifier, :group_number],
|
21
21
|
)
|
22
22
|
end
|
23
23
|
end
|
data/lib/eventsimple/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: eventsimple
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Zulfiqar Ali
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-04-
|
11
|
+
date: 2024-04-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-struct
|
@@ -81,21 +81,21 @@ dependencies:
|
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '3.1'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: with_advisory_lock
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
90
|
-
type: :
|
89
|
+
version: '5.1'
|
90
|
+
type: :runtime
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '
|
96
|
+
version: '5.1'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
98
|
+
name: bundle-audit
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - ">="
|