eventsimple 1.3.3 → 1.4.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: 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
  - - ">="