eventsimple 1.4.3 → 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: 89459aafe1f0b6343f5893f9921d43e2b41a77bdd051160f8206837589941dc7
4
- data.tar.gz: 9920a2bd4454bc8f97fbf9d9fc036ddc3801508321602a27cf1236de005a3633
3
+ metadata.gz: 0e34181920a4294d6f99a84819ea4861377c3e760a33bde393821ba4de8a8165
4
+ data.tar.gz: 426eeaa345d28493854e989c08891b40766fd595d5b822876dbac030e65ce9d8
5
5
  SHA512:
6
- metadata.gz: fa543c41fa1318ab325ad2be11586ba642f07ba4210cb9163b309023da7c56cffb9220ffa92af85b57f43011e02a38a3f60e416fa5c96e6c6a3da8ef5f465541
7
- data.tar.gz: c6a1e7bda23440825c00effa10ae939690f9b0963dd5269b048df08de4ea7de72785a6f26a168a0b2ddc7e1e3a038e8d20a35fccc55465130688058ee04b342c
6
+ metadata.gz: a9f364c4e825147973326bcb9c87f0201720b6773957a6670a2c466b7febd606c5ba3868f81557f06b8a95a2e49035b866c83c188a2bc5e11a8c2af6e0de49f2
7
+ data.tar.gz: d761164e0bf06957c696f1d720c9d292b71a43a50827d44a0564a67280d36d0c98805af8bee7a042104cb8011da7ab0876c0b8e8e74e8eda61cdd7d20ca27b17
data/CHANGELOG.md CHANGED
@@ -6,6 +6,11 @@ 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
+
9
14
  ## 1.4.3 - 2024-05-06
10
15
  ### Changed
11
16
  - The order of execution for synchronous reactors is now guaranteed to be the order in which they were registered.
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- eventsimple (1.4.3)
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 }
@@ -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,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Eventsimple
4
- VERSION = '1.4.3'
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.3
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-05-06 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