eventsimple 1.4.3 → 1.5.3

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: 42e3b62f3cfbc6f95ff097ea4e23917365589ff637aa69b8fe4a9a65fb13d41b
4
+ data.tar.gz: 796eb8680f793866df0b3d6689b8eb42bff643c6fcd70e8f90794e1c26a341a0
5
5
  SHA512:
6
- metadata.gz: fa543c41fa1318ab325ad2be11586ba642f07ba4210cb9163b309023da7c56cffb9220ffa92af85b57f43011e02a38a3f60e416fa5c96e6c6a3da8ef5f465541
7
- data.tar.gz: c6a1e7bda23440825c00effa10ae939690f9b0963dd5269b048df08de4ea7de72785a6f26a168a0b2ddc7e1e3a038e8d20a35fccc55465130688058ee04b342c
6
+ metadata.gz: 6d0c5d23e6963904e2ea48400d02f3b4878e00ab4a1b4b8def5972795d7dad5e6753169fbff261f0b207c21b03c37bd28300e1b6bdf9ec924eb45afa1d8fcd38
7
+ data.tar.gz: 67165eb3d7c2e324328fcef9c406e95118d8d1cc4e8c4ec7ef6b3e4b49a5ca5126907d42c2a2721e8b8fdfff1d288dd7f906d3f37971e8606c5548e85684f8f9
data/CHANGELOG.md CHANGED
@@ -6,6 +6,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 1.5.3 - 2024-09-09
10
+ ### Changed
11
+ - Pass self to `enable_writes!` block
12
+
13
+ ## 1.5.2 - 2024-05-22
14
+ ### Changed
15
+ - Add created_at index in events migration generation
16
+
17
+ ## 1.5.1 - 2024-05-13
18
+ ### Changed
19
+ - Fix bug where outbox concurrency was not being configured correctly.
20
+
21
+ ## 1.5.0 - 2024-05-07
22
+ ### Changed
23
+ - The outbox consumer processes event batches concurrently
24
+ - Removes unused group_number/group_size config
25
+
9
26
  ## 1.4.3 - 2024-05-06
10
27
  ### Changed
11
28
  - 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.3)
5
+ concurrent-ruby (>= 1.2.3)
5
6
  dry-struct (~> 1.6)
6
7
  dry-types (~> 1.7)
7
8
  pg (~> 1.4)
@@ -108,7 +109,7 @@ GEM
108
109
  dry-core (1.0.1)
109
110
  concurrent-ruby (~> 1.0)
110
111
  zeitwerk (~> 2.6)
111
- dry-inflector (1.0.0)
112
+ dry-inflector (1.1.0)
112
113
  dry-logic (1.5.0)
113
114
  concurrent-ruby (~> 1.0)
114
115
  dry-core (~> 1.0, < 2)
@@ -183,7 +184,7 @@ GEM
183
184
  minitest (5.22.2)
184
185
  mutex_m (0.2.0)
185
186
  nenv (0.3.0)
186
- net-imap (0.4.10)
187
+ net-imap (0.4.16)
187
188
  date
188
189
  net-protocol
189
190
  net-pop (0.1.2)
@@ -193,9 +194,9 @@ GEM
193
194
  net-smtp (0.5.0)
194
195
  net-protocol
195
196
  nio4r (2.7.0)
196
- nokogiri (1.16.2-arm64-darwin)
197
+ nokogiri (1.16.5-arm64-darwin)
197
198
  racc (~> 1.4)
198
- nokogiri (1.16.2-x86_64-linux)
199
+ nokogiri (1.16.5-x86_64-linux)
199
200
  racc (~> 1.4)
200
201
  notiffany (0.1.3)
201
202
  nenv (~> 0.1)
@@ -206,7 +207,7 @@ GEM
206
207
  parser (3.3.0.5)
207
208
  ast (~> 2.4.1)
208
209
  racc
209
- pg (1.5.6)
210
+ pg (1.5.8)
210
211
  polyglot (0.3.5)
211
212
  pry (0.14.2)
212
213
  coderay (~> 1.1)
@@ -260,13 +261,14 @@ GEM
260
261
  rb-inotify (0.10.1)
261
262
  ffi (~> 1.0)
262
263
  rchardet (1.8.0)
263
- rdoc (6.6.2)
264
+ rdoc (6.6.3.1)
264
265
  psych (>= 4.0.0)
265
266
  regexp_parser (2.9.0)
266
267
  reline (0.4.3)
267
268
  io-console (~> 0.5)
268
269
  retriable (3.1.2)
269
- rexml (3.2.6)
270
+ rexml (3.2.8)
271
+ strscan (>= 3.0.9)
270
272
  rspec (3.13.0)
271
273
  rspec-core (~> 3.13.0)
272
274
  rspec-expectations (~> 3.13.0)
@@ -338,6 +340,7 @@ GEM
338
340
  lint_roller (~> 1.0)
339
341
  rubocop-rails (~> 2.23.1)
340
342
  stringio (3.1.0)
343
+ strscan (3.1.0)
341
344
  thor (1.3.1)
342
345
  timeout (0.4.1)
343
346
  treetop (1.6.12)
data/README.md CHANGED
@@ -128,6 +128,7 @@ create_table :user_events do |t|
128
128
  t.timestamps
129
129
 
130
130
  t.index :idempotency_key, unique: true
131
+ t.index :created_at
131
132
  end
132
133
 
133
134
  add_column :users, :lock_version, :integer
@@ -276,9 +277,9 @@ end
276
277
 
277
278
  ## Configuring an outbox consumer
278
279
 
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.
280
+ 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
281
 
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.
282
+ 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
283
 
283
284
  For an explaination of why an advisory lock is required:
284
285
  https://github.com/pawelpacana/account-basics
@@ -292,7 +293,6 @@ Generate migration to setup the outbox cursor table. This table is used to track
292
293
  ```
293
294
 
294
295
  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
296
 
297
297
  ```ruby
298
298
  require 'eventsimple/outbox/consumer'
@@ -303,7 +303,7 @@ module UserComponent
303
303
 
304
304
  identitfier 'UserComponent::Consumer'
305
305
  consumes_event UserEvent
306
- processor EventProcessor
306
+ processor EventProcessor, concurrency: 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
 
@@ -39,7 +39,7 @@ module Eventsimple
39
39
  @readonly = false
40
40
 
41
41
  if block
42
- yield
42
+ yield self
43
43
  @readonly = true if was_readonly
44
44
  end
45
45
  end
@@ -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
@@ -13,6 +13,7 @@ class Create<%= model_name.camelize %>Events < ActiveRecord::Migration[7.0]
13
13
  t.timestamps
14
14
 
15
15
  t.index :idempotency_key, unique: true
16
+ t.index :created_at
16
17
  end
17
18
 
18
19
  # Enables optimistic locking on the evented table
@@ -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,19 @@ 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
- def processor(processor_klass)
33
+ def processor(processor_klass, concurrency: 5)
34
+ self._concurrency = concurrency
31
35
  self._processor_klass = processor_klass
32
- self._processor = processor_klass.new
36
+ self._processor_pool = _concurrency.times.map { processor_klass.new }
33
37
  end
34
38
 
35
- def start(group_number: 0) # rubocop:disable Metrics/AbcSize
39
+ def start # rubocop:disable Metrics/AbcSize
36
40
  Signal.trap('INT') do
37
41
  self.stop_consumer = true
38
42
  $stdout.puts('INT received, stopping consumer')
@@ -42,33 +46,42 @@ module Eventsimple
42
46
  $stdout.puts('TERM received, stopping consumer')
43
47
  end
44
48
 
45
- run_consumer(group_number: group_number)
49
+ run_consumer
46
50
  end
47
51
 
48
- def run_consumer(group_number:)
52
+ def run_consumer
49
53
  raise 'Eventsimple: No event class defined' unless _event_klass
50
- raise 'Eventsimple: No processor defined' unless _processor
54
+ raise 'Eventsimple: No processor defined' unless _processor_klass
51
55
  raise 'Eventsimple: No identifier defined' unless _identifier
56
+ raise 'Eventsimple: No concurrency defined' unless _concurrency.is_a?(Integer)
52
57
 
53
- Rails.logger.info("Starting consumer for #{_identifier}, processing #{_event_klass} events with group number #{group_number}")
58
+ $stdout.puts("Starting consumer for #{_identifier}")
54
59
 
55
- cursor = Outbox::Cursor.fetch(_identifier, group_number: group_number)
60
+ cursor = Outbox::Cursor.fetch(_identifier)
56
61
 
57
62
  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)
63
+ _event_klass.unscoped.in_batches(start: cursor + 1, load: true, of: _batch_size).each do |batch|
64
+ grouped_events = batch.group_by { |event| event.aggregate_id.unpack1('L') % _concurrency }
65
+
66
+ promises = grouped_events.map { |index, events|
67
+ Concurrent::Promises.future {
68
+ events.each do |event|
69
+ _processor_pool[index].call(event)
70
+ raise ExitError if stop_consumer
71
+ end
72
+ }
73
+ }
61
74
 
62
- cursor = event.id
63
- break if stop_consumer
64
- end
75
+ Concurrent::Promises.zip(*promises).value!
65
76
 
66
- Outbox::Cursor.set(_identifier, cursor, group_number: group_number)
67
- break if stop_consumer
77
+ cursor = batch.last.id
78
+ Outbox::Cursor.set(_identifier, cursor)
68
79
  end
69
80
 
70
81
  sleep(1)
71
82
  end
83
+ rescue ExitError
84
+ $stdout.puts("Stopping consumer for #{_identifier}")
72
85
  end
73
86
  end
74
87
  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.3'
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.3
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-09-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