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.
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