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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c4e4684f9e4b569bd20e0b09e70a043d44d1f650530e192d8a4eb9e60fcc35c7
4
- data.tar.gz: 40ee62f907e02a421db4e83653c7c64953df65402c64613597fafa75ae2d8370
3
+ metadata.gz: 0e34181920a4294d6f99a84819ea4861377c3e760a33bde393821ba4de8a8165
4
+ data.tar.gz: 426eeaa345d28493854e989c08891b40766fd595d5b822876dbac030e65ce9d8
5
5
  SHA512:
6
- metadata.gz: 4ba23518b78c46ca08a7b5669a2870a2e993054d95995c4bf7a564db63b0bcf5b8a141d380a3006fc2259f41689d6570a1ed9d39b8483badb84d1fb5f7d02105
7
- data.tar.gz: 9f27f387e7a7cff1631b6b4f427c167a692b1e2664783bd1a4ded614a3abb7266921c2f3195a521fe4e348064f3cce808e2eb9a481b58bdba4154f45af64179a
6
+ metadata.gz: a9f364c4e825147973326bcb9c87f0201720b6773957a6670a2c466b7febd606c5ba3868f81557f06b8a95a2e49035b866c83c188a2bc5e11a8c2af6e0de49f2
7
+ data.tar.gz: d761164e0bf06957c696f1d720c9d292b71a43a50827d44a0564a67280d36d0c98805af8bee7a042104cb8011da7ab0876c0b8e8e74e8eda61cdd7d20ca27b17
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.2.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
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- eventsimple (1.4.2)
4
+ eventsimple (1.5.0)
5
+ concurrent-ruby (>= 1.2.3)
5
6
  dry-struct (~> 1.6)
6
7
  dry-types (~> 1.7)
7
8
  pg (~> 1.4)
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
- **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.
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'
@@ -12,6 +12,7 @@ module Eventsimple
12
12
 
13
13
  config.after_initialize do
14
14
  require 'eventsimple/reactor'
15
+ require 'eventsimple/outbox/models/cursor'
15
16
 
16
17
  verify_dispatchers!
17
18
 
@@ -15,7 +15,6 @@ module Eventsimple
15
15
  self._aggregate_id = aggregate_id
16
16
 
17
17
  class_attribute :_outbox_enabled
18
- class_attribute :_consumer_group_size
19
18
 
20
19
  class_attribute :_on_invalid_transition
21
20
  self._on_invalid_transition = ->(error) { raise error }
@@ -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 = Set.new
81
- @async = Set.new
81
+ @sync = []
82
+ @async = []
82
83
  end
83
84
 
84
85
  def add_sync(reactors)
85
- @sync += reactors
86
+ @sync |= reactors
86
87
  end
87
88
 
88
89
  def add_async(reactors)
89
- @async += reactors
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, :group_number], unique: true
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 :_processor
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, group_size: 1)
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._processor = processor_klass.new
35
+ self._processor_pool = _concurrency.times.map { processor_klass.new }
33
36
  end
34
37
 
35
- def start(group_number: 0) # rubocop:disable Metrics/AbcSize
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(group_number: group_number)
52
+ run_consumer
46
53
  end
47
54
 
48
- def run_consumer(group_number:)
55
+ def run_consumer
49
56
  raise 'Eventsimple: No event class defined' unless _event_klass
50
- raise 'Eventsimple: No processor defined' unless _processor
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
- Rails.logger.info("Starting consumer for #{_identifier}, processing #{_event_klass} events with group number #{group_number}")
61
+ $stdout.puts("Starting consumer for #{_identifier}")
54
62
 
55
- cursor = Outbox::Cursor.fetch(_identifier, group_number: group_number)
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.each do |event|
60
- _processor.call(event)
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
- cursor = event.id
63
- break if stop_consumer
64
- end
78
+ Concurrent::Promises.zip(*promises).value!
65
79
 
66
- Outbox::Cursor.set(_identifier, cursor, group_number: group_number)
67
- break if stop_consumer
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, group_number: 0)
9
- existing = find_by(identifier: identifier.to_s, group_number: group_number)
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, group_number: 0)
13
+ def self.set(identifier, cursor)
14
14
  upsert(
15
15
  {
16
- identifier: identifier.to_s,
17
- group_number: group_number,
16
+ identifier: identifier,
18
17
  cursor: cursor,
19
18
  },
20
- unique_by: [:identifier, :group_number],
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 |dispatcher_klass|
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
- expect(reactors.sync).to include(dispatcher_klass)
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 |dispatcher_klass|
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
- expect(reactors.async).to include(dispatcher_klass)
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Eventsimple
4
- VERSION = '1.4.2'
4
+ VERSION = '1.5.0'
5
5
  end
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.2
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-04-30 00:00:00.000000000 Z
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.10
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