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
data/lib/sbmt/outbox/worker.rb
DELETED
@@ -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
|