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
@@ -1,233 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "redlock"
4
- require "sbmt/outbox/thread_pool"
5
-
6
- module Sbmt
7
- module Outbox
8
- class Worker
9
- Job = Struct.new(
10
- :item_class,
11
- :partition,
12
- :buckets,
13
- :log_tags,
14
- :yabeda_labels,
15
- :resource_key,
16
- :resource_path,
17
- keyword_init: true
18
- )
19
-
20
- delegate :config,
21
- :logger,
22
- :batch_process_middlewares,
23
- to: "Sbmt::Outbox"
24
- delegate :stop, to: :thread_pool
25
- delegate :general_timeout, :cutoff_timeout, :batch_size, to: "Sbmt::Outbox.config.process_items"
26
- delegate :job_counter,
27
- :job_execution_runtime,
28
- :item_execution_runtime,
29
- :job_items_counter,
30
- :job_timeout_counter,
31
- to: "Yabeda.box_worker"
32
-
33
- def initialize(boxes:, concurrency: 10)
34
- self.queue = Queue.new
35
- build_jobs(boxes).each { |job| queue << job }
36
- self.thread_pool = ThreadPool.new { queue.pop }
37
- self.concurrency = [concurrency, queue.size].min
38
- self.thread_workers = {}
39
- init_redis
40
- end
41
-
42
- def start
43
- raise "Outbox is already started" if started
44
- self.started = true
45
- self.thread_workers = {}
46
-
47
- thread_pool.start(concurrency: concurrency) do |worker_number, job|
48
- touch_thread_worker!
49
- result = ThreadPool::PROCESSED
50
- logger.with_tags(**job.log_tags.merge(worker: worker_number)) do
51
- lock_manager.lock("#{job.resource_path}:lock", general_timeout * 1000) do |locked|
52
- labels = job.yabeda_labels.merge(worker_number: worker_number)
53
-
54
- if locked
55
- job_execution_runtime.measure(labels) do
56
- ::Rails.application.executor.wrap do
57
- safe_process_job(job, worker_number, labels)
58
- end
59
- end
60
- else
61
- result = ThreadPool::SKIPPED
62
- logger.log_info("Skip processing already locked #{job.resource_key}")
63
- end
64
-
65
- job_counter.increment(labels.merge(state: locked ? "processed" : "skipped"), by: 1)
66
- end
67
- end
68
-
69
- result
70
- ensure
71
- queue << job
72
- end
73
- rescue => e
74
- Outbox.error_tracker.error(e)
75
- raise
76
- ensure
77
- self.started = false
78
- end
79
-
80
- def ready?
81
- started && thread_workers.any?
82
- end
83
-
84
- def alive?
85
- return false unless started
86
-
87
- deadline = Time.current - general_timeout
88
- thread_workers.all? do |_worker_number, time|
89
- deadline < time
90
- end
91
- end
92
-
93
- private
94
-
95
- attr_accessor :queue, :thread_pool, :concurrency, :lock_manager, :redis, :thread_workers, :started
96
-
97
- def init_redis
98
- self.redis = ConnectionPool::Wrapper.new(size: concurrency) { RedisClientFactory.build(config.redis) }
99
-
100
- client = if Gem::Version.new(Redlock::VERSION) >= Gem::Version.new("2.0.0")
101
- redis
102
- else
103
- ConnectionPool::Wrapper.new(size: concurrency) { Redis.new(config.redis) }
104
- end
105
-
106
- self.lock_manager = Redlock::Client.new([client], retry_count: 0)
107
- end
108
-
109
- def build_jobs(boxes)
110
- res = boxes.map do |item_class|
111
- partitions = (0...item_class.config.partition_size).to_a
112
- partitions.map do |partition|
113
- buckets = item_class.partition_buckets.fetch(partition)
114
- resource_key = "#{item_class.box_name}/#{partition}"
115
-
116
- Job.new(
117
- item_class: item_class,
118
- partition: partition,
119
- buckets: buckets,
120
- log_tags: {
121
- box_type: item_class.box_type,
122
- box_name: item_class.box_name,
123
- box_partition: partition,
124
- trace_id: nil
125
- },
126
- yabeda_labels: {
127
- type: item_class.box_type,
128
- name: item_class.box_name,
129
- partition: partition
130
- },
131
- resource_key: resource_key,
132
- resource_path: "sbmt/outbox/worker/#{resource_key}"
133
- )
134
- end
135
- end.flatten
136
-
137
- res.shuffle! if Outbox.config.worker.shuffle_jobs
138
- res
139
- end
140
-
141
- def touch_thread_worker!
142
- thread_workers[thread_pool.worker_number] = Time.current
143
- end
144
-
145
- def safe_process_job(job, worker_number, labels)
146
- middlewares = Middleware::Builder.new(batch_process_middlewares)
147
-
148
- middlewares.call(job) do
149
- start_id ||= redis.call("GETDEL", "#{job.resource_path}:last_id").to_i + 1
150
- logger.log_info("Start processing #{job.resource_key} from id #{start_id}")
151
- process_job_with_timeouts(job, start_id, labels)
152
- end
153
- rescue => e
154
- log_fatal(e, job, worker_number)
155
- track_fatal(e, job, worker_number)
156
- end
157
-
158
- def process_job_with_timeouts(job, start_id, labels)
159
- count = 0
160
- last_id = nil
161
- lock_timer = Cutoff.new(general_timeout)
162
- requeue_timer = Cutoff.new(cutoff_timeout)
163
-
164
- process_job(job, start_id, labels) do |item|
165
- job_items_counter.increment(labels, by: 1)
166
- last_id = item.id
167
- count += 1
168
- lock_timer.checkpoint!
169
- requeue_timer.checkpoint!
170
- end
171
-
172
- logger.log_info("Finish processing #{job.resource_key} at id #{last_id}")
173
- rescue Cutoff::CutoffExceededError
174
- job_timeout_counter.increment(labels, by: 1)
175
-
176
- msg = if lock_timer.exceeded?
177
- "Lock timeout"
178
- elsif requeue_timer.exceeded?
179
- redis.call("SET", "#{job.resource_path}:last_id", last_id, "EX", general_timeout) if last_id
180
- "Requeue timeout"
181
- end
182
- raise "Unknown timer has been timed out" unless msg
183
-
184
- logger.log_info("#{msg} while processing #{job.resource_key} at id #{last_id}")
185
- end
186
-
187
- def process_job(job, start_id, labels)
188
- Outbox.database_switcher.use_slave do
189
- item_class = job.item_class
190
-
191
- scope = item_class
192
- .for_processing
193
- .select(:id)
194
-
195
- if item_class.has_attribute?(:bucket)
196
- scope = scope.where(bucket: job.buckets)
197
- elsif job.partition > 0
198
- raise "Could not filter by partition #{job.resource_key}"
199
- end
200
-
201
- scope.find_each(start: start_id, batch_size: batch_size) do |item|
202
- touch_thread_worker!
203
- item_execution_runtime.measure(labels) do
204
- Outbox.database_switcher.use_master do
205
- ProcessItem.call(job.item_class, item.id)
206
- end
207
- yield item
208
- end
209
- end
210
- end
211
- end
212
-
213
- def log_fatal(e, job, worker_number)
214
- backtrace = e.backtrace.join("\n") if e.respond_to?(:backtrace)
215
-
216
- logger.log_error(
217
- "Failed processing #{job.resource_key} with error: #{e.class} #{e.message}",
218
- backtrace: backtrace
219
- )
220
- end
221
-
222
- def track_fatal(e, job, worker_number)
223
- job_counter
224
- .increment(
225
- job.yabeda_labels.merge(worker_number: worker_number, state: "failed"),
226
- by: 1
227
- )
228
-
229
- Outbox.error_tracker.error(e, **job.log_tags)
230
- end
231
- end
232
- end
233
- end