ruby_event_store-outbox 0.0.15 → 0.0.16
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +19 -2
- data/lib/generators/ruby_event_store/outbox/migration_generator.rb +0 -5
- data/lib/ruby_event_store/outbox.rb +1 -1
- data/lib/ruby_event_store/outbox/cleanup_strategies/clean_old_enqueued.rb +19 -0
- data/lib/ruby_event_store/outbox/cleanup_strategies/none.rb +13 -0
- data/lib/ruby_event_store/outbox/cli.rb +8 -3
- data/lib/ruby_event_store/outbox/consumer.rb +29 -16
- data/lib/ruby_event_store/outbox/fetch_specification.rb +18 -0
- data/lib/ruby_event_store/outbox/repository.rb +153 -0
- data/lib/ruby_event_store/outbox/sidekiq_producer.rb +5 -2
- data/lib/ruby_event_store/outbox/sidekiq_scheduler.rb +2 -2
- data/lib/ruby_event_store/outbox/version.rb +1 -1
- metadata +7 -5
- data/lib/ruby_event_store/outbox/record.rb +0 -103
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '008b96c4b43e3cbfc23d0aa8c9ebb160f0adbfcfc82a976967f1c56338346cc0'
|
4
|
+
data.tar.gz: e65157c0638ecec0a4967a251e820979d18eb92e55212f107be95c23d5a4315b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f0181744b8c621bc1f0b20d6d4a65ac779db7ba48a80c9f1a02f39208762aa82e5579e745f20cd35f2b54992876065f803e04e24c1c239abe22641f927327070
|
7
|
+
data.tar.gz: 453ce565584436b1b7dd145c738a0d50e95821cb8b8f5d9c00f0d70d2a8ea88e585402de534388459d04853d293c0bcbf7027285d31bbd00aeaef09471cf666d
|
data/README.md
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
# Ruby Event Store Outbox
|
2
2
|
|
3
|
-
|
3
|
+
![Ruby Event Store Outbox](https://github.com/RailsEventStore/rails_event_store/workflows/ruby_event_store-outbox/badge.svg)
|
4
4
|
|
5
|
+
**Experimental feature of RES ecosystem.**
|
6
|
+
|
7
|
+
This repository includes a process and a Rails Event Store scheduler, which can be used to transactionally enqueue background jobs into your background jobs tool of choice. The scheduler included in this repo adds the jobs into the RDBMS into specific table instead of redis inside your transaction, and the process is enqueuing the jobs from that table to the background jobs tool.
|
5
8
|
|
6
9
|
## Installation (app)
|
7
10
|
|
@@ -35,7 +38,21 @@ end
|
|
35
38
|
Run following process in any way you prefer:
|
36
39
|
|
37
40
|
```
|
38
|
-
res_outbox --database-url="mysql2://root@0.0.0.0:3306/my_database" --redis-url="redis://localhost:6379/0" --log-level=info
|
41
|
+
res_outbox --database-url="mysql2://root@0.0.0.0:3306/my_database" --redis-url="redis://localhost:6379/0" --log-level=info --split-keys=sidekiq_queue1,sidekiq_queue2
|
42
|
+
```
|
43
|
+
|
44
|
+
It is possible to run as many instances as you prefer, but it does not make sense to run more instances than there are different split keys (sidekiq queues), as one process is operating at one moment only one split key.
|
45
|
+
|
46
|
+
### Metrics
|
47
|
+
|
48
|
+
It is possible for the outbox process to send metrics to InfluxDB. In order to do that, specify a `--metrics-url` parameter, for example:
|
49
|
+
|
50
|
+
```
|
51
|
+
res_outbox --database-url="mysql2://root@0.0.0.0:3306/my_database" \
|
52
|
+
--redis-url="redis://localhost:6379/0" \
|
53
|
+
--log-level=info \
|
54
|
+
--split-keys=sidekiq_queue1,sidekiq_queue2 \
|
55
|
+
--metrics-url=http://user:password@localhost:8086/dbname"
|
39
56
|
```
|
40
57
|
|
41
58
|
|
@@ -6,7 +6,7 @@ module RubyEventStore
|
|
6
6
|
end
|
7
7
|
|
8
8
|
require_relative 'outbox/fetch_specification'
|
9
|
-
require_relative 'outbox/
|
9
|
+
require_relative 'outbox/repository'
|
10
10
|
require_relative 'outbox/sidekiq_scheduler'
|
11
11
|
require_relative 'outbox/consumer'
|
12
12
|
require_relative 'outbox/version'
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module RubyEventStore
|
2
|
+
module Outbox
|
3
|
+
module CleanupStrategies
|
4
|
+
class CleanOldEnqueued
|
5
|
+
def initialize(repository, duration)
|
6
|
+
@repository = repository
|
7
|
+
@duration = duration
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(fetch_specification)
|
11
|
+
repository.delete_enqueued_older_than(fetch_specification, duration)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
attr_reader :repository, :duration
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -6,11 +6,11 @@ require "ruby_event_store/outbox/metrics"
|
|
6
6
|
module RubyEventStore
|
7
7
|
module Outbox
|
8
8
|
class CLI
|
9
|
-
Options = Struct.new(:database_url, :redis_url, :log_level, :split_keys, :message_format, :batch_size, :metrics_url)
|
9
|
+
Options = Struct.new(:database_url, :redis_url, :log_level, :split_keys, :message_format, :batch_size, :metrics_url, :cleanup_strategy)
|
10
10
|
|
11
11
|
class Parser
|
12
12
|
def self.parse(argv)
|
13
|
-
options = Options.new(nil, nil, :warn, nil, nil, 100)
|
13
|
+
options = Options.new(nil, nil, :warn, nil, nil, 100, nil, :none)
|
14
14
|
OptionParser.new do |option_parser|
|
15
15
|
option_parser.banner = "Usage: res_outbox [options]"
|
16
16
|
|
@@ -42,6 +42,10 @@ module RubyEventStore
|
|
42
42
|
options.metrics_url = metrics_url
|
43
43
|
end
|
44
44
|
|
45
|
+
option_parser.on("--cleanup CLEANUP_STRATEGY", "A strategy for cleaning old records. One of: none or iso8601 duration format how old enqueued records should be removed") do |cleanup_strategy|
|
46
|
+
options.cleanup_strategy = cleanup_strategy
|
47
|
+
end
|
48
|
+
|
45
49
|
option_parser.on_tail("--version", "Show version") do
|
46
50
|
puts VERSION
|
47
51
|
exit
|
@@ -67,11 +71,12 @@ module RubyEventStore
|
|
67
71
|
batch_size: options.batch_size,
|
68
72
|
database_url: options.database_url,
|
69
73
|
redis_url: options.redis_url,
|
74
|
+
cleanup: options.cleanup_strategy,
|
70
75
|
)
|
71
76
|
metrics = Metrics.from_url(options.metrics_url)
|
72
77
|
outbox_consumer = RubyEventStore::Outbox::Consumer.new(
|
73
78
|
consumer_uuid,
|
74
|
-
|
79
|
+
consumer_configuration,
|
75
80
|
logger: logger,
|
76
81
|
metrics: metrics,
|
77
82
|
)
|
@@ -1,10 +1,12 @@
|
|
1
1
|
require "logger"
|
2
2
|
require "redis"
|
3
3
|
require "active_record"
|
4
|
-
require "ruby_event_store/outbox/
|
4
|
+
require "ruby_event_store/outbox/repository"
|
5
5
|
require "ruby_event_store/outbox/sidekiq5_format"
|
6
6
|
require "ruby_event_store/outbox/sidekiq_processor"
|
7
7
|
require "ruby_event_store/outbox/fetch_specification"
|
8
|
+
require "ruby_event_store/outbox/cleanup_strategies/none"
|
9
|
+
require "ruby_event_store/outbox/cleanup_strategies/clean_old_enqueued"
|
8
10
|
|
9
11
|
module RubyEventStore
|
10
12
|
module Outbox
|
@@ -18,13 +20,15 @@ module RubyEventStore
|
|
18
20
|
message_format:,
|
19
21
|
batch_size:,
|
20
22
|
database_url:,
|
21
|
-
redis_url
|
23
|
+
redis_url:,
|
24
|
+
cleanup:
|
22
25
|
)
|
23
26
|
@split_keys = split_keys
|
24
27
|
@message_format = message_format
|
25
28
|
@batch_size = batch_size || 100
|
26
29
|
@database_url = database_url
|
27
30
|
@redis_url = redis_url
|
31
|
+
@cleanup = cleanup
|
28
32
|
freeze
|
29
33
|
end
|
30
34
|
|
@@ -35,10 +39,11 @@ module RubyEventStore
|
|
35
39
|
batch_size: overriden_options.fetch(:batch_size, batch_size),
|
36
40
|
database_url: overriden_options.fetch(:database_url, database_url),
|
37
41
|
redis_url: overriden_options.fetch(:redis_url, redis_url),
|
42
|
+
cleanup: overriden_options.fetch(:cleanup, cleanup)
|
38
43
|
)
|
39
44
|
end
|
40
45
|
|
41
|
-
attr_reader :split_keys, :message_format, :batch_size, :database_url, :redis_url
|
46
|
+
attr_reader :split_keys, :message_format, :batch_size, :database_url, :redis_url, :cleanup
|
42
47
|
end
|
43
48
|
|
44
49
|
def initialize(consumer_uuid, configuration, clock: Time, logger:, metrics:)
|
@@ -48,16 +53,20 @@ module RubyEventStore
|
|
48
53
|
@metrics = metrics
|
49
54
|
@batch_size = configuration.batch_size
|
50
55
|
@consumer_uuid = consumer_uuid
|
51
|
-
ActiveRecord::Base.establish_connection(configuration.database_url) unless ActiveRecord::Base.connected?
|
52
|
-
if ActiveRecord::Base.connection.adapter_name == "Mysql2"
|
53
|
-
ActiveRecord::Base.connection.execute("SET SESSION innodb_lock_wait_timeout = 1;")
|
54
|
-
end
|
55
56
|
|
56
57
|
raise "Unknown format" if configuration.message_format != SIDEKIQ5_FORMAT
|
57
58
|
@processor = SidekiqProcessor.new(Redis.new(url: configuration.redis_url))
|
58
59
|
|
59
60
|
@gracefully_shutting_down = false
|
60
61
|
prepare_traps
|
62
|
+
|
63
|
+
@repository = Repository.new(configuration.database_url)
|
64
|
+
@cleanup_strategy = case configuration.cleanup
|
65
|
+
when :none
|
66
|
+
CleanupStrategies::None.new
|
67
|
+
else
|
68
|
+
CleanupStrategies::CleanOldEnqueued.new(repository, ActiveSupport::Duration.parse(configuration.cleanup))
|
69
|
+
end
|
61
70
|
end
|
62
71
|
|
63
72
|
def init
|
@@ -105,7 +114,7 @@ module RubyEventStore
|
|
105
114
|
now = @clock.now.utc
|
106
115
|
processor.process(record, now)
|
107
116
|
|
108
|
-
|
117
|
+
repository.mark_as_enqueued(record, now)
|
109
118
|
something_processed |= true
|
110
119
|
updated_record_ids << record.id
|
111
120
|
rescue => e
|
@@ -124,8 +133,8 @@ module RubyEventStore
|
|
124
133
|
|
125
134
|
logger.info "Sent #{updated_record_ids.size} messages from outbox table"
|
126
135
|
|
127
|
-
|
128
|
-
break unless
|
136
|
+
refresh_successful = refresh_lock_for_process(obtained_lock)
|
137
|
+
break unless refresh_successful
|
129
138
|
end
|
130
139
|
|
131
140
|
metrics.write_point_queue(
|
@@ -136,16 +145,18 @@ module RubyEventStore
|
|
136
145
|
|
137
146
|
release_lock_for_process(fetch_specification)
|
138
147
|
|
148
|
+
cleanup_strategy.call(fetch_specification)
|
149
|
+
|
139
150
|
processor.after_batch
|
140
151
|
|
141
152
|
something_processed
|
142
153
|
end
|
143
154
|
|
144
155
|
private
|
145
|
-
attr_reader :split_keys, :logger, :batch_size, :metrics, :processor, :consumer_uuid
|
156
|
+
attr_reader :split_keys, :logger, :batch_size, :metrics, :processor, :consumer_uuid, :repository, :cleanup_strategy
|
146
157
|
|
147
158
|
def obtain_lock_for_process(fetch_specification)
|
148
|
-
result =
|
159
|
+
result = repository.obtain_lock_for_process(fetch_specification, consumer_uuid, clock: @clock)
|
149
160
|
case result
|
150
161
|
when :deadlocked
|
151
162
|
logger.warn "Obtaining lock for split_key '#{fetch_specification.split_key}' failed (deadlock)"
|
@@ -165,7 +176,7 @@ module RubyEventStore
|
|
165
176
|
end
|
166
177
|
|
167
178
|
def release_lock_for_process(fetch_specification)
|
168
|
-
result =
|
179
|
+
result = repository.release_lock_for_process(fetch_specification, consumer_uuid)
|
169
180
|
case result
|
170
181
|
when :ok
|
171
182
|
when :deadlocked
|
@@ -185,6 +196,8 @@ module RubyEventStore
|
|
185
196
|
def refresh_lock_for_process(lock)
|
186
197
|
result = lock.refresh(clock: @clock)
|
187
198
|
case result
|
199
|
+
when :ok
|
200
|
+
return true
|
188
201
|
when :deadlocked
|
189
202
|
logger.warn "Refreshing lock for split_key '#{lock.split_key}' failed (deadlock)"
|
190
203
|
metrics.write_operation_result("refresh", "deadlocked")
|
@@ -198,7 +211,7 @@ module RubyEventStore
|
|
198
211
|
metrics.write_operation_result("refresh", "stolen")
|
199
212
|
return false
|
200
213
|
else
|
201
|
-
|
214
|
+
raise "Unexpected result #{result}"
|
202
215
|
end
|
203
216
|
end
|
204
217
|
|
@@ -216,11 +229,11 @@ module RubyEventStore
|
|
216
229
|
end
|
217
230
|
|
218
231
|
def retrieve_batch(fetch_specification)
|
219
|
-
|
232
|
+
repository.retrieve_batch(fetch_specification, batch_size)
|
220
233
|
end
|
221
234
|
|
222
235
|
def get_remaining_count(fetch_specification)
|
223
|
-
|
236
|
+
repository.get_remaining_count(fetch_specification)
|
224
237
|
end
|
225
238
|
end
|
226
239
|
end
|
@@ -8,6 +8,24 @@ module RubyEventStore
|
|
8
8
|
end
|
9
9
|
|
10
10
|
attr_reader :message_format, :split_key
|
11
|
+
|
12
|
+
def ==(other)
|
13
|
+
other.instance_of?(self.class) &&
|
14
|
+
other.message_format.eql?(message_format) &&
|
15
|
+
other.split_key.eql?(split_key)
|
16
|
+
end
|
17
|
+
|
18
|
+
BIG_VALUE = 0b111111100100010000010010110010101011011101110101001100100110000
|
19
|
+
|
20
|
+
def hash
|
21
|
+
[
|
22
|
+
self.class,
|
23
|
+
message_format,
|
24
|
+
split_key,
|
25
|
+
].hash ^ BIG_VALUE
|
26
|
+
end
|
27
|
+
|
28
|
+
alias_method :eql?, :==
|
11
29
|
end
|
12
30
|
end
|
13
31
|
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
require 'active_support/core_ext/numeric/time.rb'
|
5
|
+
|
6
|
+
module RubyEventStore
|
7
|
+
module Outbox
|
8
|
+
class Repository
|
9
|
+
RECENTLY_LOCKED_DURATION = 10.minutes
|
10
|
+
|
11
|
+
class Record < ::ActiveRecord::Base
|
12
|
+
self.primary_key = :id
|
13
|
+
self.table_name = 'event_store_outbox'
|
14
|
+
|
15
|
+
def self.remaining_for(fetch_specification)
|
16
|
+
where(format: fetch_specification.message_format, split_key: fetch_specification.split_key, enqueued_at: nil)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.for_fetch_specification(fetch_specification)
|
20
|
+
where(format: fetch_specification.message_format, split_key: fetch_specification.split_key)
|
21
|
+
end
|
22
|
+
|
23
|
+
def hash_payload
|
24
|
+
JSON.parse(payload).deep_symbolize_keys
|
25
|
+
end
|
26
|
+
|
27
|
+
def enqueued?
|
28
|
+
!enqueued_at.nil?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Lock < ::ActiveRecord::Base
|
33
|
+
self.table_name = 'event_store_outbox_locks'
|
34
|
+
|
35
|
+
def self.obtain(fetch_specification, process_uuid, clock:)
|
36
|
+
transaction do
|
37
|
+
l = get_lock_record(fetch_specification)
|
38
|
+
|
39
|
+
if l.recently_locked?
|
40
|
+
:taken
|
41
|
+
else
|
42
|
+
l.update!(
|
43
|
+
locked_by: process_uuid,
|
44
|
+
locked_at: clock.now,
|
45
|
+
)
|
46
|
+
l
|
47
|
+
end
|
48
|
+
end
|
49
|
+
rescue ActiveRecord::Deadlocked
|
50
|
+
:deadlocked
|
51
|
+
rescue ActiveRecord::LockWaitTimeout
|
52
|
+
:lock_timeout
|
53
|
+
end
|
54
|
+
|
55
|
+
def refresh(clock:)
|
56
|
+
transaction do
|
57
|
+
current_process_uuid = locked_by
|
58
|
+
lock!
|
59
|
+
if locked_by == current_process_uuid
|
60
|
+
update!(locked_at: clock.now)
|
61
|
+
:ok
|
62
|
+
else
|
63
|
+
:stolen
|
64
|
+
end
|
65
|
+
end
|
66
|
+
rescue ActiveRecord::Deadlocked
|
67
|
+
:deadlocked
|
68
|
+
rescue ActiveRecord::LockWaitTimeout
|
69
|
+
:lock_timeout
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.release(fetch_specification, process_uuid)
|
73
|
+
transaction do
|
74
|
+
l = get_lock_record(fetch_specification)
|
75
|
+
if !l.locked_by?(process_uuid)
|
76
|
+
:not_taken_by_this_process
|
77
|
+
else
|
78
|
+
l.update!(locked_by: nil, locked_at: nil)
|
79
|
+
:ok
|
80
|
+
end
|
81
|
+
end
|
82
|
+
rescue ActiveRecord::Deadlocked
|
83
|
+
:deadlocked
|
84
|
+
rescue ActiveRecord::LockWaitTimeout
|
85
|
+
:lock_timeout
|
86
|
+
end
|
87
|
+
|
88
|
+
def locked_by?(process_uuid)
|
89
|
+
locked_by.eql?(process_uuid)
|
90
|
+
end
|
91
|
+
|
92
|
+
def recently_locked?
|
93
|
+
locked_by && locked_at > RECENTLY_LOCKED_DURATION.ago
|
94
|
+
end
|
95
|
+
|
96
|
+
def fetch_specification
|
97
|
+
FetchSpecification.new(format, split_key)
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
def self.lock_for_split_key(fetch_specification)
|
102
|
+
lock.find_by(format: fetch_specification.message_format, split_key: fetch_specification.split_key)
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.get_lock_record(fetch_specification)
|
106
|
+
l = lock_for_split_key(fetch_specification)
|
107
|
+
if l.nil?
|
108
|
+
begin
|
109
|
+
l = create!(format: fetch_specification.message_format, split_key: fetch_specification.split_key)
|
110
|
+
rescue ActiveRecord::RecordNotUnique
|
111
|
+
l = lock_for_split_key(fetch_specification)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
l
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def initialize(database_url)
|
119
|
+
ActiveRecord::Base.establish_connection(database_url) unless ActiveRecord::Base.connected?
|
120
|
+
if ActiveRecord::Base.connection.adapter_name == "Mysql2"
|
121
|
+
ActiveRecord::Base.connection.execute("SET SESSION innodb_lock_wait_timeout = 1;")
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def retrieve_batch(fetch_specification, batch_size)
|
126
|
+
Record.remaining_for(fetch_specification).order("id ASC").limit(batch_size).to_a
|
127
|
+
end
|
128
|
+
|
129
|
+
def get_remaining_count(fetch_specification)
|
130
|
+
Record.remaining_for(fetch_specification).count
|
131
|
+
end
|
132
|
+
|
133
|
+
def obtain_lock_for_process(fetch_specification, process_uuid, clock:)
|
134
|
+
Lock.obtain(fetch_specification, process_uuid, clock: clock)
|
135
|
+
end
|
136
|
+
|
137
|
+
def release_lock_for_process(fetch_specification, process_uuid)
|
138
|
+
Lock.release(fetch_specification, process_uuid)
|
139
|
+
end
|
140
|
+
|
141
|
+
def mark_as_enqueued(record, now)
|
142
|
+
record.update_column(:enqueued_at, now)
|
143
|
+
end
|
144
|
+
|
145
|
+
def delete_enqueued_older_than(fetch_specification, duration)
|
146
|
+
Record
|
147
|
+
.for_fetch_specification(fetch_specification)
|
148
|
+
.where("enqueued_at < ?", duration.ago)
|
149
|
+
.delete_all
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'sidekiq'
|
4
|
-
require "ruby_event_store/outbox/
|
4
|
+
require "ruby_event_store/outbox/repository"
|
5
5
|
|
6
6
|
module RubyEventStore
|
7
7
|
module Outbox
|
@@ -15,13 +15,16 @@ module RubyEventStore
|
|
15
15
|
normalized_item = sidekiq_client.__send__(:normalize_item, item)
|
16
16
|
payload = sidekiq_client.__send__(:process_single, normalized_item.fetch('class'), normalized_item)
|
17
17
|
if payload
|
18
|
-
Record.create!(
|
18
|
+
Repository::Record.create!(
|
19
19
|
format: SIDEKIQ5_FORMAT,
|
20
20
|
split_key: payload.fetch('queue'),
|
21
21
|
payload: payload.to_json
|
22
22
|
)
|
23
23
|
end
|
24
24
|
end
|
25
|
+
|
26
|
+
private
|
27
|
+
attr_reader :repository
|
25
28
|
end
|
26
29
|
end
|
27
30
|
end
|
@@ -9,8 +9,8 @@ module RubyEventStore
|
|
9
9
|
@sidekiq_producer = SidekiqProducer.new
|
10
10
|
end
|
11
11
|
|
12
|
-
def call(klass,
|
13
|
-
sidekiq_producer.call(klass, [
|
12
|
+
def call(klass, serialized_record)
|
13
|
+
sidekiq_producer.call(klass, [serialized_record.to_h])
|
14
14
|
end
|
15
15
|
|
16
16
|
def verify(subscriber)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_event_store-outbox
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.16
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Arkency
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-01-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ruby_event_store
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '5.2'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '5.2'
|
41
41
|
description:
|
42
42
|
email:
|
43
43
|
- dev@arkency.com
|
@@ -51,6 +51,8 @@ files:
|
|
51
51
|
- lib/generators/ruby_event_store/outbox/migration_generator.rb
|
52
52
|
- lib/generators/ruby_event_store/outbox/templates/create_event_store_outbox_template.rb
|
53
53
|
- lib/ruby_event_store/outbox.rb
|
54
|
+
- lib/ruby_event_store/outbox/cleanup_strategies/clean_old_enqueued.rb
|
55
|
+
- lib/ruby_event_store/outbox/cleanup_strategies/none.rb
|
54
56
|
- lib/ruby_event_store/outbox/cli.rb
|
55
57
|
- lib/ruby_event_store/outbox/consumer.rb
|
56
58
|
- lib/ruby_event_store/outbox/consumer_process.rb
|
@@ -58,7 +60,7 @@ files:
|
|
58
60
|
- lib/ruby_event_store/outbox/metrics.rb
|
59
61
|
- lib/ruby_event_store/outbox/metrics/influx.rb
|
60
62
|
- lib/ruby_event_store/outbox/metrics/null.rb
|
61
|
-
- lib/ruby_event_store/outbox/
|
63
|
+
- lib/ruby_event_store/outbox/repository.rb
|
62
64
|
- lib/ruby_event_store/outbox/sidekiq5_format.rb
|
63
65
|
- lib/ruby_event_store/outbox/sidekiq_message_handler.rb
|
64
66
|
- lib/ruby_event_store/outbox/sidekiq_processor.rb
|
@@ -1,103 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'active_record'
|
4
|
-
|
5
|
-
module RubyEventStore
|
6
|
-
module Outbox
|
7
|
-
class Record < ::ActiveRecord::Base
|
8
|
-
self.primary_key = :id
|
9
|
-
self.table_name = 'event_store_outbox'
|
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
|
-
|
15
|
-
def hash_payload
|
16
|
-
JSON.parse(payload).deep_symbolize_keys
|
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
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|