lowkiq 1.0.0 → 1.0.1

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.
data/README.ru.md ADDED
@@ -0,0 +1,645 @@
1
+ [![Gem Version](https://badge.fury.io/rb/lowkiq.svg)](https://badge.fury.io/rb/lowkiq)
2
+
3
+ # Lowkiq
4
+
5
+ Упорядоченная обработка фоновых задач.
6
+
7
+ ![dashboard](doc/dashboard.png)
8
+
9
+ * [Rationale](#rationale)
10
+ * [Description](#description)
11
+ * [Sidekiq](#sidekiq)
12
+ * [Очередь](#%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C)
13
+ + [Алгоритм расчета retry_count и perform_in](#%D0%B0%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC-%D1%80%D0%B0%D1%81%D1%87%D0%B5%D1%82%D0%B0-retry_count-%D0%B8-perform_in)
14
+ + [Правило слияния задач](#%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D0%BB%D0%BE-%D1%81%D0%BB%D0%B8%D1%8F%D0%BD%D0%B8%D1%8F-%D0%B7%D0%B0%D0%B4%D0%B0%D1%87)
15
+ * [Install](#install)
16
+ * [Api](#api)
17
+ * [Ring app](#ring-app)
18
+ * [Настройка](#%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B0)
19
+ * [Запуск](#%D0%B7%D0%B0%D0%BF%D1%83%D1%81%D0%BA)
20
+ * [Остановка](#%D0%BE%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0)
21
+ * [Debug](#debug)
22
+ * [Development](#development)
23
+ * [Исключения](#%D0%B8%D1%81%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D1%8F)
24
+ * [Rails integration](#rails-integration)
25
+ * [Splitter](#splitter)
26
+ * [Scheduler](#scheduler)
27
+ * [Рекомендации по настройке](#%D1%80%D0%B5%D0%BA%D0%BE%D0%BC%D0%B5%D0%BD%D0%B4%D0%B0%D1%86%D0%B8%D0%B8-%D0%BF%D0%BE-%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B5)
28
+ + [`SomeWorker.shards_count`](#someworkershards_count)
29
+ + [`SomeWorker.max_retry_count`](#someworkermax_retry_count)
30
+ * [Изменение количества шардов воркера](#%D0%B8%D0%B7%D0%BC%D0%B5%D0%BD%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BA%D0%BE%D0%BB%D0%B8%D1%87%D0%B5%D1%81%D1%82%D0%B2%D0%B0-%D1%88%D0%B0%D1%80%D0%B4%D0%BE%D0%B2-%D0%B2%D0%BE%D1%80%D0%BA%D0%B5%D1%80%D0%B0)
31
+
32
+ ## Rationale
33
+
34
+ При использовании Sidekiq мы столкнулись с проблемами при обработке сообщений от сторонней системы.
35
+
36
+ Скажем, сообщение представляет собой данные заказа в определенный момент времени.
37
+ При изменении атрибутов или статуса отправляется новое сообщение сторонней системой.
38
+ Заказы обновляются часто и в очереди рядом находятся сообщения, касающиеся одного и того же заказа.
39
+
40
+ Sidekiq не гарантирует строгого порядка сообщений, т.к. очередь обрабатывается в несколько потоков.
41
+ Например, пришло 2 сообщения: M1 и M2.
42
+ Sidekiq обработчики начинают обрабатывать их параллельно,
43
+ при этом M2 может обработаться раньше M1.
44
+
45
+ Параллельная обработка данных одного заказа приводит к:
46
+
47
+ + dead locks
48
+ + затиранию новых данных старыми
49
+
50
+ Lowkiq призван устранить эти проблемы, исключая параллельность обработки сообщений в рамках одной сущности.
51
+
52
+ ## Description
53
+
54
+ Очереди надежны. Lowkiq сохраняет данные об обрабатываемой задаче и при запуске переносит
55
+ незавершенные задачи обратно в очередь.
56
+
57
+ Задачи в очереди отсортированы по заданному времени исполнения, т.е. это не FIFO очереди.
58
+
59
+ Каждая задача имеет идентификатор. Очереди гарантируют, что не может быть ситуации,
60
+ когда несколько потоков обрабатывают задачи с одинаковыми идентификаторами.
61
+
62
+ Каждая очередь разбивается на постоянный набор шардов.
63
+ На основе идентификатора задачи выбирается шард, в который попадет задача.
64
+ Таким образом задачи с одним идентификатором всегда попадают в один и тот же шард.
65
+ Задачи шарда всегда обрабатываются одним и тем же потоком.
66
+ Это гарантирует порядок обработки задач с одинаковым идентификатором и исключает возможность блокировок.
67
+
68
+ Кроме идентификатора задача имеет полезную нагрузку или данные задачи (payload).
69
+ Для задач с одинаковым идентификаторм происходит слияние полезных нагрузок.
70
+ Таким образом одновременно в обработку попадают все накопленные полезные нагрузки задачи.
71
+ Это полезно, если нужно обработать только последнее сообщение и отбросить все предыдущие.
72
+
73
+ Каждой очереди соответствует воркер, содержащий логику обработки задачи.
74
+
75
+ Для обработки всех задач всех очередей используется фиксированное количество тредов.
76
+ Добавление или удаление очередей или их шардов не приводит к изменению числа тредов.
77
+
78
+ ## Sidekiq
79
+
80
+ Если для ваших задач подходит Sidekiq - используйте его.
81
+
82
+ Если вы используете плагины вроде
83
+ [sidekiq-grouping](https://github.com/gzigzigzeo/sidekiq-grouping),
84
+ [sidekiq-unique-jobs](https://github.com/mhenrixon/sidekiq-unique-jobs),
85
+ [sidekiq-merger](https://github.com/dtaniwaki/sidekiq-merger)
86
+ или реализуете собственный механизм блокировок, то стоит рассмотреть Lowkiq.
87
+
88
+ Например, sidekiq-grouping предварительно накапливает пачку задач, ставит ее в очередь и начинает накапливать следующую.
89
+ При таком подходе случается ситуация, когда в очереди находятся 2 пачки с данными одного заказа.
90
+ Эти пачки начинают обрабатываться одновременно разными тредами, что приводит к изначальной проблеме.
91
+
92
+ Lowkiq изначально проектировался так, чтобы не использовать любые блокировки.
93
+
94
+ Кроме того, в Lowkiq очереди изначально надежны. Только Sidekiq Pro или плагины добавляют такую функциональность.
95
+
96
+ Этот [бенчмарк](examples/benchmark) показывает накладные расходы на взаимодействие с redis.
97
+ Для 5 threads, 100'000 blank jobs получились результаты:
98
+
99
+ + lowkiq: 214 sec или 2,14 мс на задачу
100
+ + sidekiq: 29 sec или 0,29 мс на задачу
101
+
102
+ Эта разница связана с принципиально различным устройством очередей.
103
+ Sidekiq использует один список для всех воркеров и извлекает задачу целиком за O(1).
104
+ Lowkiq использует несколько типов данных, включая сортированные множества для хранения идентификаторов задач.
105
+ Таким образом только получение идентификатора задачи занимает O(log(N)).
106
+
107
+ ## Очередь
108
+
109
+ Каждая задача в очереди имеет аттрибуты:
110
+
111
+ + `id` - идентификатор задачи (строка)
112
+ + `payloads` - сортированное множество payload'ов (объекты) по их score (вещественное число)
113
+ + `perform_in` - запланированное время начала иполнения задачи (unix timestamp, вещественное число)
114
+ + `retry_count` - количество совершённых повторов задачи (вещественное число)
115
+
116
+ `id` может быть, например, идентификатором реплицируемой сущности
117
+ `payloads` - множество,
118
+ получаемое в результате группировки полезной нагрузки задачи по `id` и отсортированное по ее `score`.
119
+ `payload` может быть ruby объектом, т.к. сериализуется с помощью `Marshal.dump`.
120
+ `score` может быть датой (unix timestamp) создания `payload`
121
+ или ее монотонно увеличивающимся номером версии.
122
+ По умолчанию - текущий unix timestamp.
123
+ По умолчанию `perform_in` - текущий unix timestamp.
124
+ `retry_count` для новой необработанной задачи равен `-1`, для упавшей один раз - `0`,
125
+ т.е. считаются не совершённые, а запланированные повторы.
126
+
127
+ Выполнение задачи может закончиться неудачей.
128
+ В этом случае ее `retry_count` инкрементируется и по заданной формуле вычисляется новый `perform_in`,
129
+ и она ставится обратно в очередь.
130
+
131
+ В случае, когда `retry_count` становится `>=` `max_retry_count`
132
+ элемент payloads с наименьшим(старейшим) score перемещается в морг,
133
+ а оставшиеся элементы помещаются обратно в очередь, при этом
134
+ `retry_count` и `perform_in` сбрасываются в `-1` и `now()` соответственно.
135
+
136
+ ### Алгоритм расчета retry_count и perform_in
137
+
138
+ 0. задача выполнилась и упала
139
+ 1. `retry_count++`
140
+ 2. `perform_in = now + retry_in(try_count)`
141
+ 3. `if retry_count >= max_retry_count` задача перемещается в морг
142
+
143
+ | тип | `retry_count` | `perform_in` |
144
+ | --- | --- | --- |
145
+ | новая не выполнялась | -1 | задан или `now()` |
146
+ | новая упала | 0 | `now() + retry_in(0)` |
147
+ | повтор упал | 1 | `now() + retry_in(1)` |
148
+
149
+ Если `max_retry_count = 1`, то попытки прекращаются.
150
+
151
+ ### Правило слияния задач
152
+
153
+ Когда применяется:
154
+
155
+ + если в очереди была задача и добавляется еще одна с тем же id
156
+ + если при обработке возникла ошибка, а в очередь успели добавили задачу с тем же id
157
+ + если задачу из морга поставили в очередь, а в очереди уже есть задача с тем же id
158
+
159
+ Алгоритм:
160
+
161
+ + payloads объединяются, при этом выбирается минимальный score,
162
+ т.е. для одинаковых payload выигрывает самая старая
163
+ + если объединяется новая и задача из очереди,
164
+ то `perform_in` и `retry_count` берутся из задачи из очереди
165
+ + если объединяется упавшая задача и задача из очереди,
166
+ то `perform_in` и `retry_count` берутся из упавшей
167
+ + если объединяется задача из морга и задача из очереди,
168
+ то `perform_in = now()`, `retry_count = -1`
169
+
170
+ Пример:
171
+
172
+ ```
173
+ # v1 - первая версия, v2 - вторая
174
+ # #{"v1": 1} - сортированное множество одного элемента, payload - "v1", score - 1
175
+
176
+ # задача в очереди
177
+ { id: "1", payloads: #{"v1": 1, "v2": 2}, retry_count: 0, perform_in: 1536323288 }
178
+ # добавляемая задача
179
+ { id: "1", payloads: #{"v2": 3, "v3": 4}, retry_count: -1, perform_in: 1536323290 }
180
+
181
+ # результат
182
+ { id: "1", payloads: #{"v1": 1, "v2": 3, "v3": 4}, retry_count: 0, perform_in: 1536323288 }
183
+ ```
184
+
185
+ Морг - часть очереди. Задачи в морге не обрабатываются.
186
+ Задача в морге имеет следующие атрибуты:
187
+
188
+ + id - идентификатор задачи
189
+ + payloads
190
+
191
+ Задачи в морге можно отсортировать по дате изменения или id.
192
+
193
+ Задачу из морга можно переместить в очередь. При этом для нее `retry_count = 0`, `perform_in = now()`.
194
+
195
+ ## Install
196
+
197
+ ```
198
+ # Gemfile
199
+
200
+ gem 'lowkiq'
201
+ ```
202
+
203
+ Redis версии >= 3.2.
204
+
205
+ ## Api
206
+
207
+ ```ruby
208
+ module ATestWorker
209
+ extend Lowkiq::Worker
210
+
211
+ self.shards_count = 24
212
+ self.batch_size = 10
213
+ self.max_retry_count = 5
214
+
215
+ def self.retry_in(count)
216
+ 10 * (count + 1) # (i.e. 10, 20, 30, 40, 50)
217
+ end
218
+
219
+ def self.perform(payloads_by_id)
220
+ # payloads_by_id - хеш
221
+ payloads_by_id.each do |id, payloads|
222
+ # id - идентификатор задачи
223
+ # payloads отсортированы по score, от старых к новым (от минимальных к максимальным)
224
+ payloads.each do |payload|
225
+ do_some_work(id, payload)
226
+ end
227
+ end
228
+ end
229
+ end
230
+ ```
231
+
232
+ Значения по умолчанию:
233
+
234
+ ```ruby
235
+ self.shards_count = 5
236
+ self.batch_size = 1
237
+ self.max_retry_count = 25
238
+ self.queue_name = self.name
239
+
240
+ # i.e. 15, 16, 31, 96, 271, ... seconds + a random amount of time
241
+ def retry_in(retry_count)
242
+ (retry_count ** 4) + 15 + (rand(30) * (retry_count + 1))
243
+ end
244
+ ```
245
+
246
+ ```ruby
247
+ ATestWorker.perform_async [
248
+ { id: 0 },
249
+ { id: 1, payload: { attr: 'v1' } },
250
+ { id: 2, payload: { attr: 'v1' }, score: Time.now.to_i, perform_in: Time.now.to_i },
251
+ ]
252
+ # payload по умолчанию равен ""
253
+ # score и perform_in по умолчанию равны Time.now.to_i
254
+ ```
255
+
256
+ Вы можете переопределить `perform_async` и вычислять `id`, `score` и `perform_in` в воркере:
257
+
258
+ ```ruby
259
+ module ATestWorker
260
+ extend Lowkiq::Worker
261
+
262
+ def self.perform_async(jobs)
263
+ jobs.each do |job|
264
+ job.merge! id: job[:payload][:id]
265
+ end
266
+ super
267
+ end
268
+
269
+ def self.perform(payloads_by_id)
270
+ #...
271
+ end
272
+ end
273
+
274
+ ATestWorker.perform_async 1000.times.map { |id| { payload: {id: id} } }
275
+ ```
276
+
277
+ ## Ring app
278
+
279
+ `Lowkiq::Web` - ring app.
280
+
281
+ + `/` - dashboard
282
+ + `/api/v1/stats` - длина очереди, длина морга, лаг для каждого воркера и суммарно
283
+
284
+ ## Настройка
285
+
286
+ Опции и значения по умолчанию:
287
+
288
+ + `Lowkiq.poll_interval = 1` - задержка в секундах между опросами очереди на предмет новых задач.
289
+ Используется только если на предыдущей итерации очередь оказалась пуста или случилась ошибка.
290
+ + `Lowkiq.threads_per_node = 5` - кол-во тредов для каждой ноды.
291
+ + `Lowkiq.redis = ->() { Redis.new url: ENV.fetch('REDIS_URL') }` - настройка redis.
292
+ + `Lowkiq.client_pool_size = 5` - размер пула редиса для постановки задач в очередь.
293
+ + `Lowkiq.pool_timeout = 5` - таймаут клиентского и серверного пула редиса
294
+ + `Lowkiq.server_middlewares = []` - список middleware, оборачивающих воркер.
295
+ + `Lowkiq.on_server_init = ->() {}` - выполнения кода при инициализации сервера.
296
+ + `Lowkiq.build_scheduler = ->() { Lowkiq.build_lag_scheduler }` - планировщик.
297
+ + `Lowkiq.build_splitter = ->() { Lowkiq.build_default_splitter }` - сплиттер.
298
+ + `Lowkiq.last_words = ->(ex) {}` - обработчик исключений, потомков `StandardError`, вызвавших остановку процесса.
299
+
300
+ ```ruby
301
+ $logger = Logger.new(STDOUT)
302
+
303
+ Lowkiq.server_middlewares << -> (worker, batch, &block) do
304
+ $logger.info "Started job for #{worker} #{batch}"
305
+ block.call
306
+ $logger.info "Finished job for #{worker} #{batch}"
307
+ end
308
+
309
+ Lowkiq.server_middlewares << -> (worker, batch, &block) do
310
+ begin
311
+ block.call
312
+ rescue => e
313
+ $logger.error "#{e.message} #{worker} #{batch}"
314
+ raise e
315
+ end
316
+ end
317
+ ```
318
+
319
+ ## Запуск
320
+
321
+ `lowkiq -r ./path_to_app`
322
+
323
+ `path_to_app.rb` должен загрузить приложение. [Пример](examples/dummy/lib/app.rb).
324
+
325
+ Ленивая загрузка модулей воркеров недопустима.
326
+ Используйте для предварительной загрузки модулей
327
+ `require` или [`require_dependency`](https://api.rubyonrails.org/classes/ActiveSupport/Dependencies/Loadable.html#method-i-require_dependency)
328
+ для Ruby on Rails.
329
+
330
+ ## Остановка
331
+
332
+ Послать процессу TERM или INT (Ctrl-C).
333
+ Процесс будет ждать завершения всех задач.
334
+
335
+ Обратите внимание, если очередь пуста, процесс спит `poll_interval` секунд.
336
+ Таким образом завершится не позднее чем через `poll_interval` секунд.
337
+
338
+ ## Debug
339
+
340
+ Получить trace всех тредов приложения:
341
+
342
+ ```
343
+ kill -TTIN <pid>
344
+ cat /tmp/lowkiq_ttin.txt
345
+ ```
346
+
347
+ ## Development
348
+
349
+ ```
350
+ docker-compose run --rm --service-port app bash
351
+ bundle
352
+ rspec
353
+ cd examples/dummy ; bundle exec ../../exe/lowkiq -r ./lib/app.rb
354
+ ```
355
+
356
+ ## Исключения
357
+
358
+ `StandardError` выброшенные воркером обрабатываются с помощью middleware.
359
+ Такие исключения не приводят к остановке процесса.
360
+
361
+ Все прочие исключения приводят к остановке процесса.
362
+ При этом Lowkiq дожидается выполнения задач другими тредами.
363
+
364
+ `StandardError` выброшенные вне воркера передаются в `Lowkiq.last_words`.
365
+ Например это происходит при потере соединения к Redis или при ошибке в коде Lowkiq.
366
+
367
+ ## Rails integration
368
+
369
+ ```ruby
370
+ # config/routes.rb
371
+
372
+ Rails.application.routes.draw do
373
+ # ...
374
+ mount Lowkiq::Web => '/lowkiq'
375
+ # ...
376
+ end
377
+ ```
378
+
379
+ ```ruby
380
+ # config/initializers/lowkiq.rb
381
+
382
+ # загружаем все lowkiq воркеры
383
+ Dir["#{Rails.root}/app/lowkiq_workers/**/*.rb"].each { |file| require_dependency file }
384
+
385
+ # конфигурация:
386
+ # Lowkiq.redis = -> { Redis.new url: ENV.fetch('LOWKIQ_REDIS_URL') }
387
+ # Lowkiq.threads_per_node = ENV.fetch('LOWKIQ_THREADS_PER_NODE').to_i
388
+ # Lowkiq.client_pool_size = ENV.fetch('LOWKIQ_CLIENT_POOL_SIZE').to_i
389
+ # ...
390
+
391
+ Lowkiq.server_middlewares << -> (worker, batch, &block) do
392
+ logger = Rails.logger
393
+ tag = "#{worker}-#{Thread.current.object_id}"
394
+
395
+ logger.tagged(tag) do
396
+ time_start = Time.now
397
+ logger.info "#{time_start} Started job, batch: #{batch}"
398
+ begin
399
+ block.call
400
+ rescue => e
401
+ logger.error e.message
402
+ raise e
403
+ ensure
404
+ time_end = Time.now
405
+ logger.info "#{time_end} Finished job, duration: #{time_end - time_start} sec"
406
+ end
407
+ end
408
+ end
409
+
410
+ # Sentry integration
411
+ Lowkiq.server_middlewares << -> (worker, batch, &block) do
412
+ opts = {
413
+ extra: {
414
+ lowkiq: {
415
+ worker: worker.name,
416
+ batch: batch,
417
+ }
418
+ }
419
+ }
420
+
421
+ Raven.capture opts do
422
+ block.call
423
+ end
424
+ end
425
+
426
+ # NewRelic integration
427
+ if defined? NewRelic
428
+ class NewRelicLowkiqMiddleware
429
+ include NewRelic::Agent::Instrumentation::ControllerInstrumentation
430
+
431
+ def call(worker, batch, &block)
432
+ opts = {
433
+ category: 'OtherTransaction/LowkiqJob',
434
+ class_name: worker.name,
435
+ name: :perform,
436
+ }
437
+
438
+ perform_action_with_newrelic_trace opts do
439
+ block.call
440
+ end
441
+ end
442
+ end
443
+
444
+ Lowkiq.server_middlewares << NewRelicLowkiqMiddleware.new
445
+ end
446
+
447
+ # Rails reloader, в том числе отвечает за высвобождение ActiveRecord коннектов
448
+ Lowkiq.server_middlewares << -> (worker, batch, &block) do
449
+ Rails.application.reloader.wrap do
450
+ block.call
451
+ end
452
+ end
453
+
454
+ Lowkiq.on_server_init = ->() do
455
+ [[ActiveRecord::Base, ActiveRecord::Base.configurations[Rails.env]]].each do |(klass, init_config)|
456
+ klass.connection_pool.disconnect!
457
+ config = init_config.merge 'pool' => Lowkiq.threads_per_node
458
+ klass.establish_connection(config)
459
+ end
460
+ end
461
+ ```
462
+
463
+ Запуск: `bundle exec lowkiq -r ./config/environment.rb`
464
+
465
+ ## Splitter
466
+
467
+ У каждого воркера есть несколько шардов:
468
+
469
+ ```
470
+ # worker: shard ids
471
+ worker A: 0, 1, 2
472
+ worker B: 0, 1, 2, 3
473
+ worker C: 0
474
+ worker D: 0, 1
475
+ ```
476
+
477
+ Lowkiq использует фиксированное кол-во тредов для обработки задач, следовательно нужно распределить шарды
478
+ между тредами. Этим занимается Splitter.
479
+
480
+ Чтобы определить набор шардов, которые будет обрабатывать тред, поместим их в один список:
481
+
482
+ ```
483
+ A0, A1, A2, B0, B1, B2, B3, C0, D0, D1
484
+ ```
485
+
486
+ Рассмотрим Default splitter, который равномерно распределяет шарды по тредам единственной ноды.
487
+
488
+ Если `threads_per_node` установлено в 3, то распределение будет таким:
489
+
490
+ ```
491
+ # thread id: shards
492
+ t0: A0, B0, B3, D1
493
+ t1: A1, B1, C0
494
+ t2: A2, B2, D0
495
+ ```
496
+
497
+ Помимо Default есть ByNode splitter. Он позволяет распределить нагрузку по нескольким процессам (нодам).
498
+
499
+ ```
500
+ Lowkiq.build_splitter = -> () do
501
+ Lowkiq.build_by_node_splitter(
502
+ ENV.fetch('LOWKIQ_NUMBER_OF_NODES').to_i,
503
+ ENV.fetch('LOWKIQ_NODE_NUMBER').to_i
504
+ )
505
+ end
506
+ ```
507
+
508
+ Таким образом, вместо одного процесса нужно запустить несколько и указать переменные окружения:
509
+
510
+ ```
511
+ # process 0
512
+ LOWKIQ_NUMBER_OF_NODES=2 LOWKIQ_NODE_NUMBER=0 bundle exec lowkiq -r ./lib/app.rb
513
+
514
+ # process 1
515
+ LOWKIQ_NUMBER_OF_NODES=2 LOWKIQ_NODE_NUMBER=1 bundle exec lowkiq -r ./lib/app.rb
516
+ ```
517
+
518
+ Отмечу, что общее количество тредов будет равно произведению `ENV.fetch('LOWKIQ_NUMBER_OF_NODES')` и `Lowkiq.threads_per_node`.
519
+
520
+ Вы можете написать свой сплиттер, если ваше приложение требует особого распределения шардов между тредами или нодами.
521
+
522
+ ## Scheduler
523
+
524
+ Каждый тред обрабатывает набор шардов. За выбор шарда для обработки отвечает планировщик.
525
+ Каждый поток имеет свой собственный экземпляр планировщика.
526
+
527
+ Lowkiq имеет 2 планировщика на выбор.
528
+ Первый, `Seq` - последовательно перебирает шарды.
529
+ Второй, `Lag` - выбирает шард с самой старой задачей, т.е. стремится минимизировать лаг.
530
+ Используется по умолчанию.
531
+
532
+ Планировщик задается через настройки:
533
+
534
+ ```
535
+ Lowkiq.build_scheduler = ->() { Lowkiq.build_seq_scheduler }
536
+ # или
537
+ Lowkiq.build_scheduler = ->() { Lowkiq.build_lag_scheduler }
538
+ ```
539
+
540
+ ## Рекомендации по настройке
541
+
542
+ ### `SomeWorker.shards_count`
543
+
544
+ Сумма `shards_count` всех воркеров не должна быть меньше `Lowkiq.threads_per_node`
545
+ иначе треды будут простаивать.
546
+
547
+ Сумма `shards_count` всех воркеров может быть равна `Lowkiq.threads_per_node`.
548
+ В этом случае тред обрабатывает единственный шард. Это имеет смысл только при равномерной нагрузке на очереди.
549
+
550
+ Сумма `shards_count` всех воркеров может быть больше `Lowkiq.threads_per_node`.
551
+ В этом случае `shards_count` можно рассматривать в качестве приоритета.
552
+ Чем он выше, тем чаще задачи этой очереди будут обрабатываться.
553
+
554
+ Нет смысла устанавливать `shards_count` одного воркера больше чем `Lowkiq.threads_per_node`,
555
+ т.к. каждый тред будет обрабатывать более одного шарда этой очереди, что увеличит накладные расходы.
556
+
557
+ ### `SomeWorker.max_retry_count`
558
+
559
+ Исходя из `retry_in` и `max_retry_count`,
560
+ можно вычислить примерное время, которая задача проведет в очереди.
561
+ Под задачей тут понимается payload задачи.
562
+ После достижения `max_retry_count` в морг переносится только payload с минимальным score.
563
+
564
+ Для `retry_in`, заданного по умолчанию получается следующая таблица:
565
+
566
+ ```ruby
567
+ def retry_in(retry_count)
568
+ (retry_count ** 4) + 15 + (rand(30) * (retry_count + 1))
569
+ end
570
+ ```
571
+
572
+ | `max_retry_count` | кол-во дней жизни задачи |
573
+ | --- | --- |
574
+ | 14 | 1 |
575
+ | 16 | 2 |
576
+ | 18 | 3 |
577
+ | 19 | 5 |
578
+ | 20 | 6 |
579
+ | 21 | 8 |
580
+ | 22 | 10 |
581
+ | 23 | 13 |
582
+ | 24 | 16 |
583
+ | 25 | 20 |
584
+
585
+ `(0...25).map{ |c| retry_in c }.sum / 60 / 60 / 24`
586
+
587
+
588
+ ## Изменение количества шардов воркера
589
+
590
+ Старайтесь сразу расчитать количество шардов и не именять их количество в будущем.
591
+
592
+ Если вы можете отключить добавление новых заданий,
593
+ то дождитесь опустошения очередей и выкатите новую версию кода с измененным количеством шардов.
594
+
595
+ Если такой возможности нет, воспользуйтесь следующим сценарием.
596
+
597
+ Например, есть воркер:
598
+
599
+ ```ruby
600
+ module ATestWorker
601
+ extend Lowkiq::Worker
602
+
603
+ self.shards_count = 5
604
+
605
+ def self.perform(payloads_by_id)
606
+ some_code
607
+ end
608
+ end
609
+ ```
610
+
611
+ Теперь нужно указать новое кол-во шардов и задать новое имя очереди:
612
+
613
+ ```ruby
614
+ module ATestWorker
615
+ extend Lowkiq::Worker
616
+
617
+ self.shards_count = 10
618
+ self.queue_name = "#{self.name}_V2"
619
+
620
+ def self.perform(payloads_by_id)
621
+ some_code
622
+ end
623
+ end
624
+ ```
625
+
626
+ И добавить воркер, перекладывающий задачи из старой очереди в новую:
627
+
628
+ ```ruby
629
+ module ATestMigrationWorker
630
+ extend Lowkiq::Worker
631
+
632
+ self.shards_count = 5
633
+ self.queue_name = "ATestWorker"
634
+
635
+ def self.perform(payloads_by_id)
636
+ jobs = payloads_by_id.each_with_object([]) do |(id, payloads), acc|
637
+ payloads.each do |payload|
638
+ acc << { id: id, payload: payload }
639
+ end
640
+ end
641
+
642
+ ATestWorker.perform_async jobs
643
+ end
644
+ end
645
+ ```