ruby_event_store-outbox 0.0.10 → 0.0.15

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: d7e02fae40b268652d0187bfff203ea94993204069992b05d71808a3a820b28b
4
- data.tar.gz: c78fdc01af8ca386f23babbcc409535e4c983238af4a13e3541d32f8b30b7f96
3
+ metadata.gz: a4c373da7338563c51f8bc7d3bae7e56ceb99f9b75d8104196d003f5316cdb6a
4
+ data.tar.gz: fbb6dad9bdaee2fea028a2cea8541eb6a9f964e1d10d4e704ebe3f715a88d623
5
5
  SHA512:
6
- metadata.gz: cca7886a64c64912770e4415ec65523309c6af0ef5edbc0c3d5a55fd2e7937be36517104d0539a1436e006af5623f82d0f7be09e79f2b78604fd257c85d8c0ed
7
- data.tar.gz: d8435157be3745483024b2ddbc867502a0a340a10846485b35ac204eba845b5e05da924f24771b9b8e697b911c9c3eb605c5d7129c4fe8c9e0d66ca04138e6eb
6
+ metadata.gz: e6f7ac3498d139a98ef14b8cbb7abe7bd4af8b794b41f90c6b65150523410a25a59ff6e863301d87f3a90bb5be80cc422d7ae94dd8a16fedd3b88a1db334eee0
7
+ data.tar.gz: 1c30a1e0c60bee39d1064e0aa7c535416a3cca7d4aa25328ab684741bf4a6bc65373e412388e65d5e57183a4dbf25e4fff385c456cd562c1afc2fe9e04e1d5d8
@@ -11,5 +11,13 @@ class CreateEventStoreOutbox < ActiveRecord::Migration<%= migration_version %>
11
11
  end
12
12
  add_index :event_store_outbox, [:format, :enqueued_at, :split_key], name: "index_event_store_outbox_for_pool"
13
13
  add_index :event_store_outbox, [:created_at, :enqueued_at], name: "index_event_store_outbox_for_clear"
14
+
15
+ create_table(:event_store_outbox_locks, force: false) do |t|
16
+ t.string :format, null: false
17
+ t.string :split_key, null: false
18
+ t.datetime :locked_at, null: true
19
+ t.string :locked_by, null: true, limit: 36
20
+ end
21
+ add_index :event_store_outbox_locks, [:format, :split_key], name: "index_event_store_outbox_locks_for_locking", unique: true
14
22
  end
15
23
  end
@@ -5,6 +5,7 @@ module RubyEventStore
5
5
  end
6
6
  end
7
7
 
8
+ require_relative 'outbox/fetch_specification'
8
9
  require_relative 'outbox/record'
9
10
  require_relative 'outbox/sidekiq_scheduler'
10
11
  require_relative 'outbox/consumer'
@@ -59,7 +59,8 @@ module RubyEventStore
59
59
  end
60
60
 
61
61
  def build_consumer(options)
62
- logger = Logger.new(STDOUT, level: options.log_level, progname: "RES-Outbox")
62
+ consumer_uuid = SecureRandom.uuid
63
+ logger = Logger.new(STDOUT, level: options.log_level, progname: "RES-Outbox #{consumer_uuid}")
63
64
  consumer_configuration = Consumer::Configuration.new(
64
65
  split_keys: options.split_keys,
65
66
  message_format: options.message_format,
@@ -69,6 +70,7 @@ module RubyEventStore
69
70
  )
70
71
  metrics = Metrics.from_url(options.metrics_url)
71
72
  outbox_consumer = RubyEventStore::Outbox::Consumer.new(
73
+ consumer_uuid,
72
74
  options,
73
75
  logger: logger,
74
76
  metrics: metrics,
@@ -3,11 +3,14 @@ require "redis"
3
3
  require "active_record"
4
4
  require "ruby_event_store/outbox/record"
5
5
  require "ruby_event_store/outbox/sidekiq5_format"
6
+ require "ruby_event_store/outbox/sidekiq_processor"
7
+ require "ruby_event_store/outbox/fetch_specification"
6
8
 
7
9
  module RubyEventStore
8
10
  module Outbox
9
11
  class Consumer
10
- SLEEP_TIME_WHEN_NOTHING_TO_DO = 0.1
12
+ SLEEP_TIME_WHEN_NOTHING_TO_DO = 0.5
13
+ MAXIMUM_BATCH_FETCHES_IN_ONE_LOCK = 10
11
14
 
12
15
  class Configuration
13
16
  def initialize(
@@ -38,28 +41,26 @@ module RubyEventStore
38
41
  attr_reader :split_keys, :message_format, :batch_size, :database_url, :redis_url
39
42
  end
40
43
 
41
- def initialize(configuration, clock: Time, logger:, metrics:)
44
+ def initialize(consumer_uuid, configuration, clock: Time, logger:, metrics:)
42
45
  @split_keys = configuration.split_keys
43
46
  @clock = clock
44
- @redis = Redis.new(url: configuration.redis_url)
45
47
  @logger = logger
46
48
  @metrics = metrics
47
49
  @batch_size = configuration.batch_size
50
+ @consumer_uuid = consumer_uuid
48
51
  ActiveRecord::Base.establish_connection(configuration.database_url) unless ActiveRecord::Base.connected?
49
52
  if ActiveRecord::Base.connection.adapter_name == "Mysql2"
50
- ActiveRecord::Base.connection.execute("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;")
51
53
  ActiveRecord::Base.connection.execute("SET SESSION innodb_lock_wait_timeout = 1;")
52
54
  end
53
55
 
54
56
  raise "Unknown format" if configuration.message_format != SIDEKIQ5_FORMAT
55
- @message_format = SIDEKIQ5_FORMAT
57
+ @processor = SidekiqProcessor.new(Redis.new(url: configuration.redis_url))
56
58
 
57
59
  @gracefully_shutting_down = false
58
60
  prepare_traps
59
61
  end
60
62
 
61
63
  def init
62
- @redis.sadd("queues", split_keys)
63
64
  logger.info("Initiated RubyEventStore::Outbox v#{VERSION}")
64
65
  logger.info("Handling split keys: #{split_keys ? split_keys.join(", ") : "(all of them)"}")
65
66
  end
@@ -76,62 +77,129 @@ module RubyEventStore
76
77
  end
77
78
 
78
79
  def one_loop
79
- Record.transaction do
80
- records_scope = Record.lock.where(format: message_format, enqueued_at: nil)
81
- records_scope = records_scope.where(split_key: split_keys) if !split_keys.nil?
82
- records = records_scope.order("id ASC").limit(batch_size).to_a
83
- if records.empty?
84
- metrics.write_point_queue(status: "ok")
85
- return false
80
+ remaining_split_keys = @split_keys.dup
81
+
82
+ was_something_changed = false
83
+ while (split_key = remaining_split_keys.shift)
84
+ was_something_changed |= handle_split(FetchSpecification.new(processor.message_format, split_key))
85
+ end
86
+ was_something_changed
87
+ end
88
+
89
+ def handle_split(fetch_specification)
90
+ obtained_lock = obtain_lock_for_process(fetch_specification)
91
+ return false unless obtained_lock
92
+
93
+ something_processed = false
94
+
95
+ MAXIMUM_BATCH_FETCHES_IN_ONE_LOCK.times do
96
+ batch = retrieve_batch(fetch_specification)
97
+ if batch.empty?
98
+ break
86
99
  end
87
100
 
88
- now = @clock.now.utc
89
101
  failed_record_ids = []
90
- records.group_by(&:split_key).each do |split_key, records2|
102
+ updated_record_ids = []
103
+ batch.each do |record|
91
104
  begin
92
- failed = handle_group_of_records(now, split_key, records2)
93
- failed_record_ids.concat(failed.map(&:id))
105
+ now = @clock.now.utc
106
+ processor.process(record, now)
107
+
108
+ record.update_column(:enqueued_at, now)
109
+ something_processed |= true
110
+ updated_record_ids << record.id
94
111
  rescue => e
95
- failed_record_ids.concat(records2.map(&:id))
112
+ failed_record_ids << record.id
96
113
  e.full_message.split($/).each {|line| logger.error(line) }
97
114
  end
98
115
  end
99
116
 
100
- updated_record_ids = records.map(&:id) - failed_record_ids
101
- Record.where(id: updated_record_ids).update_all(enqueued_at: now)
102
- metrics.write_point_queue(status: "ok", enqueued: updated_record_ids.size, failed: failed_record_ids.size)
117
+ metrics.write_point_queue(
118
+ enqueued: updated_record_ids.size,
119
+ failed: failed_record_ids.size,
120
+ format: fetch_specification.message_format,
121
+ split_key: fetch_specification.split_key,
122
+ remaining: get_remaining_count(fetch_specification)
123
+ )
103
124
 
104
125
  logger.info "Sent #{updated_record_ids.size} messages from outbox table"
105
- true
126
+
127
+ obtained_lock = refresh_lock_for_process(obtained_lock)
128
+ break unless obtained_lock
106
129
  end
107
- rescue ActiveRecord::Deadlocked
108
- logger.warn "Outbox fetch deadlocked"
109
- metrics.write_point_queue(status: "deadlocked")
110
- false
111
- rescue ActiveRecord::LockWaitTimeout
112
- logger.warn "Outbox fetch lock timeout"
113
- metrics.write_point_queue(status: "lock_timeout")
114
- false
130
+
131
+ metrics.write_point_queue(
132
+ format: fetch_specification.message_format,
133
+ split_key: fetch_specification.split_key,
134
+ remaining: get_remaining_count(fetch_specification)
135
+ ) unless something_processed
136
+
137
+ release_lock_for_process(fetch_specification)
138
+
139
+ processor.after_batch
140
+
141
+ something_processed
115
142
  end
116
143
 
117
144
  private
118
- attr_reader :split_keys, :logger, :message_format, :batch_size, :metrics
119
-
120
- def handle_group_of_records(now, split_key, records)
121
- failed = []
122
- elements = []
123
- records.each do |record|
124
- begin
125
- elements << JSON.generate(JSON.parse(record.payload).merge({
126
- "enqueued_at" => now.to_f,
127
- }))
128
- rescue => e
129
- failed << record
130
- e.full_message.split($/).each {|line| logger.error(line) }
131
- end
145
+ attr_reader :split_keys, :logger, :batch_size, :metrics, :processor, :consumer_uuid
146
+
147
+ def obtain_lock_for_process(fetch_specification)
148
+ result = Lock.obtain(fetch_specification, consumer_uuid, clock: @clock)
149
+ case result
150
+ when :deadlocked
151
+ logger.warn "Obtaining lock for split_key '#{fetch_specification.split_key}' failed (deadlock)"
152
+ metrics.write_operation_result("obtain", "deadlocked")
153
+ return false
154
+ when :lock_timeout
155
+ logger.warn "Obtaining lock for split_key '#{fetch_specification.split_key}' failed (lock timeout)"
156
+ metrics.write_operation_result("obtain", "lock_timeout")
157
+ return false
158
+ when :taken
159
+ logger.debug "Obtaining lock for split_key '#{fetch_specification.split_key}' unsuccessful (taken)"
160
+ metrics.write_operation_result("obtain", "taken")
161
+ return false
162
+ else
163
+ return result
164
+ end
165
+ end
166
+
167
+ def release_lock_for_process(fetch_specification)
168
+ result = Lock.release(fetch_specification, consumer_uuid)
169
+ case result
170
+ when :ok
171
+ when :deadlocked
172
+ logger.warn "Releasing lock for split_key '#{fetch_specification.split_key}' failed (deadlock)"
173
+ metrics.write_operation_result("release", "deadlocked")
174
+ when :lock_timeout
175
+ logger.warn "Releasing lock for split_key '#{fetch_specification.split_key}' failed (lock timeout)"
176
+ metrics.write_operation_result("release", "lock_timeout")
177
+ when :not_taken_by_this_process
178
+ logger.debug "Releasing lock for split_key '#{fetch_specification.split_key}' failed (not taken by this process)"
179
+ metrics.write_operation_result("release", "not_taken_by_this_process")
180
+ else
181
+ raise "Unexpected result #{result}"
182
+ end
183
+ end
184
+
185
+ def refresh_lock_for_process(lock)
186
+ result = lock.refresh(clock: @clock)
187
+ case result
188
+ when :deadlocked
189
+ logger.warn "Refreshing lock for split_key '#{lock.split_key}' failed (deadlock)"
190
+ metrics.write_operation_result("refresh", "deadlocked")
191
+ return false
192
+ when :lock_timeout
193
+ logger.warn "Refreshing lock for split_key '#{lock.split_key}' failed (lock timeout)"
194
+ metrics.write_operation_result("refresh", "lock_timeout")
195
+ return false
196
+ when :stolen
197
+ logger.debug "Refreshing lock for split_key '#{lock.split_key}' unsuccessful (stolen)"
198
+ metrics.write_operation_result("refresh", "stolen")
199
+ return false
200
+ else
201
+ return result
132
202
  end
133
- @redis.lpush("queue:#{split_key}", elements)
134
- failed
135
203
  end
136
204
 
137
205
  def prepare_traps
@@ -146,6 +214,14 @@ module RubyEventStore
146
214
  def initiate_graceful_shutdown
147
215
  @gracefully_shutting_down = true
148
216
  end
217
+
218
+ def retrieve_batch(fetch_specification)
219
+ Record.remaining_for(fetch_specification).order("id ASC").limit(batch_size).to_a
220
+ end
221
+
222
+ def get_remaining_count(fetch_specification)
223
+ Record.remaining_for(fetch_specification).count
224
+ end
149
225
  end
150
226
  end
151
227
  end
@@ -0,0 +1,6 @@
1
+ module RubyEventStore
2
+ module Outbox
3
+ class ConsumerProcess
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,13 @@
1
+ module RubyEventStore
2
+ module Outbox
3
+ class FetchSpecification
4
+ def initialize(message_format, split_key)
5
+ @message_format = message_format
6
+ @split_key = split_key
7
+ freeze
8
+ end
9
+
10
+ attr_reader :message_format, :split_key
11
+ end
12
+ end
13
+ end
@@ -17,14 +17,28 @@ module RubyEventStore
17
17
  @influxdb_client = InfluxDB::Client.new(**options)
18
18
  end
19
19
 
20
- def write_point_queue(status:, enqueued: 0, failed: 0)
20
+ def write_operation_result(operation, result)
21
+ write_point("ruby_event_store.outbox.lock", {
22
+ values: {
23
+ value: 1,
24
+ },
25
+ tags: {
26
+ operation: operation,
27
+ result: result,
28
+ }
29
+ })
30
+ end
31
+
32
+ def write_point_queue(enqueued: 0, failed: 0, remaining: 0, format: nil, split_key: nil)
21
33
  write_point("ruby_event_store.outbox.queue", {
22
34
  values: {
23
35
  enqueued: enqueued,
24
36
  failed: failed,
37
+ remaining: remaining,
25
38
  },
26
39
  tags: {
27
- status: status,
40
+ format: format,
41
+ split_key: split_key,
28
42
  }
29
43
  })
30
44
  end
@@ -2,6 +2,9 @@ module RubyEventStore
2
2
  module Outbox
3
3
  module Metrics
4
4
  class Null
5
+ def write_operation_result(operation, result)
6
+ end
7
+
5
8
  def write_point_queue(**kwargs)
6
9
  end
7
10
  end
@@ -8,9 +8,96 @@ module RubyEventStore
8
8
  self.primary_key = :id
9
9
  self.table_name = 'event_store_outbox'
10
10
 
11
+ def self.remaining_for(fetch_specification)
12
+ where(format: fetch_specification.message_format, split_key: fetch_specification.split_key, enqueued_at: nil)
13
+ end
14
+
11
15
  def hash_payload
12
16
  JSON.parse(payload).deep_symbolize_keys
13
17
  end
18
+
19
+ def enqueued?
20
+ !enqueued_at.nil?
21
+ end
22
+ end
23
+
24
+ class Lock < ::ActiveRecord::Base
25
+ self.table_name = 'event_store_outbox_locks'
26
+
27
+ def self.obtain(fetch_specification, process_uuid, clock:)
28
+ l = nil
29
+ transaction do
30
+ l = get_lock_record(fetch_specification)
31
+
32
+ return :taken if l.recently_locked?
33
+
34
+ l.update!(
35
+ locked_by: process_uuid,
36
+ locked_at: clock.now,
37
+ )
38
+ end
39
+ l
40
+ rescue ActiveRecord::Deadlocked
41
+ :deadlocked
42
+ rescue ActiveRecord::LockWaitTimeout
43
+ :lock_timeout
44
+ end
45
+
46
+ def refresh(clock:)
47
+ transaction do
48
+ current_process_uuid = locked_by
49
+ lock!
50
+ if locked_by == current_process_uuid
51
+ update!(locked_at: clock.now)
52
+ return self
53
+ else
54
+ return :stolen
55
+ end
56
+ end
57
+ rescue ActiveRecord::Deadlocked
58
+ :deadlocked
59
+ rescue ActiveRecord::LockWaitTimeout
60
+ :lock_timeout
61
+ end
62
+
63
+ def self.release(fetch_specification, process_uuid)
64
+ transaction do
65
+ l = get_lock_record(fetch_specification)
66
+ return :not_taken_by_this_process if !l.locked_by?(process_uuid)
67
+
68
+ l.update!(locked_by: nil, locked_at: nil)
69
+ end
70
+ :ok
71
+ rescue ActiveRecord::Deadlocked
72
+ :deadlocked
73
+ rescue ActiveRecord::LockWaitTimeout
74
+ :lock_timeout
75
+ end
76
+
77
+ def locked_by?(process_uuid)
78
+ locked_by.eql?(process_uuid)
79
+ end
80
+
81
+ def recently_locked?
82
+ locked_by && locked_at > 10.minutes.ago
83
+ end
84
+
85
+ private
86
+ def self.lock_for_split_key(fetch_specification)
87
+ lock.find_by(format: fetch_specification.message_format, split_key: fetch_specification.split_key)
88
+ end
89
+
90
+ def self.get_lock_record(fetch_specification)
91
+ l = lock_for_split_key(fetch_specification)
92
+ if l.nil?
93
+ begin
94
+ l = create!(format: fetch_specification.message_format, split_key: fetch_specification.split_key)
95
+ rescue ActiveRecord::RecordNotUnique
96
+ l = lock_for_split_key(fetch_specification)
97
+ end
98
+ end
99
+ l
100
+ end
14
101
  end
15
102
  end
16
103
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_event_store/outbox/sidekiq5_format"
4
+
5
+ module RubyEventStore
6
+ module Outbox
7
+ class SidekiqProcessor
8
+ InvalidPayload = Class.new(StandardError)
9
+
10
+ def initialize(redis)
11
+ @redis = redis
12
+ @recently_used_queues = Set.new
13
+ end
14
+
15
+ def process(record, now)
16
+ parsed_record = JSON.parse(record.payload)
17
+
18
+ queue = parsed_record["queue"]
19
+ raise InvalidPayload.new("Missing queue") if queue.nil? || queue.empty?
20
+ payload = JSON.generate(parsed_record.merge({
21
+ "enqueued_at" => now.to_f,
22
+ }))
23
+
24
+ redis.lpush("queue:#{queue}", payload)
25
+
26
+ @recently_used_queues << queue
27
+ end
28
+
29
+ def after_batch
30
+ if !@recently_used_queues.empty?
31
+ redis.sadd("queues", @recently_used_queues.to_a)
32
+ @recently_used_queues.clear
33
+ end
34
+ end
35
+
36
+ def message_format
37
+ SIDEKIQ5_FORMAT
38
+ end
39
+
40
+ private
41
+ attr_reader :redis
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sidekiq'
4
+ require "ruby_event_store/outbox/sidekiq5_format"
5
+
6
+ module RubyEventStore
7
+ module Outbox
8
+ class SidekiqProducer
9
+ def call(klass, args)
10
+ sidekiq_client = Sidekiq::Client.new(Sidekiq.redis_pool)
11
+ item = {
12
+ 'class' => klass,
13
+ 'args' => args,
14
+ }
15
+ normalized_item = sidekiq_client.__send__(:normalize_item, item)
16
+ payload = sidekiq_client.__send__(:process_single, normalized_item.fetch('class'), normalized_item)
17
+ if payload
18
+ Record.create!(
19
+ format: SIDEKIQ5_FORMAT,
20
+ split_key: payload.fetch('queue'),
21
+ payload: payload.to_json
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,31 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'sidekiq'
4
- require "ruby_event_store/outbox/sidekiq5_format"
3
+ require "ruby_event_store/outbox/sidekiq_producer"
5
4
 
6
5
  module RubyEventStore
7
6
  module Outbox
8
7
  class SidekiqScheduler
8
+ def initialize
9
+ @sidekiq_producer = SidekiqProducer.new
10
+ end
11
+
9
12
  def call(klass, serialized_event)
10
- sidekiq_client = Sidekiq::Client.new(Sidekiq.redis_pool)
11
- item = {
12
- 'class' => klass,
13
- 'args' => [serialized_event.to_h],
14
- }
15
- normalized_item = sidekiq_client.__send__(:normalize_item, item)
16
- payload = sidekiq_client.__send__(:process_single, normalized_item.fetch('class'), normalized_item)
17
- if payload
18
- Record.create!(
19
- format: SIDEKIQ5_FORMAT,
20
- split_key: payload.fetch('queue'),
21
- payload: payload.to_json
22
- )
23
- end
13
+ sidekiq_producer.call(klass, [serialized_event.to_h])
24
14
  end
25
15
 
26
16
  def verify(subscriber)
27
17
  Class === subscriber && subscriber.respond_to?(:through_outbox?) && subscriber.through_outbox?
28
18
  end
19
+
20
+ private
21
+ attr_reader :sidekiq_producer
29
22
  end
30
23
  end
31
24
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RubyEventStore
4
4
  module Outbox
5
- VERSION = "0.0.10"
5
+ VERSION = "0.0.15"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,27 +1,27 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_event_store-outbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.10
4
+ version: 0.0.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arkency
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-26 00:00:00.000000000 Z
11
+ date: 2020-08-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby_event_store
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: 1.0.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - '='
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 1.0.0
27
27
  - !ruby/object:Gem::Dependency
@@ -53,12 +53,16 @@ files:
53
53
  - lib/ruby_event_store/outbox.rb
54
54
  - lib/ruby_event_store/outbox/cli.rb
55
55
  - lib/ruby_event_store/outbox/consumer.rb
56
+ - lib/ruby_event_store/outbox/consumer_process.rb
57
+ - lib/ruby_event_store/outbox/fetch_specification.rb
56
58
  - lib/ruby_event_store/outbox/metrics.rb
57
59
  - lib/ruby_event_store/outbox/metrics/influx.rb
58
60
  - lib/ruby_event_store/outbox/metrics/null.rb
59
61
  - lib/ruby_event_store/outbox/record.rb
60
62
  - lib/ruby_event_store/outbox/sidekiq5_format.rb
61
63
  - lib/ruby_event_store/outbox/sidekiq_message_handler.rb
64
+ - lib/ruby_event_store/outbox/sidekiq_processor.rb
65
+ - lib/ruby_event_store/outbox/sidekiq_producer.rb
62
66
  - lib/ruby_event_store/outbox/sidekiq_scheduler.rb
63
67
  - lib/ruby_event_store/outbox/version.rb
64
68
  homepage: https://railseventstore.org