eventsimple 1.3.3 → 1.4.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: 872c921ebca70dcc352f005afe39e0cf48ae4bb2d009f7f55acc95f0b1250f04
4
- data.tar.gz: 0445db43a4641baceb36926e29e9b9eaa9c41aff5c69029d05bc2af9c457afa3
3
+ metadata.gz: 124fba5b3a01b411d35423265f33d4de64cf5a5824cb57491e502454557de90c
4
+ data.tar.gz: 9352b0c064d692bfc5f3623bf3c3a487a9c430a08f3526da2cb03ecaf36bd40c
5
5
  SHA512:
6
- metadata.gz: fa5257d5083fa3a2f1f6a6e6d57e249e0a78dd8a8c4d1a257a1726a1b6939a857dde9832e1a7e25e113c8697a869de435737d836449bd5a4ccb0358da8d6d7d7
7
- data.tar.gz: b89aa88f3b19f9286148631207176c828be018c78cf68291235e267be2194f01f08bf85d56be04d9fd6045f02e98f008ade400ac0e7eb2c3858059fd96b552e7
6
+ metadata.gz: 669a73959571590b6cc4ac32835c761d2aeb93a3b4c57e6043d79b94f5d271f523020f839845ae9f8aef0274757bb8de837d59a1822f6b51790fd582ff1e344a
7
+ data.tar.gz: d0740368d330aa08bfbd8e65fb7253800b1c8f055c29d9d92202b08812a79d64b74a08c8d557ab563efad57a021830a9a70fc4986b4bfd6aa652cb5e526b56db
data/CHANGELOG.md CHANGED
@@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 1.4.0 - 2024-04-02
10
+ ### Changed
11
+ - Production ready release of the outbox consumer
12
+ - Multiple consumers on an event stream are supported
13
+ - The outbox processor is instantiated once and takes the event as an argument.
14
+ - Added proper SIGTERM event handling and fixed shutdown behaviour.
15
+
9
16
  ## 1.3.3 - 2024-04-02
10
17
  ### Changed
11
18
  - add `parent_record` configuration so it can be easily overwritten
data/Gemfile CHANGED
@@ -2,5 +2,9 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
+ group :test do
6
+ gem 'factory_bot_rails'
7
+ end
8
+
5
9
  # Specify your gem's dependencies in eventsimple.gemspec
6
10
  gemspec
data/Gemfile.lock CHANGED
@@ -1,12 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- eventsimple (1.3.3)
4
+ eventsimple (1.4.0)
5
5
  dry-struct (~> 1.6)
6
6
  dry-types (~> 1.7)
7
7
  pg (~> 1.4)
8
8
  rails (~> 7.0)
9
9
  retriable (~> 3.1)
10
+ with_advisory_lock (>= 5.1)
10
11
 
11
12
  GEM
12
13
  remote: https://rubygems.org/
@@ -176,7 +177,7 @@ GEM
176
177
  net-imap
177
178
  net-pop
178
179
  net-smtp
179
- marcel (1.0.3)
180
+ marcel (1.0.4)
180
181
  method_source (1.0.0)
181
182
  mini_mime (1.1.5)
182
183
  minitest (5.22.2)
@@ -189,7 +190,7 @@ GEM
189
190
  net-protocol
190
191
  net-protocol (0.2.2)
191
192
  timeout
192
- net-smtp (0.4.0.1)
193
+ net-smtp (0.5.0)
193
194
  net-protocol
194
195
  nio4r (2.7.0)
195
196
  nokogiri (1.16.2-arm64-darwin)
@@ -205,7 +206,7 @@ GEM
205
206
  parser (3.3.0.5)
206
207
  ast (~> 2.4.1)
207
208
  racc
208
- pg (1.5.5)
209
+ pg (1.5.6)
209
210
  polyglot (0.3.5)
210
211
  pry (0.14.2)
211
212
  coderay (~> 1.1)
@@ -348,6 +349,9 @@ GEM
348
349
  websocket-driver (0.7.6)
349
350
  websocket-extensions (>= 0.1.0)
350
351
  websocket-extensions (0.1.5)
352
+ with_advisory_lock (5.1.0)
353
+ activerecord (>= 6.1)
354
+ zeitwerk (>= 2.6)
351
355
  ws-style (7.4.3)
352
356
  rubocop-rspec (>= 2.2.0)
353
357
  rubocop-vendor (>= 0.11)
data/README.md CHANGED
@@ -276,14 +276,11 @@ 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.
280
- However since reactors use ActiveJob, order is not guaranteed.
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.
281
280
 
282
- Eventsimple provides an outbox implementation with order and eventual consistency guarantees.
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.
283
282
 
284
- **Caveat**: The current implementation leverages a single advisory lock to guarantee write order. This will significantly impact write throughput on the model. On a standard Aurora instance for example, write throughput is limited to ~300 events per second.
285
-
286
- For more information on why an advisory lock is required:
283
+ For an explaination of why an advisory lock is required:
287
284
  https://github.com/pawelpacana/account-basics
288
285
 
289
286
  ### Setup an ordered outbox
@@ -297,8 +294,6 @@ Generate migration to setup the outbox cursor table. This table is used to track
297
294
  Create a consummer and processor class for the outbox.
298
295
  Note: The presence of the consumer class moves all writes to the respective events table to be written using an advisory lock.
299
296
 
300
- Only a single outbox consumer per events table is supported. **DO NOT** create multiple consumers for the same events table.
301
-
302
297
  ```ruby
303
298
  require 'eventsimple/outbox/consumer'
304
299
 
@@ -306,6 +301,7 @@ module UserComponent
306
301
  class Consumer
307
302
  extend Eventsimple::Outbox::Consumer
308
303
 
304
+ identitfier 'UserComponent::Consumer'
309
305
  consumes_event UserEvent
310
306
  processor EventProcessor
311
307
  end
@@ -315,12 +311,7 @@ end
315
311
  ```ruby
316
312
  module UserComponent
317
313
  class EventProcessor
318
- def initialize(event)
319
- @event = event
320
- end
321
- attr_reader :event
322
-
323
- def call
314
+ def call(event)
324
315
  Rails.logger.info("PROCESSING EVENT: #{event.id}")
325
316
  end
326
317
  end
@@ -339,6 +330,12 @@ Create a rake task to run the consumer
339
330
  end
340
331
  ```
341
332
 
333
+ To set the cursor position to the latest event:
334
+
335
+ ```ruby
336
+ Eventsimple::Outbox::Cursor.set('UserComponent::Consumer', UserEvent.last.id)
337
+ ```
338
+
342
339
  ## Helper methods
343
340
  Some convenience methods are provided to help with common use cases.
344
341
 
data/eventsimple.gemspec CHANGED
@@ -28,9 +28,9 @@ Gem::Specification.new do |spec|
28
28
  spec.add_runtime_dependency 'pg', '~> 1.4'
29
29
  spec.add_runtime_dependency 'rails', '~> 7.0'
30
30
  spec.add_runtime_dependency 'retriable', '~> 3.1'
31
+ spec.add_runtime_dependency 'with_advisory_lock', '>= 5.1'
31
32
 
32
33
  spec.add_development_dependency 'bundle-audit'
33
- spec.add_development_dependency 'factory_bot_rails'
34
34
  spec.add_development_dependency 'fuubar'
35
35
  spec.add_development_dependency 'git'
36
36
  spec.add_development_dependency 'guard-rspec'
@@ -14,8 +14,8 @@ module Eventsimple
14
14
  class_attribute :_aggregate_id
15
15
  self._aggregate_id = aggregate_id
16
16
 
17
- class_attribute :_outbox_mode
18
- class_attribute :_outbox_concurrency
17
+ class_attribute :_outbox_enabled
18
+ class_attribute :_consumer_group_size
19
19
 
20
20
  class_attribute :_on_invalid_transition
21
21
  self._on_invalid_transition = ->(error) { raise error }
@@ -161,7 +161,7 @@ module Eventsimple
161
161
  end
162
162
 
163
163
  def with_locks(&block)
164
- if _outbox_mode
164
+ if _outbox_enabled
165
165
  base_class.with_advisory_lock(base_class.name, { transaction: true }, &block)
166
166
  else
167
167
  yield
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'with_advisory_lock'
3
4
  require 'eventsimple/outbox/models/cursor'
4
5
 
5
6
  module Eventsimple
@@ -9,39 +10,55 @@ module Eventsimple
9
10
  klass.class_exec do
10
11
  class_attribute :_event_klass
11
12
  class_attribute :_processor_klass
13
+ class_attribute :_processor
12
14
  class_attribute :stop_consumer, default: false
13
-
14
- Signal.trap('SIGINT') do
15
- self.stop_consumer = true
16
- $stdout.puts('SIGINT received, stopping consumer')
17
- end
15
+ class_attribute :_identifier, default: name.to_s
18
16
  end
19
17
  end
20
18
 
21
- def consumes_event(event_klass, concurrency: 1)
22
- event_klass._outbox_mode = true
23
- event_klass._outbox_concurrency = concurrency
19
+ def identifier(name = nil)
20
+ self._identifier = name
21
+ end
22
+
23
+ def consumes_event(event_klass, group_size: 1)
24
+ event_klass._outbox_enabled = true
25
+ event_klass._consumer_group_size = group_size
24
26
 
25
27
  self._event_klass = event_klass
26
28
  end
27
29
 
28
30
  def processor(processor_klass)
29
31
  self._processor_klass = processor_klass
32
+ self._processor = processor_klass.new
33
+ end
34
+
35
+ def start(group_number: 0) # rubocop:disable Metrics/AbcSize
36
+ Signal.trap('INT') do
37
+ self.stop_consumer = true
38
+ $stdout.puts('INT received, stopping consumer')
39
+ end
40
+ Signal.trap('TERM') do
41
+ self.stop_consumer = true
42
+ $stdout.puts('TERM received, stopping consumer')
43
+ end
44
+
45
+ run_consumer(group_number: group_number)
30
46
  end
31
47
 
32
- def start # rubocop:disable Metrics/AbcSize
33
- cursor = Outbox::Cursor.fetch(_event_klass, 0)
48
+ def run_consumer(group_number:)
49
+ cursor = Outbox::Cursor.fetch(_identifier, group_number: group_number)
34
50
 
35
51
  until stop_consumer
36
52
  _event_klass.unscoped.in_batches(start: cursor + 1, load: true).each do |batch|
37
53
  batch.each do |event|
38
- _processor_klass.new(event).call
54
+ _processor.call(event)
39
55
 
56
+ cursor = event.id
40
57
  break if stop_consumer
41
58
  end
42
59
 
43
- cursor = batch.last.id
44
- Outbox::Cursor.set(_event_klass, 0, cursor)
60
+ Outbox::Cursor.set(_identifier, cursor, group_number: group_number)
61
+ break if stop_consumer
45
62
  end
46
63
 
47
64
  sleep(1)
@@ -5,19 +5,19 @@ 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(event_klass, group_number)
9
- existing = find_by(event_klass: event_klass.to_s, group_number: group_number)
8
+ def self.fetch(identifier, group_number: 0)
9
+ existing = find_by(identifier: identifier.to_s, group_number: group_number)
10
10
  existing ? existing.cursor : 0
11
11
  end
12
12
 
13
- def self.set(event_klass, group_number, cursor)
13
+ def self.set(identifier, cursor, group_number: 0)
14
14
  upsert(
15
15
  {
16
- event_klass: event_klass.to_s,
16
+ identifier: identifier.to_s,
17
17
  group_number: group_number,
18
18
  cursor: cursor,
19
19
  },
20
- unique_by: [:event_klass, :group_number],
20
+ unique_by: [:identifier, :group_number],
21
21
  )
22
22
  end
23
23
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Eventsimple
4
- VERSION = '1.3.3'
4
+ VERSION = '1.4.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.3.3
4
+ version: 1.4.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-03 00:00:00.000000000 Z
11
+ date: 2024-04-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-struct
@@ -81,21 +81,21 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '3.1'
83
83
  - !ruby/object:Gem::Dependency
84
- name: bundle-audit
84
+ name: with_advisory_lock
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
89
+ version: '5.1'
90
+ type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
- version: '0'
96
+ version: '5.1'
97
97
  - !ruby/object:Gem::Dependency
98
- name: factory_bot_rails
98
+ name: bundle-audit
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - ">="