eventsimple 1.4.2 → 1.5.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: 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