sbmt-outbox 5.0.1 → 6.0.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 +4 -4
- data/README.md +68 -9
- data/app/interactors/sbmt/outbox/process_item.rb +2 -1
- data/app/interactors/sbmt/outbox/retry_strategies/base.rb +15 -0
- data/app/interactors/sbmt/outbox/retry_strategies/compacted_log.rb +2 -32
- data/app/interactors/sbmt/outbox/retry_strategies/exponential_backoff.rb +3 -5
- data/app/interactors/sbmt/outbox/retry_strategies/latest_available.rb +39 -0
- data/app/interactors/sbmt/outbox/retry_strategies/no_delay.rb +13 -0
- data/app/models/sbmt/outbox/base_item.rb +9 -8
- data/app/models/sbmt/outbox/base_item_config.rb +23 -4
- data/config/initializers/yabeda.rb +32 -5
- data/lib/generators/helpers/migration.rb +2 -2
- data/lib/sbmt/outbox/cli.rb +50 -7
- data/lib/sbmt/outbox/engine.rb +26 -0
- data/lib/sbmt/outbox/logger.rb +6 -0
- data/lib/sbmt/outbox/v1/thread_pool.rb +110 -0
- data/lib/sbmt/outbox/v1/throttler.rb +54 -0
- data/lib/sbmt/outbox/v1/worker.rb +231 -0
- data/lib/sbmt/outbox/v2/box_processor.rb +148 -0
- data/lib/sbmt/outbox/v2/poll_throttler/base.rb +43 -0
- data/lib/sbmt/outbox/v2/poll_throttler/composite.rb +42 -0
- data/lib/sbmt/outbox/v2/poll_throttler/fixed_delay.rb +28 -0
- data/lib/sbmt/outbox/v2/poll_throttler/noop.rb +17 -0
- data/lib/sbmt/outbox/v2/poll_throttler/rate_limited.rb +24 -0
- data/lib/sbmt/outbox/v2/poll_throttler/redis_queue_size.rb +46 -0
- data/lib/sbmt/outbox/v2/poll_throttler/redis_queue_time_lag.rb +45 -0
- data/lib/sbmt/outbox/v2/poll_throttler.rb +49 -0
- data/lib/sbmt/outbox/v2/poller.rb +180 -0
- data/lib/sbmt/outbox/v2/processor.rb +101 -0
- data/lib/sbmt/outbox/v2/redis_job.rb +42 -0
- data/lib/sbmt/outbox/v2/tasks/base.rb +48 -0
- data/lib/sbmt/outbox/v2/tasks/default.rb +17 -0
- data/lib/sbmt/outbox/v2/tasks/poll.rb +34 -0
- data/lib/sbmt/outbox/v2/tasks/process.rb +31 -0
- data/lib/sbmt/outbox/v2/thread_pool.rb +152 -0
- data/lib/sbmt/outbox/v2/throttler.rb +13 -0
- data/lib/sbmt/outbox/v2/worker.rb +52 -0
- data/lib/sbmt/outbox/version.rb +1 -1
- data/lib/sbmt/outbox.rb +16 -2
- metadata +41 -5
- data/lib/sbmt/outbox/thread_pool.rb +0 -108
- data/lib/sbmt/outbox/throttler.rb +0 -52
- data/lib/sbmt/outbox/worker.rb +0 -233
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module Outbox
|
5
|
+
module V2
|
6
|
+
class ThreadPool
|
7
|
+
delegate :logger, to: "Sbmt::Outbox"
|
8
|
+
|
9
|
+
BREAK = Object.new.freeze
|
10
|
+
SKIPPED = Object.new.freeze
|
11
|
+
PROCESSED = Object.new.freeze
|
12
|
+
|
13
|
+
def initialize(concurrency:, name: "thread_pool", random_startup_delay: true, start_async: true, &block)
|
14
|
+
self.concurrency = concurrency
|
15
|
+
self.name = name
|
16
|
+
self.random_startup_delay = random_startup_delay
|
17
|
+
self.start_async = start_async
|
18
|
+
self.task_source = block
|
19
|
+
self.task_mutex = Mutex.new
|
20
|
+
self.stopped = true
|
21
|
+
self.threads = Concurrent::Array.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def next_task
|
25
|
+
task_mutex.synchronize do
|
26
|
+
return if stopped
|
27
|
+
task = task_source.call
|
28
|
+
|
29
|
+
if task == BREAK
|
30
|
+
self.stopped = true
|
31
|
+
return
|
32
|
+
end
|
33
|
+
|
34
|
+
task
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def start
|
39
|
+
self.stopped = false
|
40
|
+
|
41
|
+
mode = start_async ? "async" : "sync"
|
42
|
+
logger.log_info("#{name}: starting #{concurrency} threads in #{mode} mode")
|
43
|
+
|
44
|
+
result = run_threads do |task|
|
45
|
+
logger.with_tags(worker: worker_number) do
|
46
|
+
yield worker_number, task
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
logger.log_info("#{name}: threads started")
|
51
|
+
|
52
|
+
raise result if result.is_a?(Exception)
|
53
|
+
end
|
54
|
+
|
55
|
+
def stop
|
56
|
+
self.stopped = true
|
57
|
+
|
58
|
+
threads.map(&:join) if start_async
|
59
|
+
ensure
|
60
|
+
stop_threads
|
61
|
+
end
|
62
|
+
|
63
|
+
def running?
|
64
|
+
return false if stopped
|
65
|
+
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
def alive?(timeout)
|
70
|
+
return false if stopped
|
71
|
+
|
72
|
+
deadline = Time.current - timeout
|
73
|
+
threads.all? do |thread|
|
74
|
+
last_active_at = last_active_at(thread)
|
75
|
+
return false unless last_active_at
|
76
|
+
|
77
|
+
deadline < last_active_at
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
attr_accessor :concurrency, :name, :random_startup_delay, :task_source, :task_mutex, :stopped, :start_async, :threads
|
84
|
+
|
85
|
+
def touch_worker!
|
86
|
+
self.last_active_at = Time.current
|
87
|
+
end
|
88
|
+
|
89
|
+
def worker_number(thread = Thread.current)
|
90
|
+
thread.thread_variable_get("#{name}_worker_number:#{object_id}")
|
91
|
+
end
|
92
|
+
|
93
|
+
def last_active_at(thread = Thread.current)
|
94
|
+
thread.thread_variable_get("#{name}_last_active_at:#{object_id}")
|
95
|
+
end
|
96
|
+
|
97
|
+
def run_threads
|
98
|
+
exception = nil
|
99
|
+
|
100
|
+
in_threads do |worker_num|
|
101
|
+
self.worker_number = worker_num
|
102
|
+
# We don't want to start all threads at the same time
|
103
|
+
sleep(rand * (worker_num + 1)) if random_startup_delay
|
104
|
+
|
105
|
+
touch_worker!
|
106
|
+
|
107
|
+
until exception
|
108
|
+
task = next_task
|
109
|
+
break unless task
|
110
|
+
|
111
|
+
touch_worker!
|
112
|
+
|
113
|
+
begin
|
114
|
+
yield task
|
115
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
116
|
+
exception = e
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
exception
|
122
|
+
end
|
123
|
+
|
124
|
+
def in_threads
|
125
|
+
Thread.handle_interrupt(Exception => :never) do
|
126
|
+
Thread.handle_interrupt(Exception => :immediate) do
|
127
|
+
concurrency.times do |i|
|
128
|
+
threads << Thread.new { yield(i) }
|
129
|
+
end
|
130
|
+
threads.map(&:value) unless start_async
|
131
|
+
end
|
132
|
+
ensure
|
133
|
+
stop_threads unless start_async
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def stop_threads
|
138
|
+
threads.each(&:kill)
|
139
|
+
threads.clear
|
140
|
+
end
|
141
|
+
|
142
|
+
def worker_number=(num)
|
143
|
+
Thread.current.thread_variable_set("#{name}_worker_number:#{object_id}", num)
|
144
|
+
end
|
145
|
+
|
146
|
+
def last_active_at=(at)
|
147
|
+
Thread.current.thread_variable_set("#{name}_last_active_at:#{object_id}", at)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redlock"
|
4
|
+
require "sbmt/outbox/v2/poller"
|
5
|
+
require "sbmt/outbox/v2/processor"
|
6
|
+
|
7
|
+
module Sbmt
|
8
|
+
module Outbox
|
9
|
+
module V2
|
10
|
+
class Worker
|
11
|
+
def initialize(boxes:, poll_tactic: nil, processor_concurrency: nil, poller_partitions_count: nil, poller_threads_count: nil)
|
12
|
+
@poller = Poller.new(boxes, throttler_tactic: poll_tactic, threads_count: poller_threads_count, partitions_count: poller_partitions_count)
|
13
|
+
@processor = Processor.new(boxes, threads_count: processor_concurrency)
|
14
|
+
end
|
15
|
+
|
16
|
+
def start
|
17
|
+
start_async
|
18
|
+
|
19
|
+
loop do
|
20
|
+
sleep 0.1
|
21
|
+
break unless @poller.started && @processor.started
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def start_async
|
26
|
+
@poller.start
|
27
|
+
@processor.start
|
28
|
+
|
29
|
+
loop do
|
30
|
+
sleep(0.1)
|
31
|
+
break if ready?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def stop
|
36
|
+
@poller.stop
|
37
|
+
@processor.stop
|
38
|
+
end
|
39
|
+
|
40
|
+
def ready?
|
41
|
+
@poller.ready? && @processor.ready?
|
42
|
+
end
|
43
|
+
|
44
|
+
def alive?
|
45
|
+
return false unless ready?
|
46
|
+
|
47
|
+
@poller.alive?(@poller.lock_timeout) && @processor.alive?(@processor.lock_timeout)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/sbmt/outbox/version.rb
CHANGED
data/lib/sbmt/outbox.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "rails"
|
4
|
+
require "active_job"
|
5
|
+
require "active_record"
|
4
6
|
require "dry-initializer"
|
5
7
|
require "dry-monads"
|
6
8
|
require "dry/monads/do"
|
@@ -53,12 +55,24 @@ module Sbmt
|
|
53
55
|
@logger ||= Sbmt::Outbox::Logger.new
|
54
56
|
end
|
55
57
|
|
58
|
+
def poller_config
|
59
|
+
@poller_config ||= config.poller
|
60
|
+
end
|
61
|
+
|
62
|
+
def processor_config
|
63
|
+
@processor_config ||= config.processor
|
64
|
+
end
|
65
|
+
|
66
|
+
def default_worker_version
|
67
|
+
@default_worker_version ||= config.default_worker_version&.to_i || 2
|
68
|
+
end
|
69
|
+
|
56
70
|
def active_record_base_class
|
57
|
-
@active_record_base_class ||= config.active_record_base_class.safe_constantize || ActiveRecord::Base
|
71
|
+
@active_record_base_class ||= config.active_record_base_class.safe_constantize || ::ActiveRecord::Base
|
58
72
|
end
|
59
73
|
|
60
74
|
def active_job_base_class
|
61
|
-
@active_job_base_class ||= config.active_job_base_class.safe_constantize || ActiveJob::Base
|
75
|
+
@active_job_base_class ||= config.active_job_base_class.safe_constantize || ::ActiveJob::Base
|
62
76
|
end
|
63
77
|
|
64
78
|
def error_tracker
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sbmt-outbox
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 6.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sbermarket Ruby-Platform Team
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-03-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: connection_pool
|
@@ -188,6 +188,20 @@ dependencies:
|
|
188
188
|
- - "~>"
|
189
189
|
- !ruby/object:Gem::Version
|
190
190
|
version: '0.5'
|
191
|
+
- !ruby/object:Gem::Dependency
|
192
|
+
name: ruby-limiter
|
193
|
+
requirement: !ruby/object:Gem::Requirement
|
194
|
+
requirements:
|
195
|
+
- - "~>"
|
196
|
+
- !ruby/object:Gem::Version
|
197
|
+
version: '2.3'
|
198
|
+
type: :runtime
|
199
|
+
prerelease: false
|
200
|
+
version_requirements: !ruby/object:Gem::Requirement
|
201
|
+
requirements:
|
202
|
+
- - "~>"
|
203
|
+
- !ruby/object:Gem::Version
|
204
|
+
version: '2.3'
|
191
205
|
- !ruby/object:Gem::Dependency
|
192
206
|
name: appraisal
|
193
207
|
requirement: !ruby/object:Gem::Requirement
|
@@ -506,8 +520,11 @@ files:
|
|
506
520
|
- app/interactors/sbmt/outbox/partition_strategies/hash_partitioning.rb
|
507
521
|
- app/interactors/sbmt/outbox/partition_strategies/number_partitioning.rb
|
508
522
|
- app/interactors/sbmt/outbox/process_item.rb
|
523
|
+
- app/interactors/sbmt/outbox/retry_strategies/base.rb
|
509
524
|
- app/interactors/sbmt/outbox/retry_strategies/compacted_log.rb
|
510
525
|
- app/interactors/sbmt/outbox/retry_strategies/exponential_backoff.rb
|
526
|
+
- app/interactors/sbmt/outbox/retry_strategies/latest_available.rb
|
527
|
+
- app/interactors/sbmt/outbox/retry_strategies/no_delay.rb
|
511
528
|
- app/jobs/sbmt/outbox/base_delete_stale_items_job.rb
|
512
529
|
- app/jobs/sbmt/outbox/delete_stale_inbox_items_job.rb
|
513
530
|
- app/jobs/sbmt/outbox/delete_stale_outbox_items_job.rb
|
@@ -563,10 +580,29 @@ files:
|
|
563
580
|
- lib/sbmt/outbox/redis_client_factory.rb
|
564
581
|
- lib/sbmt/outbox/tasks/delete_failed_items.rake
|
565
582
|
- lib/sbmt/outbox/tasks/retry_failed_items.rake
|
566
|
-
- lib/sbmt/outbox/thread_pool.rb
|
567
|
-
- lib/sbmt/outbox/throttler.rb
|
583
|
+
- lib/sbmt/outbox/v1/thread_pool.rb
|
584
|
+
- lib/sbmt/outbox/v1/throttler.rb
|
585
|
+
- lib/sbmt/outbox/v1/worker.rb
|
586
|
+
- lib/sbmt/outbox/v2/box_processor.rb
|
587
|
+
- lib/sbmt/outbox/v2/poll_throttler.rb
|
588
|
+
- lib/sbmt/outbox/v2/poll_throttler/base.rb
|
589
|
+
- lib/sbmt/outbox/v2/poll_throttler/composite.rb
|
590
|
+
- lib/sbmt/outbox/v2/poll_throttler/fixed_delay.rb
|
591
|
+
- lib/sbmt/outbox/v2/poll_throttler/noop.rb
|
592
|
+
- lib/sbmt/outbox/v2/poll_throttler/rate_limited.rb
|
593
|
+
- lib/sbmt/outbox/v2/poll_throttler/redis_queue_size.rb
|
594
|
+
- lib/sbmt/outbox/v2/poll_throttler/redis_queue_time_lag.rb
|
595
|
+
- lib/sbmt/outbox/v2/poller.rb
|
596
|
+
- lib/sbmt/outbox/v2/processor.rb
|
597
|
+
- lib/sbmt/outbox/v2/redis_job.rb
|
598
|
+
- lib/sbmt/outbox/v2/tasks/base.rb
|
599
|
+
- lib/sbmt/outbox/v2/tasks/default.rb
|
600
|
+
- lib/sbmt/outbox/v2/tasks/poll.rb
|
601
|
+
- lib/sbmt/outbox/v2/tasks/process.rb
|
602
|
+
- lib/sbmt/outbox/v2/thread_pool.rb
|
603
|
+
- lib/sbmt/outbox/v2/throttler.rb
|
604
|
+
- lib/sbmt/outbox/v2/worker.rb
|
568
605
|
- lib/sbmt/outbox/version.rb
|
569
|
-
- lib/sbmt/outbox/worker.rb
|
570
606
|
homepage: https://github.com/SberMarket-Tech/sbmt-outbox
|
571
607
|
licenses:
|
572
608
|
- MIT
|
@@ -1,108 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "sbmt/outbox/throttler"
|
4
|
-
|
5
|
-
module Sbmt
|
6
|
-
module Outbox
|
7
|
-
class ThreadPool
|
8
|
-
BREAK = Object.new.freeze
|
9
|
-
SKIPPED = Object.new.freeze
|
10
|
-
PROCESSED = Object.new.freeze
|
11
|
-
|
12
|
-
def initialize(&block)
|
13
|
-
self.task_source = block
|
14
|
-
self.task_mutex = Mutex.new
|
15
|
-
self.stopped = true
|
16
|
-
end
|
17
|
-
|
18
|
-
def next_task
|
19
|
-
task_mutex.synchronize do
|
20
|
-
return if stopped
|
21
|
-
item = task_source.call
|
22
|
-
|
23
|
-
if item == BREAK
|
24
|
-
self.stopped = true
|
25
|
-
return
|
26
|
-
end
|
27
|
-
|
28
|
-
item
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def start(concurrency:)
|
33
|
-
self.stopped = false
|
34
|
-
result = run_threads(count: concurrency) do |item|
|
35
|
-
yield worker_number, item
|
36
|
-
end
|
37
|
-
|
38
|
-
raise result if result.is_a?(Exception)
|
39
|
-
nil
|
40
|
-
ensure
|
41
|
-
self.stopped = true
|
42
|
-
end
|
43
|
-
|
44
|
-
def stop
|
45
|
-
self.stopped = true
|
46
|
-
end
|
47
|
-
|
48
|
-
def worker_number
|
49
|
-
Thread.current["thread_pool_worker_number:#{object_id}"]
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
|
54
|
-
attr_accessor :task_source, :task_mutex, :stopped
|
55
|
-
|
56
|
-
def run_threads(count:)
|
57
|
-
exception = nil
|
58
|
-
|
59
|
-
in_threads(count: count) do |worker_num|
|
60
|
-
self.worker_number = worker_num
|
61
|
-
# We don't want to start all threads at the same time
|
62
|
-
random_sleep = rand * (worker_num + 1)
|
63
|
-
|
64
|
-
throttler = Throttler.new(
|
65
|
-
limit: Outbox.config.worker.rate_limit,
|
66
|
-
interval: Outbox.config.worker.rate_interval + random_sleep
|
67
|
-
)
|
68
|
-
|
69
|
-
sleep(random_sleep)
|
70
|
-
|
71
|
-
last_result = nil
|
72
|
-
until exception
|
73
|
-
throttler.wait if last_result == PROCESSED
|
74
|
-
item = next_task
|
75
|
-
break unless item
|
76
|
-
|
77
|
-
begin
|
78
|
-
last_result = yield item
|
79
|
-
rescue Exception => e # rubocop:disable Lint/RescueException
|
80
|
-
exception = e
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
exception
|
86
|
-
end
|
87
|
-
|
88
|
-
def in_threads(count:)
|
89
|
-
threads = []
|
90
|
-
|
91
|
-
Thread.handle_interrupt(Exception => :never) do
|
92
|
-
Thread.handle_interrupt(Exception => :immediate) do
|
93
|
-
count.times do |i|
|
94
|
-
threads << Thread.new { yield(i) }
|
95
|
-
end
|
96
|
-
threads.map(&:value)
|
97
|
-
end
|
98
|
-
ensure
|
99
|
-
threads.each(&:kill)
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def worker_number=(num)
|
104
|
-
Thread.current["thread_pool_worker_number:#{object_id}"] = num
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
@@ -1,52 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Sbmt
|
4
|
-
module Outbox
|
5
|
-
# Based on https://github.com/Shopify/limiter/blob/master/lib/limiter/rate_queue.rb
|
6
|
-
# We cannot use that gem because we have to support Ruby 2.5,
|
7
|
-
# but Shopify's limiter requires minimum Ruby 2.6
|
8
|
-
class Throttler
|
9
|
-
def initialize(limit: nil, interval: nil)
|
10
|
-
@limit = limit
|
11
|
-
@interval = limit
|
12
|
-
@map = (0...@limit).map { |i| base_time + (gap * i) }
|
13
|
-
@index = 0
|
14
|
-
@mutex = Mutex.new
|
15
|
-
end
|
16
|
-
|
17
|
-
def wait
|
18
|
-
time = nil
|
19
|
-
|
20
|
-
@mutex.synchronize do
|
21
|
-
time = @map[@index]
|
22
|
-
|
23
|
-
sleep_until(time + @interval)
|
24
|
-
|
25
|
-
@map[@index] = now
|
26
|
-
@index = (@index + 1) % @limit
|
27
|
-
end
|
28
|
-
|
29
|
-
time
|
30
|
-
end
|
31
|
-
|
32
|
-
private
|
33
|
-
|
34
|
-
def sleep_until(time)
|
35
|
-
period = time - now
|
36
|
-
sleep(period) if period > 0
|
37
|
-
end
|
38
|
-
|
39
|
-
def base_time
|
40
|
-
now - @interval
|
41
|
-
end
|
42
|
-
|
43
|
-
def gap
|
44
|
-
@interval.to_f / @limit.to_f
|
45
|
-
end
|
46
|
-
|
47
|
-
def now
|
48
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|