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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +68 -9
  3. data/app/interactors/sbmt/outbox/process_item.rb +2 -1
  4. data/app/interactors/sbmt/outbox/retry_strategies/base.rb +15 -0
  5. data/app/interactors/sbmt/outbox/retry_strategies/compacted_log.rb +2 -32
  6. data/app/interactors/sbmt/outbox/retry_strategies/exponential_backoff.rb +3 -5
  7. data/app/interactors/sbmt/outbox/retry_strategies/latest_available.rb +39 -0
  8. data/app/interactors/sbmt/outbox/retry_strategies/no_delay.rb +13 -0
  9. data/app/models/sbmt/outbox/base_item.rb +9 -8
  10. data/app/models/sbmt/outbox/base_item_config.rb +23 -4
  11. data/config/initializers/yabeda.rb +32 -5
  12. data/lib/generators/helpers/migration.rb +2 -2
  13. data/lib/sbmt/outbox/cli.rb +50 -7
  14. data/lib/sbmt/outbox/engine.rb +26 -0
  15. data/lib/sbmt/outbox/logger.rb +6 -0
  16. data/lib/sbmt/outbox/v1/thread_pool.rb +110 -0
  17. data/lib/sbmt/outbox/v1/throttler.rb +54 -0
  18. data/lib/sbmt/outbox/v1/worker.rb +231 -0
  19. data/lib/sbmt/outbox/v2/box_processor.rb +148 -0
  20. data/lib/sbmt/outbox/v2/poll_throttler/base.rb +43 -0
  21. data/lib/sbmt/outbox/v2/poll_throttler/composite.rb +42 -0
  22. data/lib/sbmt/outbox/v2/poll_throttler/fixed_delay.rb +28 -0
  23. data/lib/sbmt/outbox/v2/poll_throttler/noop.rb +17 -0
  24. data/lib/sbmt/outbox/v2/poll_throttler/rate_limited.rb +24 -0
  25. data/lib/sbmt/outbox/v2/poll_throttler/redis_queue_size.rb +46 -0
  26. data/lib/sbmt/outbox/v2/poll_throttler/redis_queue_time_lag.rb +45 -0
  27. data/lib/sbmt/outbox/v2/poll_throttler.rb +49 -0
  28. data/lib/sbmt/outbox/v2/poller.rb +180 -0
  29. data/lib/sbmt/outbox/v2/processor.rb +101 -0
  30. data/lib/sbmt/outbox/v2/redis_job.rb +42 -0
  31. data/lib/sbmt/outbox/v2/tasks/base.rb +48 -0
  32. data/lib/sbmt/outbox/v2/tasks/default.rb +17 -0
  33. data/lib/sbmt/outbox/v2/tasks/poll.rb +34 -0
  34. data/lib/sbmt/outbox/v2/tasks/process.rb +31 -0
  35. data/lib/sbmt/outbox/v2/thread_pool.rb +152 -0
  36. data/lib/sbmt/outbox/v2/throttler.rb +13 -0
  37. data/lib/sbmt/outbox/v2/worker.rb +52 -0
  38. data/lib/sbmt/outbox/version.rb +1 -1
  39. data/lib/sbmt/outbox.rb +16 -2
  40. metadata +41 -5
  41. data/lib/sbmt/outbox/thread_pool.rb +0 -108
  42. data/lib/sbmt/outbox/throttler.rb +0 -52
  43. 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Outbox
5
+ module V2
6
+ module Throttler
7
+ THROTTLE_STATUS = "throttle"
8
+ SKIP_STATUS = "skip"
9
+ NOOP_STATUS = "noop"
10
+ end
11
+ end
12
+ end
13
+ 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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sbmt
4
4
  module Outbox
5
- VERSION = "5.0.1"
5
+ VERSION = "6.0.0"
6
6
  end
7
7
  end
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: 5.0.1
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-02-29 00:00:00.000000000 Z
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