eventsimple 1.4.2 → 1.5.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/.ruby-version +1 -1
- data/CHANGELOG.md +10 -0
- data/Gemfile.lock +2 -1
- data/README.md +3 -3
- data/eventsimple.gemspec +1 -0
- data/lib/eventsimple/engine.rb +1 -0
- data/lib/eventsimple/event.rb +0 -1
- data/lib/eventsimple/event_dispatcher.rb +5 -4
- data/lib/eventsimple/generators/outbox/templates/create_outbox_cursor.erb +1 -2
- data/lib/eventsimple/outbox/consumer.rb +35 -19
- data/lib/eventsimple/outbox/models/cursor.rb +5 -6
- data/lib/eventsimple/support/spec_helpers.rb +6 -4
- data/lib/eventsimple/version.rb +1 -1
- metadata +17 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e34181920a4294d6f99a84819ea4861377c3e760a33bde393821ba4de8a8165
|
|
4
|
+
data.tar.gz: 426eeaa345d28493854e989c08891b40766fd595d5b822876dbac030e65ce9d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a9f364c4e825147973326bcb9c87f0201720b6773957a6670a2c466b7febd606c5ba3868f81557f06b8a95a2e49035b866c83c188a2bc5e11a8c2af6e0de49f2
|
|
7
|
+
data.tar.gz: d761164e0bf06957c696f1d720c9d292b71a43a50827d44a0564a67280d36d0c98805af8bee7a042104cb8011da7ab0876c0b8e8e74e8eda61cdd7d20ca27b17
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.2.
|
|
1
|
+
3.2.3
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 1.5.0 - 2024-05-07
|
|
10
|
+
### Changed
|
|
11
|
+
- The outbox consumer processes event batches concurrently
|
|
12
|
+
- Removes unused group_number/group_size config
|
|
13
|
+
|
|
14
|
+
## 1.4.3 - 2024-05-06
|
|
15
|
+
### Changed
|
|
16
|
+
- The order of execution for synchronous reactors is now guaranteed to be the order in which they were registered.
|
|
17
|
+
- The shared examples for reactors `'an event which (a)synchronously dispatches'` now accept 1 or multiple arguments. The synchronous version checks that reactors are given in the order in which they are registered.
|
|
18
|
+
|
|
9
19
|
## 1.4.2 - 2024-04-30
|
|
10
20
|
### Changed
|
|
11
21
|
- Raise error on missing consumer config
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -276,9 +276,9 @@ 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. However as reactors use ActiveJob, order is not guaranteed. For use cases requiring order, eventsimple provides an ordered outbox implementation.
|
|
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 simple ordered outbox implementation.
|
|
280
280
|
|
|
281
|
-
|
|
281
|
+
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 to the table is ~300 events per second.
|
|
282
282
|
|
|
283
283
|
For an explaination of why an advisory lock is required:
|
|
284
284
|
https://github.com/pawelpacana/account-basics
|
|
@@ -292,7 +292,6 @@ Generate migration to setup the outbox cursor table. This table is used to track
|
|
|
292
292
|
```
|
|
293
293
|
|
|
294
294
|
Create a consummer and processor class for the outbox.
|
|
295
|
-
Note: The presence of the consumer class moves all writes to the respective events table to be written using an advisory lock.
|
|
296
295
|
|
|
297
296
|
```ruby
|
|
298
297
|
require 'eventsimple/outbox/consumer'
|
|
@@ -304,6 +303,7 @@ module UserComponent
|
|
|
304
303
|
identitfier 'UserComponent::Consumer'
|
|
305
304
|
consumes_event UserEvent
|
|
306
305
|
processor EventProcessor
|
|
306
|
+
concurrency 5 # default is 5
|
|
307
307
|
end
|
|
308
308
|
end
|
|
309
309
|
```
|
data/eventsimple.gemspec
CHANGED
|
@@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
|
|
|
29
29
|
spec.add_runtime_dependency 'rails', '~> 7.0'
|
|
30
30
|
spec.add_runtime_dependency 'retriable', '~> 3.1'
|
|
31
31
|
spec.add_runtime_dependency 'with_advisory_lock', '>= 5.1'
|
|
32
|
+
spec.add_runtime_dependency 'concurrent-ruby', '>= 1.2.3'
|
|
32
33
|
|
|
33
34
|
spec.add_development_dependency 'bundle-audit'
|
|
34
35
|
spec.add_development_dependency 'fuubar'
|
data/lib/eventsimple/engine.rb
CHANGED
data/lib/eventsimple/event.rb
CHANGED
|
@@ -55,6 +55,7 @@ module Eventsimple
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# Return a ReactorSet containing all Reactors matching an Event
|
|
58
|
+
# Reactors will be in the order in which they were registered
|
|
58
59
|
def for(event)
|
|
59
60
|
reactors = ReactorSet.new
|
|
60
61
|
|
|
@@ -77,16 +78,16 @@ module Eventsimple
|
|
|
77
78
|
attr_reader :sync, :async
|
|
78
79
|
|
|
79
80
|
def initialize
|
|
80
|
-
@sync =
|
|
81
|
-
@async =
|
|
81
|
+
@sync = []
|
|
82
|
+
@async = []
|
|
82
83
|
end
|
|
83
84
|
|
|
84
85
|
def add_sync(reactors)
|
|
85
|
-
@sync
|
|
86
|
+
@sync |= reactors
|
|
86
87
|
end
|
|
87
88
|
|
|
88
89
|
def add_async(reactors)
|
|
89
|
-
@async
|
|
90
|
+
@async |= reactors
|
|
90
91
|
end
|
|
91
92
|
end
|
|
92
93
|
end
|
|
@@ -4,10 +4,9 @@ class CreateEventsimpleOutboxCursor < ActiveRecord::Migration<%= migration_versi
|
|
|
4
4
|
def change
|
|
5
5
|
create_table :eventsimple_outbox_cursors do |t|
|
|
6
6
|
t.string :event_klass, null: false
|
|
7
|
-
t.integer :group_number, null: false
|
|
8
7
|
t.bigint :cursor, null: false
|
|
9
8
|
|
|
10
|
-
t.index [:event_klass
|
|
9
|
+
t.index [:event_klass], unique: true
|
|
11
10
|
end
|
|
12
11
|
end
|
|
13
12
|
end
|
|
@@ -6,13 +6,17 @@ require 'eventsimple/outbox/models/cursor'
|
|
|
6
6
|
module Eventsimple
|
|
7
7
|
module Outbox
|
|
8
8
|
module Consumer
|
|
9
|
+
class ExitError < StandardError; end
|
|
10
|
+
|
|
9
11
|
def self.extended(klass)
|
|
10
12
|
klass.class_exec do
|
|
11
13
|
class_attribute :_event_klass
|
|
14
|
+
class_attribute :_identifier
|
|
12
15
|
class_attribute :_processor_klass
|
|
13
|
-
class_attribute :
|
|
16
|
+
class_attribute :_processor_pool
|
|
17
|
+
class_attribute :_concurrency, default: 5
|
|
18
|
+
class_attribute :_batch_size, default: 1000
|
|
14
19
|
class_attribute :stop_consumer, default: false
|
|
15
|
-
class_attribute :_identifier
|
|
16
20
|
end
|
|
17
21
|
end
|
|
18
22
|
|
|
@@ -20,19 +24,22 @@ module Eventsimple
|
|
|
20
24
|
self._identifier = name
|
|
21
25
|
end
|
|
22
26
|
|
|
23
|
-
def consumes_event(event_klass
|
|
27
|
+
def consumes_event(event_klass)
|
|
24
28
|
event_klass._outbox_enabled = true
|
|
25
|
-
event_klass._consumer_group_size = group_size
|
|
26
29
|
|
|
27
30
|
self._event_klass = event_klass
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
def processor(processor_klass)
|
|
31
34
|
self._processor_klass = processor_klass
|
|
32
|
-
self.
|
|
35
|
+
self._processor_pool = _concurrency.times.map { processor_klass.new }
|
|
33
36
|
end
|
|
34
37
|
|
|
35
|
-
def
|
|
38
|
+
def concurrency(concurrency)
|
|
39
|
+
self._concurrency = concurrency
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def start # rubocop:disable Metrics/AbcSize
|
|
36
43
|
Signal.trap('INT') do
|
|
37
44
|
self.stop_consumer = true
|
|
38
45
|
$stdout.puts('INT received, stopping consumer')
|
|
@@ -42,33 +49,42 @@ module Eventsimple
|
|
|
42
49
|
$stdout.puts('TERM received, stopping consumer')
|
|
43
50
|
end
|
|
44
51
|
|
|
45
|
-
run_consumer
|
|
52
|
+
run_consumer
|
|
46
53
|
end
|
|
47
54
|
|
|
48
|
-
def run_consumer
|
|
55
|
+
def run_consumer
|
|
49
56
|
raise 'Eventsimple: No event class defined' unless _event_klass
|
|
50
|
-
raise 'Eventsimple: No processor defined' unless
|
|
57
|
+
raise 'Eventsimple: No processor defined' unless _processor_klass
|
|
51
58
|
raise 'Eventsimple: No identifier defined' unless _identifier
|
|
59
|
+
raise 'Eventsimple: No concurrency defined' unless _concurrency.is_a?(Integer)
|
|
52
60
|
|
|
53
|
-
|
|
61
|
+
$stdout.puts("Starting consumer for #{_identifier}")
|
|
54
62
|
|
|
55
|
-
cursor = Outbox::Cursor.fetch(_identifier
|
|
63
|
+
cursor = Outbox::Cursor.fetch(_identifier)
|
|
56
64
|
|
|
57
65
|
until stop_consumer
|
|
58
|
-
_event_klass.unscoped.in_batches(start: cursor + 1, load: true).each do |batch|
|
|
59
|
-
batch.
|
|
60
|
-
|
|
66
|
+
_event_klass.unscoped.in_batches(start: cursor + 1, load: true, of: _batch_size).each do |batch|
|
|
67
|
+
grouped_events = batch.group_by { |event| event.aggregate_id.unpack1('L') % _concurrency }
|
|
68
|
+
|
|
69
|
+
promises = grouped_events.map { |index, events|
|
|
70
|
+
Concurrent::Promises.future {
|
|
71
|
+
events.each do |event|
|
|
72
|
+
_processor_pool[index].call(event)
|
|
73
|
+
raise ExitError if stop_consumer
|
|
74
|
+
end
|
|
75
|
+
}
|
|
76
|
+
}
|
|
61
77
|
|
|
62
|
-
|
|
63
|
-
break if stop_consumer
|
|
64
|
-
end
|
|
78
|
+
Concurrent::Promises.zip(*promises).value!
|
|
65
79
|
|
|
66
|
-
|
|
67
|
-
|
|
80
|
+
cursor = batch.last.id
|
|
81
|
+
Outbox::Cursor.set(_identifier, cursor)
|
|
68
82
|
end
|
|
69
83
|
|
|
70
84
|
sleep(1)
|
|
71
85
|
end
|
|
86
|
+
rescue ExitError
|
|
87
|
+
$stdout.puts("Stopping consumer for #{_identifier}")
|
|
72
88
|
end
|
|
73
89
|
end
|
|
74
90
|
end
|
|
@@ -5,19 +5,18 @@ 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(identifier
|
|
9
|
-
existing = find_by(identifier: identifier
|
|
8
|
+
def self.fetch(identifier)
|
|
9
|
+
existing = find_by(identifier: identifier)
|
|
10
10
|
existing ? existing.cursor : 0
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def self.set(identifier, cursor
|
|
13
|
+
def self.set(identifier, cursor)
|
|
14
14
|
upsert(
|
|
15
15
|
{
|
|
16
|
-
identifier: identifier
|
|
17
|
-
group_number: group_number,
|
|
16
|
+
identifier: identifier,
|
|
18
17
|
cursor: cursor,
|
|
19
18
|
},
|
|
20
|
-
unique_by: [:identifier
|
|
19
|
+
unique_by: [:identifier],
|
|
21
20
|
)
|
|
22
21
|
end
|
|
23
22
|
end
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
RSpec.shared_examples 'an event which synchronously dispatches' do |
|
|
3
|
+
RSpec.shared_examples 'an event which synchronously dispatches' do |*dispatcher_klasses|
|
|
4
4
|
specify do
|
|
5
5
|
reactors = Eventsimple::EventDispatcher.rules.for(described_class.new)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# Order is important here since the synchronous reactors are executed sequentially
|
|
8
|
+
expect(reactors.sync & dispatcher_klasses).to eq(dispatcher_klasses)
|
|
8
9
|
end
|
|
9
10
|
end
|
|
10
11
|
|
|
11
|
-
RSpec.shared_examples 'an event which asynchronously dispatches' do |
|
|
12
|
+
RSpec.shared_examples 'an event which asynchronously dispatches' do |*dispatcher_klasses|
|
|
12
13
|
specify do
|
|
13
14
|
reactors = Eventsimple::EventDispatcher.rules.for(described_class.new)
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
# Order is _not_ important here since async reactors have no order guarantee
|
|
17
|
+
expect(reactors.async).to include(*dispatcher_klasses)
|
|
16
18
|
end
|
|
17
19
|
end
|
|
18
20
|
|
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.5.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-
|
|
11
|
+
date: 2024-05-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: dry-struct
|
|
@@ -94,6 +94,20 @@ dependencies:
|
|
|
94
94
|
- - ">="
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
96
|
version: '5.1'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: concurrent-ruby
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: 1.2.3
|
|
104
|
+
type: :runtime
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: 1.2.3
|
|
97
111
|
- !ruby/object:Gem::Dependency
|
|
98
112
|
name: bundle-audit
|
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -298,7 +312,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
298
312
|
- !ruby/object:Gem::Version
|
|
299
313
|
version: '0'
|
|
300
314
|
requirements: []
|
|
301
|
-
rubygems_version: 3.4.
|
|
315
|
+
rubygems_version: 3.4.19
|
|
302
316
|
signing_key:
|
|
303
317
|
specification_version: 4
|
|
304
318
|
summary: Event sourcing toolkit using Rails and ActiveJob
|