ruby_event_store-outbox 0.0.15 → 0.0.19
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +37 -2
- data/lib/generators/ruby_event_store/outbox/migration_generator.rb +0 -5
- data/lib/ruby_event_store/outbox/cleanup_strategies/clean_old_enqueued.rb +19 -0
- data/lib/ruby_event_store/outbox/cleanup_strategies/none.rb +14 -0
- data/lib/ruby_event_store/outbox/cli.rb +34 -13
- data/lib/ruby_event_store/outbox/consumer.rb +53 -28
- data/lib/ruby_event_store/outbox/fetch_specification.rb +18 -0
- data/lib/ruby_event_store/outbox/legacy_sidekiq_scheduler.rb +24 -0
- data/lib/ruby_event_store/outbox/metrics/influx.rb +0 -3
- data/lib/ruby_event_store/outbox/metrics/test.rb +24 -0
- data/lib/ruby_event_store/outbox/metrics.rb +2 -2
- data/lib/ruby_event_store/outbox/repository.rb +158 -0
- data/lib/ruby_event_store/outbox/sidekiq_processor.rb +1 -1
- data/lib/ruby_event_store/outbox/sidekiq_producer.rb +6 -3
- data/lib/ruby_event_store/outbox/sidekiq_scheduler.rb +6 -5
- data/lib/ruby_event_store/outbox/version.rb +1 -1
- data/lib/ruby_event_store/outbox.rb +2 -2
- metadata +20 -18
- data/lib/ruby_event_store/outbox/consumer_process.rb +0 -6
- data/lib/ruby_event_store/outbox/record.rb +0 -103
- data/lib/ruby_event_store/outbox/sidekiq_message_handler.rb +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b78f20ac9a815274e65c78322746557d961af6b05982d2ebd828f65186002982
|
4
|
+
data.tar.gz: 4a88357afd1a360398d8c1b3fe1fe6081f6e5c05169ba892d16cd701dfd38b53
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3d82d744889b4f595ecfb9c16a6ab57817fd3d652e3c30cbe4f4367bf794de41245b82c35ae02e01e73907ae745f829b1fe3486378a8b0685eaed47d25a359fd
|
7
|
+
data.tar.gz: '058fec9678d6d6db03bc58c1e982ebce51547714dccf657ab5dd2161f2de55af0d07cd44b15e286e9186626c3cb4ad087fd100b118f235fdd7650163ca2a9f68'
|
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
|
|
@@ -11,6 +14,12 @@ Add to your gemfile in application:
|
|
11
14
|
gem "ruby_event_store-outbox"
|
12
15
|
```
|
13
16
|
|
17
|
+
Generate and execute the migration adding necessary tables. If it's needed, change the type of the `payload` column to `mediumbinary` or `longbinary`.
|
18
|
+
|
19
|
+
```
|
20
|
+
bin/rails generate ruby_event_store:outbox:migration
|
21
|
+
```
|
22
|
+
|
14
23
|
In your event store configuration, as a dispatcher use `RubyEventStore::ImmediateAsyncDispatcher` with `RubyEventStore::Outbox::SidekiqScheduler`, for example:
|
15
24
|
|
16
25
|
```ruby
|
@@ -35,10 +44,36 @@ end
|
|
35
44
|
Run following process in any way you prefer:
|
36
45
|
|
37
46
|
```
|
38
|
-
res_outbox
|
47
|
+
res_outbox \
|
48
|
+
--database-url="mysql2://root@0.0.0.0:3306/my_database" \
|
49
|
+
--redis-url="redis://localhost:6379/0" \
|
50
|
+
--log-level=info \
|
51
|
+
--split-keys=sidekiq_queue1,sidekiq_queue2
|
52
|
+
```
|
53
|
+
|
54
|
+
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.
|
55
|
+
|
56
|
+
### Metrics
|
57
|
+
|
58
|
+
It is possible for the outbox process to send metrics to InfluxDB (this requires influxdb gem in version at least 0.8.1). In order to do that, specify a `--metrics-url` parameter, for example:
|
59
|
+
|
60
|
+
```
|
61
|
+
res_outbox --database-url="mysql2://root@0.0.0.0:3306/my_database" \
|
62
|
+
--redis-url="redis://localhost:6379/0" \
|
63
|
+
--log-level=info \
|
64
|
+
--split-keys=sidekiq_queue1,sidekiq_queue2 \
|
65
|
+
--metrics-url=http://user:password@localhost:8086/dbname"
|
39
66
|
```
|
40
67
|
|
41
68
|
|
42
69
|
## Contributing
|
43
70
|
|
44
71
|
Bug reports and pull requests are welcome on GitHub at https://github.com/RailsEventStore/rails_event_store.
|
72
|
+
|
73
|
+
## Releasing
|
74
|
+
|
75
|
+
1. Bump version
|
76
|
+
2. `make build`
|
77
|
+
3. `make push`
|
78
|
+
4. `make docker-build`
|
79
|
+
5. `make docker-push`
|
@@ -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
|
@@ -1,47 +1,66 @@
|
|
1
1
|
require "optparse"
|
2
|
-
|
3
|
-
|
4
|
-
|
2
|
+
require_relative "version"
|
3
|
+
require_relative "consumer"
|
4
|
+
require_relative "metrics"
|
5
5
|
|
6
6
|
module RubyEventStore
|
7
7
|
module Outbox
|
8
8
|
class CLI
|
9
|
-
|
9
|
+
DEFAULTS = {
|
10
|
+
database_url: nil,
|
11
|
+
redis_url: nil,
|
12
|
+
log_level: :warn,
|
13
|
+
split_keys: nil,
|
14
|
+
message_format: 'sidekiq5',
|
15
|
+
batch_size: 100,
|
16
|
+
metrics_url: nil,
|
17
|
+
cleanup_strategy: :none,
|
18
|
+
sleep_on_empty: 0.5
|
19
|
+
}
|
20
|
+
Options = Struct.new(*DEFAULTS.keys)
|
10
21
|
|
11
22
|
class Parser
|
12
23
|
def self.parse(argv)
|
13
|
-
options = Options.new(
|
24
|
+
options = Options.new(*DEFAULTS.values)
|
14
25
|
OptionParser.new do |option_parser|
|
15
26
|
option_parser.banner = "Usage: res_outbox [options]"
|
16
27
|
|
17
|
-
option_parser.on("--database-url
|
28
|
+
option_parser.on("--database-url=DATABASE_URL", "Database where outbox table is stored") do |database_url|
|
18
29
|
options.database_url = database_url
|
19
30
|
end
|
20
31
|
|
21
|
-
option_parser.on("--redis-url
|
32
|
+
option_parser.on("--redis-url=REDIS_URL", "URL to redis database") do |redis_url|
|
22
33
|
options.redis_url = redis_url
|
23
34
|
end
|
24
35
|
|
25
|
-
option_parser.on("--log-level
|
36
|
+
option_parser.on("--log-level=LOG_LEVEL", [:fatal, :error, :warn, :info, :debug], "Logging level, one of: fatal, error, warn, info, debug. Default: warn") do |log_level|
|
26
37
|
options.log_level = log_level.to_sym
|
27
38
|
end
|
28
39
|
|
29
|
-
option_parser.on("--message-format
|
40
|
+
option_parser.on("--message-format=FORMAT", ["sidekiq5"], "Message format, supported: sidekiq5. Default: sidekiq5") do |message_format|
|
30
41
|
options.message_format = message_format
|
31
42
|
end
|
32
43
|
|
33
|
-
option_parser.on("--split-keys=
|
44
|
+
option_parser.on("--split-keys=SPLIT_KEYS", Array, "Split keys which should be handled, all if not specified") do |split_keys|
|
34
45
|
options.split_keys = split_keys if !split_keys.empty?
|
35
46
|
end
|
36
47
|
|
37
|
-
option_parser.on("--batch-size
|
48
|
+
option_parser.on("--batch-size=BATCH_SIZE", Integer, "Amount of records fetched in one fetch. Bigger value means more duplicated messages when network problems occur. Default: 100") do |batch_size|
|
38
49
|
options.batch_size = batch_size
|
39
50
|
end
|
40
51
|
|
41
|
-
option_parser.on("--metrics-url
|
52
|
+
option_parser.on("--metrics-url=METRICS_URL", "URI to metrics collector, optional") do |metrics_url|
|
42
53
|
options.metrics_url = metrics_url
|
43
54
|
end
|
44
55
|
|
56
|
+
option_parser.on("--cleanup=STRATEGY", "A strategy for cleaning old records. One of: none or iso8601 duration format how old enqueued records should be removed. Default: none") do |cleanup_strategy|
|
57
|
+
options.cleanup_strategy = cleanup_strategy
|
58
|
+
end
|
59
|
+
|
60
|
+
option_parser.on("--sleep-on-empty=SLEEP_TIME", Float, "How long to sleep before next check when there was nothing to do. Default: 0.5") do |sleep_on_empty|
|
61
|
+
options.sleep_on_empty = sleep_on_empty
|
62
|
+
end
|
63
|
+
|
45
64
|
option_parser.on_tail("--version", "Show version") do
|
46
65
|
puts VERSION
|
47
66
|
exit
|
@@ -67,11 +86,13 @@ module RubyEventStore
|
|
67
86
|
batch_size: options.batch_size,
|
68
87
|
database_url: options.database_url,
|
69
88
|
redis_url: options.redis_url,
|
89
|
+
cleanup: options.cleanup_strategy,
|
90
|
+
sleep_on_empty: options.sleep_on_empty,
|
70
91
|
)
|
71
92
|
metrics = Metrics.from_url(options.metrics_url)
|
72
93
|
outbox_consumer = RubyEventStore::Outbox::Consumer.new(
|
73
94
|
consumer_uuid,
|
74
|
-
|
95
|
+
consumer_configuration,
|
75
96
|
logger: logger,
|
76
97
|
metrics: metrics,
|
77
98
|
)
|
@@ -1,15 +1,16 @@
|
|
1
1
|
require "logger"
|
2
2
|
require "redis"
|
3
3
|
require "active_record"
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
require_relative "repository"
|
5
|
+
require_relative "sidekiq5_format"
|
6
|
+
require_relative "sidekiq_processor"
|
7
|
+
require_relative "fetch_specification"
|
8
|
+
require_relative "cleanup_strategies/none"
|
9
|
+
require_relative "cleanup_strategies/clean_old_enqueued"
|
8
10
|
|
9
11
|
module RubyEventStore
|
10
12
|
module Outbox
|
11
13
|
class Consumer
|
12
|
-
SLEEP_TIME_WHEN_NOTHING_TO_DO = 0.5
|
13
14
|
MAXIMUM_BATCH_FETCHES_IN_ONE_LOCK = 10
|
14
15
|
|
15
16
|
class Configuration
|
@@ -18,13 +19,17 @@ module RubyEventStore
|
|
18
19
|
message_format:,
|
19
20
|
batch_size:,
|
20
21
|
database_url:,
|
21
|
-
redis_url
|
22
|
+
redis_url:,
|
23
|
+
cleanup:,
|
24
|
+
sleep_on_empty:
|
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
|
32
|
+
@sleep_on_empty = sleep_on_empty
|
28
33
|
freeze
|
29
34
|
end
|
30
35
|
|
@@ -35,10 +40,12 @@ module RubyEventStore
|
|
35
40
|
batch_size: overriden_options.fetch(:batch_size, batch_size),
|
36
41
|
database_url: overriden_options.fetch(:database_url, database_url),
|
37
42
|
redis_url: overriden_options.fetch(:redis_url, redis_url),
|
43
|
+
cleanup: overriden_options.fetch(:cleanup, cleanup),
|
44
|
+
sleep_on_empty: overriden_options.fetch(:sleep_on_empty, sleep_on_empty)
|
38
45
|
)
|
39
46
|
end
|
40
47
|
|
41
|
-
attr_reader :split_keys, :message_format, :batch_size, :database_url, :redis_url
|
48
|
+
attr_reader :split_keys, :message_format, :batch_size, :database_url, :redis_url, :cleanup, :sleep_on_empty
|
42
49
|
end
|
43
50
|
|
44
51
|
def initialize(consumer_uuid, configuration, clock: Time, logger:, metrics:)
|
@@ -47,17 +54,22 @@ module RubyEventStore
|
|
47
54
|
@logger = logger
|
48
55
|
@metrics = metrics
|
49
56
|
@batch_size = configuration.batch_size
|
57
|
+
@sleep_on_empty = configuration.sleep_on_empty
|
50
58
|
@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
59
|
|
56
60
|
raise "Unknown format" if configuration.message_format != SIDEKIQ5_FORMAT
|
57
61
|
@processor = SidekiqProcessor.new(Redis.new(url: configuration.redis_url))
|
58
62
|
|
59
63
|
@gracefully_shutting_down = false
|
60
64
|
prepare_traps
|
65
|
+
|
66
|
+
@repository = Repository.new(configuration.database_url)
|
67
|
+
@cleanup_strategy = case configuration.cleanup
|
68
|
+
when :none
|
69
|
+
CleanupStrategies::None.new
|
70
|
+
else
|
71
|
+
CleanupStrategies::CleanOldEnqueued.new(repository, ActiveSupport::Duration.parse(configuration.cleanup))
|
72
|
+
end
|
61
73
|
end
|
62
74
|
|
63
75
|
def init
|
@@ -70,7 +82,7 @@ module RubyEventStore
|
|
70
82
|
was_something_changed = one_loop
|
71
83
|
if !was_something_changed
|
72
84
|
STDOUT.flush
|
73
|
-
sleep
|
85
|
+
sleep sleep_on_empty
|
74
86
|
end
|
75
87
|
end
|
76
88
|
logger.info "Gracefully shutting down"
|
@@ -105,7 +117,7 @@ module RubyEventStore
|
|
105
117
|
now = @clock.now.utc
|
106
118
|
processor.process(record, now)
|
107
119
|
|
108
|
-
|
120
|
+
repository.mark_as_enqueued(record, now)
|
109
121
|
something_processed |= true
|
110
122
|
updated_record_ids << record.id
|
111
123
|
rescue => e
|
@@ -124,8 +136,8 @@ module RubyEventStore
|
|
124
136
|
|
125
137
|
logger.info "Sent #{updated_record_ids.size} messages from outbox table"
|
126
138
|
|
127
|
-
|
128
|
-
break unless
|
139
|
+
refresh_successful = refresh_lock_for_process(obtained_lock)
|
140
|
+
break unless refresh_successful
|
129
141
|
end
|
130
142
|
|
131
143
|
metrics.write_point_queue(
|
@@ -136,38 +148,39 @@ module RubyEventStore
|
|
136
148
|
|
137
149
|
release_lock_for_process(fetch_specification)
|
138
150
|
|
151
|
+
cleanup(fetch_specification)
|
152
|
+
|
139
153
|
processor.after_batch
|
140
154
|
|
141
155
|
something_processed
|
142
156
|
end
|
143
157
|
|
144
158
|
private
|
145
|
-
attr_reader :split_keys, :logger, :batch_size, :metrics, :processor, :consumer_uuid
|
159
|
+
attr_reader :split_keys, :logger, :batch_size, :metrics, :processor, :consumer_uuid, :repository, :cleanup_strategy, :sleep_on_empty
|
146
160
|
|
147
161
|
def obtain_lock_for_process(fetch_specification)
|
148
|
-
result =
|
162
|
+
result = repository.obtain_lock_for_process(fetch_specification, consumer_uuid, clock: @clock)
|
149
163
|
case result
|
150
164
|
when :deadlocked
|
151
165
|
logger.warn "Obtaining lock for split_key '#{fetch_specification.split_key}' failed (deadlock)"
|
152
166
|
metrics.write_operation_result("obtain", "deadlocked")
|
153
|
-
|
167
|
+
false
|
154
168
|
when :lock_timeout
|
155
169
|
logger.warn "Obtaining lock for split_key '#{fetch_specification.split_key}' failed (lock timeout)"
|
156
170
|
metrics.write_operation_result("obtain", "lock_timeout")
|
157
|
-
|
171
|
+
false
|
158
172
|
when :taken
|
159
173
|
logger.debug "Obtaining lock for split_key '#{fetch_specification.split_key}' unsuccessful (taken)"
|
160
174
|
metrics.write_operation_result("obtain", "taken")
|
161
|
-
|
175
|
+
false
|
162
176
|
else
|
163
|
-
|
177
|
+
result
|
164
178
|
end
|
165
179
|
end
|
166
180
|
|
167
181
|
def release_lock_for_process(fetch_specification)
|
168
|
-
result =
|
182
|
+
result = repository.release_lock_for_process(fetch_specification, consumer_uuid)
|
169
183
|
case result
|
170
|
-
when :ok
|
171
184
|
when :deadlocked
|
172
185
|
logger.warn "Releasing lock for split_key '#{fetch_specification.split_key}' failed (deadlock)"
|
173
186
|
metrics.write_operation_result("release", "deadlocked")
|
@@ -177,14 +190,14 @@ module RubyEventStore
|
|
177
190
|
when :not_taken_by_this_process
|
178
191
|
logger.debug "Releasing lock for split_key '#{fetch_specification.split_key}' failed (not taken by this process)"
|
179
192
|
metrics.write_operation_result("release", "not_taken_by_this_process")
|
180
|
-
else
|
181
|
-
raise "Unexpected result #{result}"
|
182
193
|
end
|
183
194
|
end
|
184
195
|
|
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,19 @@ module RubyEventStore
|
|
198
211
|
metrics.write_operation_result("refresh", "stolen")
|
199
212
|
return false
|
200
213
|
else
|
201
|
-
|
214
|
+
raise "Unexpected result #{result}"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def cleanup(fetch_specification)
|
219
|
+
result = cleanup_strategy.call(fetch_specification)
|
220
|
+
case result
|
221
|
+
when :deadlocked
|
222
|
+
logger.warn "Cleanup for split_key '#{fetch_specification.split_key}' failed (deadlock)"
|
223
|
+
metrics.write_operation_result("cleanup", "deadlocked")
|
224
|
+
when :lock_timeout
|
225
|
+
logger.warn "Cleanup for split_key '#{fetch_specification.split_key}' failed (lock timeout)"
|
226
|
+
metrics.write_operation_result("cleanup", "lock_timeout")
|
202
227
|
end
|
203
228
|
end
|
204
229
|
|
@@ -216,11 +241,11 @@ module RubyEventStore
|
|
216
241
|
end
|
217
242
|
|
218
243
|
def retrieve_batch(fetch_specification)
|
219
|
-
|
244
|
+
repository.retrieve_batch(fetch_specification, batch_size)
|
220
245
|
end
|
221
246
|
|
222
247
|
def get_remaining_count(fetch_specification)
|
223
|
-
|
248
|
+
repository.get_remaining_count(fetch_specification)
|
224
249
|
end
|
225
250
|
end
|
226
251
|
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,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "sidekiq_producer"
|
4
|
+
|
5
|
+
module RubyEventStore
|
6
|
+
module Outbox
|
7
|
+
class LegacySidekiqScheduler
|
8
|
+
def initialize
|
9
|
+
@sidekiq_producer = SidekiqProducer.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(klass, serialized_record)
|
13
|
+
sidekiq_producer.call(klass, [serialized_record])
|
14
|
+
end
|
15
|
+
|
16
|
+
def verify(subscriber)
|
17
|
+
Class === subscriber && subscriber.respond_to?(:through_outbox?) && subscriber.through_outbox?
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
attr_reader :sidekiq_producer
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -6,14 +6,11 @@ module RubyEventStore
|
|
6
6
|
class Influx
|
7
7
|
def initialize(url)
|
8
8
|
uri = URI.parse(url)
|
9
|
-
params = CGI.parse(uri.query || "")
|
10
9
|
options = {
|
11
10
|
url: url,
|
12
11
|
async: true,
|
13
12
|
time_precision: 'ns',
|
14
13
|
}
|
15
|
-
options[:username] = params.fetch("username").first if params.key?("username")
|
16
|
-
options[:password] = params.fetch("password").first if params.key?("password")
|
17
14
|
@influxdb_client = InfluxDB::Client.new(**options)
|
18
15
|
end
|
19
16
|
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'influxdb'
|
2
|
+
|
3
|
+
module RubyEventStore
|
4
|
+
module Outbox
|
5
|
+
module Metrics
|
6
|
+
class Test
|
7
|
+
def initialize
|
8
|
+
@operation_results = []
|
9
|
+
@queue_stats = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def write_operation_result(operation, result)
|
13
|
+
@operation_results << { operation: operation, result: result }
|
14
|
+
end
|
15
|
+
|
16
|
+
def write_point_queue(enqueued: 0, failed: 0, remaining: 0, format: nil, split_key: nil)
|
17
|
+
@queue_stats << { enqueued: enqueued, failed: failed, remaining: remaining, format: format, split_key: split_key}
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :operation_results, :queue_stats
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -3,10 +3,10 @@ module RubyEventStore
|
|
3
3
|
module Metrics
|
4
4
|
def self.from_url(metrics_url)
|
5
5
|
if metrics_url.nil?
|
6
|
-
|
6
|
+
require_relative "metrics/null"
|
7
7
|
Null.new
|
8
8
|
else
|
9
|
-
|
9
|
+
require_relative "metrics/influx"
|
10
10
|
Influx.new(metrics_url)
|
11
11
|
end
|
12
12
|
end
|
@@ -0,0 +1,158 @@
|
|
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?(clock: clock)
|
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?(clock:)
|
93
|
+
locked_by && locked_at > RECENTLY_LOCKED_DURATION.ago(clock.now)
|
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
|
+
:ok
|
151
|
+
rescue ActiveRecord::Deadlocked
|
152
|
+
:deadlocked
|
153
|
+
rescue ActiveRecord::LockWaitTimeout
|
154
|
+
:lock_timeout
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'sidekiq'
|
4
|
-
|
4
|
+
require_relative "repository"
|
5
5
|
|
6
6
|
module RubyEventStore
|
7
7
|
module Outbox
|
@@ -10,18 +10,21 @@ module RubyEventStore
|
|
10
10
|
sidekiq_client = Sidekiq::Client.new(Sidekiq.redis_pool)
|
11
11
|
item = {
|
12
12
|
'class' => klass,
|
13
|
-
'args' => args,
|
13
|
+
'args' => args.map(&:to_h),
|
14
14
|
}
|
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
|
@@ -1,16 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative "sidekiq_producer"
|
4
4
|
|
5
5
|
module RubyEventStore
|
6
6
|
module Outbox
|
7
7
|
class SidekiqScheduler
|
8
|
-
def initialize
|
8
|
+
def initialize(serializer: YAML)
|
9
|
+
@serializer = serializer
|
9
10
|
@sidekiq_producer = SidekiqProducer.new
|
10
11
|
end
|
11
12
|
|
12
|
-
def call(klass,
|
13
|
-
sidekiq_producer.call(klass, [
|
13
|
+
def call(klass, record)
|
14
|
+
sidekiq_producer.call(klass, [record.serialize(serializer)])
|
14
15
|
end
|
15
16
|
|
16
17
|
def verify(subscriber)
|
@@ -18,7 +19,7 @@ module RubyEventStore
|
|
18
19
|
end
|
19
20
|
|
20
21
|
private
|
21
|
-
attr_reader :sidekiq_producer
|
22
|
+
attr_reader :serializer, :sidekiq_producer
|
22
23
|
end
|
23
24
|
end
|
24
25
|
end
|
@@ -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
|
-
require_relative 'outbox/
|
11
|
+
require_relative 'outbox/legacy_sidekiq_scheduler'
|
12
12
|
require_relative 'outbox/version'
|
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.19
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Arkency
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-11-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ruby_event_store
|
@@ -30,37 +30,39 @@ 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: '
|
41
|
-
description:
|
42
|
-
email:
|
43
|
-
- dev@arkency.com
|
40
|
+
version: '5.2'
|
41
|
+
description:
|
42
|
+
email: dev@arkency.com
|
44
43
|
executables:
|
45
44
|
- res_outbox
|
46
45
|
extensions: []
|
47
|
-
extra_rdoc_files:
|
46
|
+
extra_rdoc_files:
|
47
|
+
- README.md
|
48
48
|
files:
|
49
49
|
- README.md
|
50
50
|
- bin/res_outbox
|
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
|
-
- lib/ruby_event_store/outbox/consumer_process.rb
|
57
58
|
- lib/ruby_event_store/outbox/fetch_specification.rb
|
59
|
+
- lib/ruby_event_store/outbox/legacy_sidekiq_scheduler.rb
|
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/metrics/test.rb
|
64
|
+
- lib/ruby_event_store/outbox/repository.rb
|
62
65
|
- lib/ruby_event_store/outbox/sidekiq5_format.rb
|
63
|
-
- lib/ruby_event_store/outbox/sidekiq_message_handler.rb
|
64
66
|
- lib/ruby_event_store/outbox/sidekiq_processor.rb
|
65
67
|
- lib/ruby_event_store/outbox/sidekiq_producer.rb
|
66
68
|
- lib/ruby_event_store/outbox/sidekiq_scheduler.rb
|
@@ -69,11 +71,11 @@ homepage: https://railseventstore.org
|
|
69
71
|
licenses:
|
70
72
|
- MIT
|
71
73
|
metadata:
|
72
|
-
homepage_uri: https://railseventstore.org
|
73
|
-
changelog_uri: https://github.com/RailsEventStore/rails_event_store/releases
|
74
|
+
homepage_uri: https://railseventstore.org
|
74
75
|
source_code_uri: https://github.com/RailsEventStore/rails_event_store
|
75
76
|
bug_tracker_uri: https://github.com/RailsEventStore/rails_event_store/issues
|
76
|
-
|
77
|
+
rubygems_mfa_required: 'true'
|
78
|
+
post_install_message:
|
77
79
|
rdoc_options: []
|
78
80
|
require_paths:
|
79
81
|
- lib
|
@@ -81,15 +83,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
81
83
|
requirements:
|
82
84
|
- - ">="
|
83
85
|
- !ruby/object:Gem::Version
|
84
|
-
version: '
|
86
|
+
version: '2.6'
|
85
87
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
88
|
requirements:
|
87
89
|
- - ">="
|
88
90
|
- !ruby/object:Gem::Version
|
89
91
|
version: '0'
|
90
92
|
requirements: []
|
91
|
-
rubygems_version: 3.
|
92
|
-
signing_key:
|
93
|
+
rubygems_version: 3.2.22
|
94
|
+
signing_key:
|
93
95
|
specification_version: 4
|
94
96
|
summary: Active Record based outbox for Ruby Event Store
|
95
97
|
test_files: []
|
@@ -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
|
@@ -1,19 +0,0 @@
|
|
1
|
-
require "sidekiq"
|
2
|
-
|
3
|
-
module RubyEventStore
|
4
|
-
module Outbox
|
5
|
-
class SidekiqMessageHandler
|
6
|
-
def initialize
|
7
|
-
@sidekiq = Sidekiq::Client.new(Sidekiq.redis_pool)
|
8
|
-
end
|
9
|
-
|
10
|
-
def init
|
11
|
-
end
|
12
|
-
|
13
|
-
def handle_one_record(now, record)
|
14
|
-
hash_payload = JSON.parse(record.payload)
|
15
|
-
@sidekiq.__send__(:raw_push, [hash_payload])
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|