sbmt-outbox 5.0.1 → 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|