sbmt-outbox 6.20.0 → 7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b5eb57696c5cf8629d009f7da69a2f74722bc1425dc79853b49416d815204e87
4
- data.tar.gz: 33bc6c09ff276884a35e351bbce3f732fad2eb22c7e5e8a3d384ee4b765e551e
3
+ metadata.gz: 23edf4cf26107ce12a000cc13fd63f2488e82c2b2f633f41ac8a8a81ab3f1dfb
4
+ data.tar.gz: c6f2e7936814535ff29759b3c72c2396128cae67310feca766060ac23d3d413c
5
5
  SHA512:
6
- metadata.gz: 8316b5867875b12c96d47a399eefde7962e5d16876c347c9cb7ed376054e79fb8d1ba14ae27dec9188311afbeac87a04dc32729a6e9c99c5274b49dc074d30d1
7
- data.tar.gz: 4df45b21bb48f1dc053b6c6625c518a4daad8e6dcef609ad433b9802c91730bf9e090adc16b35397e97d316d0782f66e0a686a3832045f969bb125b2c51755bf
6
+ metadata.gz: cb10853b53332370d2ef41d11b7ac4bb1415f0461e79588ce26f2afedcd6c7d2d88e02c11a3bc4f488489af0f46b1537b3ce20b35f991549fad80b18135a41cc
7
+ data.tar.gz: 283e97a0d0129a7ea9813bcd8f911487ed55436c9c9d6588071ae261a14d56d82aef172b192823a3036b42da8b414503ed5e81ce4f9548397cf1b8d4d792b61d
data/README.md CHANGED
@@ -154,7 +154,7 @@ Rails.application.config.outbox.tap do |config|
154
154
  config.redis = {url: ENV.fetch("REDIS_URL")} # Redis is used as a coordinator service
155
155
  config.paths << Rails.root.join("config/outbox.yml").to_s # optional; configuration file paths, deep merged at the application start, useful with Rails engines
156
156
 
157
- # optional (worker v2: default)
157
+ # optional
158
158
  config.poller = ActiveSupport::OrderedOptions.new.tap do |pc|
159
159
  # max parallel threads (per box-item, globally)
160
160
  pc.concurrency = 6
@@ -184,7 +184,7 @@ Rails.application.config.outbox.tap do |config|
184
184
  pc.queue_delay = 0.1
185
185
  end
186
186
 
187
- # optional (worker v2: default)
187
+ # optional
188
188
  config.processor = ActiveSupport::OrderedOptions.new.tap do |pc|
189
189
  # max threads count (per worker process)
190
190
  pc.threads_count = 4
@@ -193,26 +193,6 @@ Rails.application.config.outbox.tap do |config|
193
193
  # BRPOP delay (in seconds) for polling redis job queue per box-item
194
194
  pc.brpop_delay = 2
195
195
  end
196
-
197
- # optional (worker v1: DEPRECATED)
198
- config.process_items.tap do |x|
199
- # maximum processing time of the batch, after which the batch will be considered hung and processing will be aborted
200
- x.general_timeout = 180
201
- # maximum batch processing time, after which the processing of the batch will be aborted in the current thread,
202
- # and the next thread that picks up the batch will start processing from the same place
203
- x.cutoff_timeout = 60
204
- # batch size
205
- x.batch_size = 200
206
- end
207
-
208
- # optional (worker v1: DEPRECATED)
209
- config.worker.tap do |worker|
210
- # number of batches that one thread will process per rate interval
211
- worker.rate_limit = 10
212
- # rate interval in seconds
213
- worker.rate_interval = 60
214
- end
215
- end
216
196
  ```
217
197
 
218
198
  ### Outbox pattern
@@ -642,7 +622,7 @@ end
642
622
 
643
623
  We would like to see more features added to the web UI. If you have any suggestions, please feel free to submit a pull request 🤗.
644
624
 
645
- ## CLI Arguments (v2: default)
625
+ ## CLI Arguments
646
626
 
647
627
  | Key | Description |
648
628
  |----------------------------|----------------------------------------------------------------------|
@@ -651,14 +631,6 @@ We would like to see more features added to the web UI. If you have any suggesti
651
631
  | `--poll-concurrency or -p` | Number of poller partitions. Default 6. |
652
632
  | `--poll-threads or -n` | Number of poll threads. Default 1. |
653
633
  | `--poll-tactic or -t` | Poll tactic. Default "default". |
654
- | `--worker-version or -w` | Worker version. Default 2. |
655
-
656
- ## CLI Arguments (v1: DEPRECATED)
657
-
658
- | Key | Description |
659
- |-----------------------|---------------------------------------------------------------------------|
660
- | `--boxes or -b` | Outbox/Inbox processors to start` |
661
- | `--concurrency or -c` | Number of threads. Default 10. |
662
634
 
663
635
  ## Development & Test
664
636
 
@@ -33,34 +33,19 @@ module Sbmt
33
33
  aliases: "-t",
34
34
  type: :string,
35
35
  desc: "Poll tactic: [default, low-priority, aggressive]"
36
- option :worker_version,
37
- aliases: "-w",
38
- type: :numeric,
39
- desc: "Worker version: [1 | 2]"
40
36
  def start
41
37
  load_environment
42
38
 
43
- version = options[:worker_version] || Outbox.default_worker_version
44
-
45
39
  boxes = format_boxes(options[:box])
46
- check_deprecations!(boxes, version)
47
-
48
- worker = if version == 1
49
- Sbmt::Outbox::V1::Worker.new(
50
- boxes: boxes,
51
- concurrency: options[:concurrency] || 10
52
- )
53
- elsif version == 2
54
- Sbmt::Outbox::V2::Worker.new(
55
- boxes: boxes,
56
- poll_tactic: options[:poll_tactic],
57
- poller_threads_count: options[:poll_threads],
58
- poller_partitions_count: options[:poll_concurrency],
59
- processor_concurrency: options[:concurrency] || 4
60
- )
61
- else
62
- raise "Worker version #{version} is invalid, available versions: 1|2"
63
- end
40
+ check_deprecations!(boxes)
41
+
42
+ worker = Sbmt::Outbox::V2::Worker.new(
43
+ boxes: boxes,
44
+ poll_tactic: options[:poll_tactic],
45
+ poller_threads_count: options[:poll_threads],
46
+ poller_partitions_count: options[:poll_concurrency],
47
+ processor_concurrency: options[:concurrency] || 4
48
+ )
64
49
 
65
50
  Sbmt::Outbox.current_worker = worker
66
51
 
@@ -77,9 +62,7 @@ module Sbmt
77
62
 
78
63
  private
79
64
 
80
- def check_deprecations!(boxes, version)
81
- return unless version == 2
82
-
65
+ def check_deprecations!(boxes)
83
66
  boxes.each do |item_class|
84
67
  next if item_class.config.partition_size_raw.blank?
85
68
 
@@ -91,7 +74,6 @@ module Sbmt
91
74
  load(lookup_outboxfile)
92
75
 
93
76
  require "sbmt/outbox"
94
- require "sbmt/outbox/v1/worker"
95
77
  require "sbmt/outbox/v2/worker"
96
78
  end
97
79
 
@@ -29,12 +29,6 @@ module Sbmt
29
29
  c.cutoff_timeout = 90
30
30
  c.batch_size = 200
31
31
  end
32
- c.worker = ActiveSupport::OrderedOptions.new.tap do |c|
33
- c.rate_limit = 20
34
- c.rate_interval = 60
35
- c.shuffle_jobs = true
36
- end
37
- c.default_worker_version = 2
38
32
 
39
33
  # worker v2
40
34
  c.poller = ActiveSupport::OrderedOptions.new.tap do |pc|
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sbmt
4
4
  module Outbox
5
- VERSION = "6.20.0"
5
+ VERSION = "7.0.0"
6
6
  end
7
7
  end
data/lib/sbmt/outbox.rb CHANGED
@@ -65,10 +65,6 @@ module Sbmt
65
65
  @processor_config ||= config.processor
66
66
  end
67
67
 
68
- def default_worker_version
69
- @default_worker_version ||= config.default_worker_version&.to_i || 2
70
- end
71
-
72
68
  def active_record_base_class
73
69
  @active_record_base_class ||= config.active_record_base_class.safe_constantize || ::ActiveRecord::Base
74
70
  end
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: 6.20.0
4
+ version: 7.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: 2025-04-02 00:00:00.000000000 Z
11
+ date: 2025-04-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: connection_pool
@@ -585,9 +585,6 @@ files:
585
585
  - lib/sbmt/outbox/tasks/delete_items.rake
586
586
  - lib/sbmt/outbox/tasks/retry_failed_items.rake
587
587
  - lib/sbmt/outbox/tasks/update_status_items.rake
588
- - lib/sbmt/outbox/v1/thread_pool.rb
589
- - lib/sbmt/outbox/v1/throttler.rb
590
- - lib/sbmt/outbox/v1/worker.rb
591
588
  - lib/sbmt/outbox/v2/box_processor.rb
592
589
  - lib/sbmt/outbox/v2/poll_throttler.rb
593
590
  - lib/sbmt/outbox/v2/poll_throttler/base.rb
@@ -1,110 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "sbmt/outbox/v1/throttler"
4
-
5
- module Sbmt
6
- module Outbox
7
- module V1
8
- class ThreadPool
9
- BREAK = Object.new.freeze
10
- SKIPPED = Object.new.freeze
11
- PROCESSED = Object.new.freeze
12
-
13
- def initialize(&block)
14
- self.task_source = block
15
- self.task_mutex = Mutex.new
16
- self.stopped = true
17
- end
18
-
19
- def next_task
20
- task_mutex.synchronize do
21
- return if stopped
22
- item = task_source.call
23
-
24
- if item == BREAK
25
- self.stopped = true
26
- return
27
- end
28
-
29
- item
30
- end
31
- end
32
-
33
- def start(concurrency:)
34
- self.stopped = false
35
- result = run_threads(count: concurrency) do |item|
36
- yield worker_number, item
37
- end
38
-
39
- raise result if result.is_a?(Exception)
40
- nil
41
- ensure
42
- self.stopped = true
43
- end
44
-
45
- def stop
46
- self.stopped = true
47
- end
48
-
49
- def worker_number
50
- Thread.current["thread_pool_worker_number:#{object_id}"]
51
- end
52
-
53
- private
54
-
55
- attr_accessor :task_source, :task_mutex, :stopped
56
-
57
- def run_threads(count:)
58
- exception = nil
59
-
60
- in_threads(count: count) do |worker_num|
61
- self.worker_number = worker_num
62
- # We don't want to start all threads at the same time
63
- random_sleep = rand * (worker_num + 1)
64
-
65
- throttler = Throttler.new(
66
- limit: Outbox.config.worker.rate_limit,
67
- interval: Outbox.config.worker.rate_interval + random_sleep
68
- )
69
-
70
- sleep(random_sleep)
71
-
72
- last_result = nil
73
- until exception
74
- throttler.wait if last_result == PROCESSED
75
- item = next_task
76
- break unless item
77
-
78
- begin
79
- last_result = yield item
80
- rescue Exception => e # rubocop:disable Lint/RescueException
81
- exception = e
82
- end
83
- end
84
- end
85
-
86
- exception
87
- end
88
-
89
- def in_threads(count:)
90
- threads = []
91
-
92
- Thread.handle_interrupt(Exception => :never) do
93
- Thread.handle_interrupt(Exception => :immediate) do
94
- count.times do |i|
95
- threads << Thread.new { yield(i) }
96
- end
97
- threads.map(&:value)
98
- end
99
- ensure
100
- threads.each(&:kill)
101
- end
102
- end
103
-
104
- def worker_number=(num)
105
- Thread.current["thread_pool_worker_number:#{object_id}"] = num
106
- end
107
- end
108
- end
109
- end
110
- end
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sbmt
4
- module Outbox
5
- module V1
6
- # Based on https://github.com/Shopify/limiter/blob/master/lib/limiter/rate_queue.rb
7
- # We cannot use that gem because we have to support Ruby 2.5,
8
- # but Shopify's limiter requires minimum Ruby 2.6
9
- class Throttler
10
- def initialize(limit: nil, interval: nil)
11
- @limit = limit
12
- @interval = limit
13
- @map = (0...@limit).map { |i| base_time + (gap * i) }
14
- @index = 0
15
- @mutex = Mutex.new
16
- end
17
-
18
- def wait
19
- time = nil
20
-
21
- @mutex.synchronize do
22
- time = @map[@index]
23
-
24
- sleep_until(time + @interval)
25
-
26
- @map[@index] = now
27
- @index = (@index + 1) % @limit
28
- end
29
-
30
- time
31
- end
32
-
33
- private
34
-
35
- def sleep_until(time)
36
- period = time - now
37
- sleep(period) if period > 0
38
- end
39
-
40
- def base_time
41
- now - @interval
42
- end
43
-
44
- def gap
45
- @interval.to_f / @limit.to_f
46
- end
47
-
48
- def now
49
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
50
- end
51
- end
52
- end
53
- end
54
- end
@@ -1,233 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "redlock"
4
- require "sbmt/outbox/v1/thread_pool"
5
- require "sbmt/outbox/metrics/utils"
6
-
7
- module Sbmt
8
- module Outbox
9
- module V1
10
- class Worker
11
- Job = Struct.new(
12
- :item_class,
13
- :partition,
14
- :buckets,
15
- :log_tags,
16
- :yabeda_labels,
17
- :resource_key,
18
- :resource_path,
19
- keyword_init: true
20
- )
21
-
22
- delegate :config,
23
- :logger,
24
- :batch_process_middlewares,
25
- to: "Sbmt::Outbox"
26
- delegate :stop, to: :thread_pool
27
- delegate :general_timeout, :cutoff_timeout, :batch_size, to: "Sbmt::Outbox.config.process_items"
28
- delegate :job_counter,
29
- :job_execution_runtime,
30
- :item_execution_runtime,
31
- :job_items_counter,
32
- :job_timeout_counter,
33
- to: "Yabeda.box_worker"
34
-
35
- def initialize(boxes:, concurrency: 10)
36
- self.queue = Queue.new
37
- build_jobs(boxes).each { |job| queue << job }
38
- self.thread_pool = ThreadPool.new { queue.pop }
39
- self.concurrency = [concurrency, queue.size].min
40
- self.thread_workers = {}
41
- init_redis
42
- end
43
-
44
- def start
45
- raise "Outbox is already started" if started
46
- self.started = true
47
- self.thread_workers = {}
48
-
49
- thread_pool.start(concurrency: concurrency) do |worker_number, job|
50
- touch_thread_worker!
51
- result = ThreadPool::PROCESSED
52
- logger.with_tags(**job.log_tags.merge(worker: worker_number)) do
53
- lock_manager.lock("#{job.resource_path}:lock", general_timeout * 1000) do |locked|
54
- labels = job.yabeda_labels
55
-
56
- if locked
57
- job_execution_runtime.measure(labels) do
58
- ::Rails.application.executor.wrap do
59
- safe_process_job(job, worker_number, labels)
60
- end
61
- end
62
- else
63
- result = ThreadPool::SKIPPED
64
- logger.log_info("Skip processing already locked #{job.resource_key}")
65
- end
66
-
67
- job_counter.increment(labels.merge(state: locked ? "processed" : "skipped"), by: 1)
68
- end
69
- end
70
-
71
- result
72
- ensure
73
- queue << job
74
- end
75
- rescue => e
76
- Outbox.error_tracker.error(e)
77
- raise
78
- ensure
79
- self.started = false
80
- end
81
-
82
- def ready?
83
- started && thread_workers.any?
84
- end
85
-
86
- def alive?
87
- return false unless started
88
-
89
- deadline = Time.current - general_timeout
90
- thread_workers.all? do |_worker_number, time|
91
- deadline < time
92
- end
93
- end
94
-
95
- private
96
-
97
- attr_accessor :queue, :thread_pool, :concurrency, :lock_manager, :redis, :thread_workers, :started
98
-
99
- def init_redis
100
- self.redis = ConnectionPool::Wrapper.new(size: concurrency) { RedisClientFactory.build(config.redis) }
101
-
102
- client = if Gem::Version.new(Redlock::VERSION) >= Gem::Version.new("2.0.0")
103
- redis
104
- else
105
- ConnectionPool::Wrapper.new(size: concurrency) { Redis.new(config.redis) }
106
- end
107
-
108
- self.lock_manager = Redlock::Client.new([client], retry_count: 0)
109
- end
110
-
111
- def build_jobs(boxes)
112
- res = boxes.map do |item_class|
113
- partitions = (0...item_class.config.partition_size).to_a
114
- partitions.map do |partition|
115
- buckets = item_class.partition_buckets.fetch(partition)
116
- resource_key = "#{item_class.box_name}/#{partition}"
117
-
118
- Job.new(
119
- item_class: item_class,
120
- partition: partition,
121
- buckets: buckets,
122
- log_tags: {
123
- box_type: item_class.box_type,
124
- box_name: item_class.box_name,
125
- box_partition: partition,
126
- trace_id: nil
127
- },
128
- yabeda_labels: {
129
- type: item_class.box_type,
130
- name: Sbmt::Outbox::Metrics::Utils.metric_safe(item_class.box_name),
131
- partition: partition,
132
- owner: item_class.owner
133
- },
134
- resource_key: resource_key,
135
- resource_path: "sbmt/outbox/worker/#{resource_key}"
136
- )
137
- end
138
- end.flatten
139
-
140
- res.shuffle! if Outbox.config.worker.shuffle_jobs
141
- res
142
- end
143
-
144
- def touch_thread_worker!
145
- thread_workers[thread_pool.worker_number] = Time.current
146
- end
147
-
148
- def safe_process_job(job, worker_number, labels)
149
- middlewares = Middleware::Builder.new(batch_process_middlewares)
150
-
151
- middlewares.call(job) do
152
- start_id ||= redis.call("GETDEL", "#{job.resource_path}:last_id").to_i + 1
153
- logger.log_info("Start processing #{job.resource_key} from id #{start_id}")
154
- process_job_with_timeouts(job, start_id, labels)
155
- end
156
- rescue => e
157
- log_fatal(e, job, worker_number)
158
- track_fatal(e, job)
159
- end
160
-
161
- def process_job_with_timeouts(job, start_id, labels)
162
- count = 0
163
- last_id = nil
164
- lock_timer = Cutoff.new(general_timeout)
165
- requeue_timer = Cutoff.new(cutoff_timeout)
166
-
167
- process_job(job, start_id, labels) do |item|
168
- job_items_counter.increment(labels, by: 1)
169
- last_id = item.id
170
- count += 1
171
- lock_timer.checkpoint!
172
- requeue_timer.checkpoint!
173
- end
174
-
175
- logger.log_info("Finish processing #{job.resource_key} at id #{last_id}")
176
- rescue Cutoff::CutoffExceededError
177
- job_timeout_counter.increment(labels, by: 1)
178
-
179
- msg = if lock_timer.exceeded?
180
- "Lock timeout"
181
- elsif requeue_timer.exceeded?
182
- redis.call("SET", "#{job.resource_path}:last_id", last_id, "EX", general_timeout) if last_id
183
- "Requeue timeout"
184
- end
185
- raise "Unknown timer has been timed out" unless msg
186
-
187
- logger.log_info("#{msg} while processing #{job.resource_key} at id #{last_id}")
188
- end
189
-
190
- def process_job(job, start_id, labels)
191
- Outbox.database_switcher.use_slave do
192
- item_class = job.item_class
193
-
194
- scope = item_class
195
- .for_processing
196
- .select(:id)
197
-
198
- if item_class.has_attribute?(:bucket)
199
- scope = scope.where(bucket: job.buckets)
200
- elsif job.partition > 0
201
- raise "Could not filter by partition #{job.resource_key}"
202
- end
203
-
204
- scope.find_each(start: start_id, batch_size: batch_size) do |item|
205
- touch_thread_worker!
206
- item_execution_runtime.measure(labels) do
207
- Outbox.database_switcher.use_master do
208
- ProcessItem.call(job.item_class, item.id)
209
- end
210
- yield item
211
- end
212
- end
213
- end
214
- end
215
-
216
- def log_fatal(e, job, worker_number)
217
- backtrace = e.backtrace.join("\n") if e.respond_to?(:backtrace)
218
-
219
- logger.log_error(
220
- "Failed processing #{job.resource_key} with error: #{e.class} #{e.message}",
221
- stacktrace: backtrace
222
- )
223
- end
224
-
225
- def track_fatal(e, job)
226
- job_counter.increment(job.yabeda_labels.merge(state: "failed"))
227
-
228
- Outbox.error_tracker.error(e, **job.log_tags)
229
- end
230
- end
231
- end
232
- end
233
- end