lowkiq 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '058eb93d910f0a3a8ebc243c6c05df173d9e03555dcabc2f554f05f40df11306'
4
+ data.tar.gz: 5a6437e5896ced972de4722b84066ef93b30b36a1be33eb01656559c50848383
5
+ SHA512:
6
+ metadata.gz: 540ceb803a0bc1e811f28a0c15a00f6ded92bf58302bd791cb2539beafbe71f13a878792889fbb438f8dfe77820e6148278472884c4d071faa7440c98df46687
7
+ data.tar.gz: 02a7bb2ab6e80c2e223a9d515a760aeae41d8db75562612830077e2e5e9a8e79b6446814c03ea130751c338d771c1a3a4b20a2445ed6429450b0864e226aa55b
@@ -0,0 +1,3 @@
1
+ .rspec_status
2
+ /assets
3
+ /*.gem
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in lowkiq.gemspec
6
+ gemspec
@@ -0,0 +1,45 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ lowkiq (1.0.0)
5
+ connection_pool (~> 2.2, >= 2.2.2)
6
+ rack (>= 1.5.0)
7
+ redis (>= 4.0.1, < 5)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ connection_pool (2.2.2)
13
+ diff-lcs (1.3)
14
+ rack (2.0.5)
15
+ rack-test (1.1.0)
16
+ rack (>= 1.0, < 3)
17
+ rake (10.5.0)
18
+ redis (4.1.3)
19
+ rspec (3.8.0)
20
+ rspec-core (~> 3.8.0)
21
+ rspec-expectations (~> 3.8.0)
22
+ rspec-mocks (~> 3.8.0)
23
+ rspec-core (3.8.0)
24
+ rspec-support (~> 3.8.0)
25
+ rspec-expectations (3.8.1)
26
+ diff-lcs (>= 1.2.0, < 2.0)
27
+ rspec-support (~> 3.8.0)
28
+ rspec-mocks (3.8.0)
29
+ diff-lcs (>= 1.2.0, < 2.0)
30
+ rspec-support (~> 3.8.0)
31
+ rspec-support (3.8.0)
32
+
33
+ PLATFORMS
34
+ ruby
35
+
36
+ DEPENDENCIES
37
+ bundler (~> 1.16)
38
+ lowkiq!
39
+ rack-test (~> 1.1)
40
+ rake (~> 10.0)
41
+ rspec (~> 3.0)
42
+ rspec-mocks (~> 3.8)
43
+
44
+ BUNDLED WITH
45
+ 1.16.4
@@ -0,0 +1,133 @@
1
+ # Licence Agreement
2
+ On granting a non-exclusive right to use open source software
3
+
4
+ **BIA-Technologies Limited Liability Company (OOO)**, registered and operating under the laws of the Russian Federation, state registration date November 6, 2014, under the main state registration number (OGRN) 1147847386906, registered in the Interdistrict FTSI (Federal Tax Service Inspectorate) of Russia No. 23 on Saint-Petersburg (TIN (taxpayer ID number) 7810385714, RRC (registration reason code) 781001001), hereinafter referred to as the **"Licensor"**, represented by the Director General Sergey Sergeevich Barykin, acting under the Charter, guided by paragraph 1 of Article 1286.1 of the Civil Code of RF (Russian Federation), provides the user (hereinafter referred to as **"the Licensee"**) on the basis and under the terms of this licence agreement (hereinafter the “Agreement”) the non-exclusive right to use **the Lowkiq open source software**:
5
+
6
+ ## 1. SUBJECT OF AGREEMENT
7
+
8
+ 1.1. The Licensor provides the Licensee, in the manner and on the terms set forth in this Agreement, the right to use (license) **the Lowkiq open source software** (hereinafter - the "Software").
9
+
10
+ 1.2. The source code for the software is available on the website located in the Internet telecommunication network "Internet" at the address: https://github.com/bia-tech/lowkiq.
11
+
12
+ 1.3. Software characteristics, that individualize it as a unique result of intellectual activity:
13
+
14
+ 1.3.1. Functionality: Server for the streamlined parallel processing of Ruby background tasks.
15
+
16
+ 1.3.2. Requirements for working with the software: Redis >= 3.2 is required to work with the Library
17
+
18
+ 1.4. The licensor guarantees, that it is the copyright holder of the exclusive right on the software, that is not alienated, not mortgaged and not challenged.
19
+
20
+ 1.5. The software is not registered with the federal executive authority for intellectual property.
21
+
22
+ 1.6. The right to use (license) granted to the Licensee under this Agreement is non-exclusive. The Licensor reserves the right to grant licenses to other parties.
23
+
24
+ 1.7. The right to use the software (license) is issued from the moment the Licensee joins the terms of this Agreement in electronic form.
25
+
26
+ 1.8. Under this Agreement, the use of the Software by the Licensee is allowed all over the world.
27
+
28
+ ## 2. RIGHTS ASSIGNED TO THE LICENSEE ON THIS AGREEMENT
29
+
30
+ 2.1. Under this Agreement, the Licensor grants the Licensee the right to use the software in the following ways:
31
+ * reproduction (full or partial), that is, the production of one or more copies of the software or its part in any tangible form, including, but not limited to, recording the software on an electronic media, including writing to the memory of the Licensee’s computer, without restrictions on the number of such computers;
32
+ * distribution by any means and in any ways of software copies through the Licensee’s internal LAN, on the Licensee’s computer, among the employees of the Licensee, related to it with labor and (or) civil - law relations.
33
+
34
+ ## 3. PROCEDURE FOR ASSIGNMENT OF AN INSTANCE AND NON-EXCLUSIVE RIGHTS TO THE SOFTWARE
35
+
36
+ 3.1. The assignment of a copy of the software to the Licensee is carried out by downloading a copy of the software by the Licensee fro the website, located in the Internet telecommunication network “Internet” at the address: https://github.com/bia-tech/lowkiq.
37
+
38
+ ## 4. RIGHTS AND OBLIGATIONS OF THE PARTIES UNDER THIS AGREEMENT
39
+
40
+ 4.1. The licensor undertakes:
41
+
42
+ 4.1.1. assign to the Licensee the software free of the rights of third parties, in the manner prescribed by this Agreement, in a condition that allows its use on the terms of this Agreement, no later than the day the Agreement is concluded;
43
+
44
+ 4.1.2. refrain from any action that could impede the Licensee from exercising the rights granted to him under this Agreement;
45
+
46
+ 4.1.3. comply with confidentiality in accordance with section 8 of this Agreement.
47
+
48
+ 4.2. Licensee undertakes:
49
+
50
+ 4.2.1. at the request of the Licensor, provide him with the opportunity to get acquainted with accounting and other documents containing information about the use of the software;
51
+
52
+ 4.2.2. at the request of the Licensor, provide a report on the use of the software in accordance with this Agreement in the time and manner specified in such a requirement;
53
+
54
+ 4.2.3. comply with confidentiality in accordance with section 8 of this Agreement.
55
+
56
+ 4.3. Licensee has the right:
57
+
58
+ 4.3.1. carry out actions, necessary for the functioning of the software (including during use in accordance with the purpose), including recording and storing in memory of an unlimited number of Licensee's computers;
59
+
60
+ 4.3.2. study, research, test the functioning of the software for the purpose of its use in accordance with this Agreement.
61
+
62
+ Application of the provisions of clause 4.3. of this Agreement shall not contradict the normal use of the software and shall not infringe in any other way the rights and legitimate interests of the Licensor.
63
+
64
+ ## 5. REMUNERATION FOR THE ASSGNMENT OF NON-EXCLUSIVE RIGHTS UNDER THIS AGREEMENT
65
+
66
+ 5.1. Compensation for using the software under this Agreement is not pad by the Licenseeo the Licensor on the basis of paragraph 1 of clause 3 of Article 1286.1 of the Civil Code of RF.
67
+
68
+ ## 6. EXCLUSIVE SOFTWARE RIGHTS
69
+
70
+ 6.1. Exclusive rights to the software, all modules compiling the software, copied and / or included in all softwares, provided to the Licensee under this Agreement, or in its part, as well as all documentation related to the software, belong to the Licensor.
71
+
72
+ 6.2. The Licensee may not use the software not on its own behalf, and also may not use the software in ways not established by this Agreement.
73
+
74
+ 6.3. The Licensor confirms that at the time of signing this Agreement, he does not know anything about the rights of third parties, that could be violated by granting the Licensee a non-exclusive right to use the software under this Agreement.
75
+
76
+ 6.4. The Licensor will assist the Licensee in the consideration of claims of third parties related to the use of the software, in case of receipt of information about such claims from the Licensee. In the event of receipt of information on the filing of such a claim, the Licensee shall immediately inform the Licensor of all claims made by the plaintiff and provide all information that he has regarding such a dispute.
77
+
78
+ ## 7. RESPONSIBILITY OF THE PARTIES UNDER THIS AGREEMENT
79
+
80
+ 7.1. For failure to fulfill or improper fulfillment of obligations under this Agreement, the Parties are liable in accordance with the current legislation of the Russian Federation.
81
+
82
+ 7.2. The use by the Licensee of the software in a manner not provided for by this Agreement, or after the termination of the Agreement, or otherwise - beyond the rights, granted to the Licensee under the Agreement, shall entail liability for violation of exclusive rights to the result of intellectual activity, established by the current legislation of the Russian Federation.
83
+
84
+ ## 8. EMERGENCIES
85
+
86
+ 8.1. The parties are exempted from liability for full or partial failure to fulfill obligations under this Agreement,if such failure is the result of emergencies (“force majeure circumstances”), which include riots, prohibitive actions of authorities, natural disasters, fires, catastrophes and other emergencies that the Parties cannot influence. The parties will consider the document issued by the authorized body of state power and (or) the corresponding chamber of commerce to be proper evidence of the presence of the emergencies.
87
+
88
+ 8.2. The parties are obliged to notify each other in writing about the existence of emergencies within 3 (three) days from the date of their discovery.
89
+
90
+ ## 9. DISPUTE RESOLUTION PROCEDURES
91
+
92
+ 9.1. All disputes and disagreements that may arise between the Parties on issues that have not been resolved in the text of this Agreement will be resolved through negotiations (in the complaint procedure) in accordance with the current legislation of the Russian Federation. The term for responding to a claim is 14 (fourteen) calendar days.
93
+
94
+ 9.2. If the dispute is not settled during the negotiations, the disputes are referred to the Court at the location of the Plaintiff
95
+
96
+ ## 10. VALIDITY AND PROCEDURE OF TERMINATION OF THIS AGREEMENT
97
+
98
+ 10.1. This Agreement is considered concluded and comes into force from the moment the Licensee joins its terms in electronic form (clause 2 of Article 434 of the Civil Code of the Russian Federation). The term of this Agreement may not exceed the term of the exclusive right to software.
99
+
100
+ 10.2. The non-exclusive right to use the software received by the Licensee shall terminate upon the early termination or termination of this Agreement.
101
+
102
+ 10.3. In case of violation by the Licensee of any obligation under this Agreement, the Licensor has the right to send a written notice to the Licensee demanding that this obligation be performed. In case of failure to fulfill or improper fulfillment of such an obligation within 3 (three) calendar days after the receipt by the Licensee of the written notice, this Agreement may be terminated by sending a notice of unilateral cancellation of this Agreement from the Licensor the Licensee . In this case, the Agreement shall be deemed to have expired upon the expiration of 3 (three) calendar days from the date a receipt of such notification.
103
+
104
+ 10.4. After the termination of this Agreement, the Licensee shall immediately cease using the software and continue to not use it. Within 5 (five) business days from the date of termination or denouncement of the Agreement, the Licensee must destroy all copies of the software received under this Agreement.
105
+
106
+ 10.5. Termination of the Agreement does not relieve the Licensee from the fulfillment of the obligation to pay fees in accordance with this Agreement
107
+
108
+ ## 11. FINAL PROVISIONS
109
+
110
+ 11.1. None of the Parties has the right to assign their rights and obligations under the Agreement to third parties without the written consent of the other Party.
111
+
112
+ 11.2. In all other aspects, which are not provided by this Agreement, the Parties will be guided by the current legislation of the Russian Federation.
113
+
114
+ 11.3. When activating and using the software, the Licensee agrees with all the terms of this Agreement, and also agrees to the transfer, receipt and processing of the personal data.
115
+
116
+ ## 12. ADDRESS AND DETAILS OF THE LICENSOR:
117
+
118
+ LICENSOR:
119
+
120
+ OOO BIA- Technologies (Limited Liability Company)
121
+
122
+ Statutory Seat and the postal address: 196084, Russian Federation,<br>
123
+ g. Saint-Petersburg, pr. Moskovskiy, d.94, lit. A, pom. 12- H
124
+
125
+ OGRN 1147847386906
126
+
127
+ TIN/ 7810385714
128
+
129
+ RRC/ 781001001
130
+
131
+ Name and email address of the representative:<br>
132
+ Pryalkin Andrey Yuryevich<br>
133
+ Andrey.Pryalkin@bia-tech.ru<br>
@@ -0,0 +1,577 @@
1
+ # Lowkiq
2
+
3
+ Упорядоченная обработка фоновых задач.
4
+
5
+ ![dashboard](doc/dashboard.png)
6
+
7
+ ## Rationale
8
+
9
+ При использовании Sidekiq мы столкнулись с проблемами при обработке сообщений от сторонней системы.
10
+
11
+ Sidekiq не гарантирует строгого порядка сообщений, т.к. очередь обрабатывается в несколько потоков.
12
+ Например, пришло 2 сообщения: M1 и M2.
13
+ Sidekiq обработчики начинают обрабатывать их параллельно,
14
+ при этом M2 может обработаться раньше M1.
15
+
16
+ В очереди могут находиться сообщения касающиеся одной сущности.
17
+ Параллельная обработка таких сообщений приводит к:
18
+
19
+ + dead locks
20
+ + затиранию новых данных старыми
21
+
22
+ Lowkiq призван устранить эти проблемы, исключая параллельность обработки сообщений в рамках одной сущности.
23
+
24
+ ## Description
25
+
26
+ Очереди надежны, т.е. задачи не теряются в случае внезапного падения процесса.
27
+ Очереди хранятся в Redis и могут не успеть записаться на диск, в случае падения Redis.
28
+
29
+ Каждая задача имеет идентификатор. Очереди гарантируют, что не может быть ситуации,
30
+ когда несколько потоков обрабатывают задачи с одинаковыми идентификаторами.
31
+
32
+ Каждая очередь разбивается на постоянный набор шардов.
33
+ На основе идентификатора задачи выбирается шард, в который попадет задача.
34
+ Таким образом задачи с одним идентификатором всегда попадают в один и тот же шард.
35
+ Задачи шарда всегда обрабатываются одним и тем же потоком.
36
+ Это гарантирует порядок обработки задач с одинаковым идентификатором и исключает возможность блокировок.
37
+
38
+ Кроме идентификатора задача имеет полезную нагрузку или данные задачи (payload).
39
+ Задачи в очереди группируются по идентификатору.
40
+ Таким образом одновременно в обработку попадают все накопленные полезные нагрузки задачи.
41
+
42
+ Если задачи содержат изменения сущности, то обработчик их все разом применит.
43
+ Если задачи содержат снимки (версии) сущности, то обработчик может использовать только последнюю версию.
44
+
45
+ Каждой очереди соответствует воркер, содержащий логику обработки задачи.
46
+ Для обработки задач используется фиксированное количество тредов,
47
+ таким образом, добавление или удаление очереди/воркера не приводит к изменению числа тредов.
48
+ Нет смысла задавать кол-во шардов одного воркера больше, чем общее кол-во тредов.
49
+
50
+ ## Аналоги
51
+
52
+ Lowkiq можно рассматривать, в некотором смысле, как замену sidekiq, работающему с плагинами:
53
+
54
+ + [sidekiq-grouping](https://github.com/gzigzigzeo/sidekiq-grouping)
55
+ + [sidekiq-unique-jobs](https://github.com/mhenrixon/sidekiq-unique-jobs)
56
+ + [sidekiq-merger](https://github.com/dtaniwaki/sidekiq-merger)
57
+
58
+ ## Benchmark
59
+
60
+ 5 threads, 100_000 blank jobs
61
+
62
+ + lowkiq: 214 sec
63
+ + sidekiq: 29 sec
64
+
65
+ Этот [бенчмарк](examples/benchmark) показывает накладные расходы на взаимодействие с redis.
66
+ В реальных задачах разница будет не так заметна.
67
+
68
+ ## Очередь
69
+
70
+ Каждая задача в очереди имеет аттрибуты:
71
+
72
+ + `id` - идентификатор задачи (строка)
73
+ + `payloads` - сортированное множество payload'ов (объекты) по их score (вещественное число)
74
+ + `perform_in` - запланированное время начала иполнения задачи (unix timestamp, вещественное число)
75
+ + `retry_count` - количество совершённых повторов задачи (вещественное число)
76
+
77
+ `id` может быть, например, идентификатором реплицируемой сущности
78
+ `payloads` - множество,
79
+ получаемое в результате группировки полезной нагрузки задачи по `id` и отсортированное по ее `score`.
80
+ `payload` может быть объектом, т.к. сериализуется с помощью `Marshal.dump`.
81
+ `score` может быть датой (unix timestamp) создания `payload`
82
+ или ее монотонно увеличивающимся номером версии.
83
+ По умолчанию - текущий unix timestamp.
84
+ По умолчанию `perform_in` - текущий unix timestamp.
85
+ `retry_count` для новой необработанной задачи равен `-1`, для упавшей один раз - `0`,
86
+ т.е. считаются не совершённые, а запланированные повторы.
87
+
88
+ `score`, `perform_at` и `retry_count` вещественные из-за особенностей работы redis.
89
+
90
+ > Redis sorted sets use a double 64-bit floating point number to represent the score. In all the architectures we support, this is represented as an IEEE 754 floating point number, that is able to represent precisely integer numbers between -(2^53) and +(2^53) included. In more practical terms, all the integers between -9007199254740992 and 9007199254740992 are perfectly representable. Larger integers, or fractions, are internally represented in exponential form, so it is possible that you get only an approximation of the decimal number, or of the very big integer, that you set as score.
91
+
92
+ Выполнение задачи может закончиться неудачей.
93
+ В этом случае ее `retry_count` инкрементируется и по заданной формуле вычисляется новый `perform_at`,
94
+ и она ставится обратно в очередь.
95
+
96
+ В случае, когда `retry_count` становится `>=` `max_retry_count`
97
+ элемент payloads с наименьшим(старейшим) score перемещается в морг,
98
+ а оставшиеся элементы помещаются обратно в очередь, при этом
99
+ `retry_count` и `perform_at` сбрасываются в `-1` и `now()` соответственно.
100
+
101
+ ### Алгоритм расчета retry_count и perform_in
102
+
103
+ 0. задача выполнилась и упала
104
+ 1. `retry_count++`
105
+ 2. `perform_in = now + retry_in(try_count)`
106
+ 3. `if retry_count >= max_retry_count` задача перемещается в морг
107
+
108
+ | тип | `retry_count` | `perform_in` |
109
+ | --- | --- | --- |
110
+ | новая не выполнялась | -1 | задан или `now()` |
111
+ | новая упала | 0 | `now() + retry_in(0)` |
112
+ | повтор упал | 1 | `now() + retry_in(1)` |
113
+
114
+ Если `max_retry_count = 1`, то попытки прекращаются.
115
+
116
+ ### Правило слияния задач
117
+
118
+ Когда применяется:
119
+
120
+ + если в очереди была задача и добавляется еще одна с тем же id
121
+ + если при обработке возникла ошибка, а в очередь успели добавили задачу с тем же id
122
+ + если задачу из морга поставили в очередь, а в очереди уже есть задача с тем же id
123
+
124
+ Алгоритм:
125
+
126
+ + payloads объединяются, при этом выбирается минимальный score,
127
+ т.е. для одинаковых payload выигрывает самая старая
128
+ + если объединяется новая и задача из очереди,
129
+ то `perform_at` и `retry_count` берутся из задачи из очереди
130
+ + если объединяется упавшая задача и задача из очереди,
131
+ то `perform_at` и `retry_count` берутся из упавшей
132
+ + если объединяется задача из морга и задача из очереди,
133
+ то `perform_at = now()`, `retry_count = -1`
134
+
135
+ Пример:
136
+
137
+ ```
138
+ # v1 - первая версия, v2 - вторая
139
+ # #{"v1": 1} - сортированное множество одного элемента, payload - "v1", score - 1
140
+
141
+ # задача в очереди
142
+ { id: "1", payloads: #{"v1": 1, "v2": 2}, retry_count: 0, perform_at: 1536323288 }
143
+ # добавляемая задача
144
+ { id: "1", payloads: #{"v2": 3, "v3": 4}, retry_count: -1, perform_at: 1536323290 }
145
+
146
+ # результат
147
+ { id: "1", payloads: #{"v1": 1, "v2": 3, "v3": 4}, retry_count: 0, perform_at: 1536323288 }
148
+ ```
149
+
150
+ Морг - часть очереди. Задачи в морге не обрабатываются.
151
+ Задача в морге имеет следующие атрибуты:
152
+
153
+ + id - идентификатор задачи
154
+ + payloads - список
155
+
156
+ Задачи в морге можно отсортировать по дате изменения или id.
157
+
158
+ Задачу из морга можно переместить в очередь. При этом для нее `retry_count = 0`, `perform_at = now()`.
159
+
160
+ ### Api
161
+
162
+ ```ruby
163
+ module ATestWorker
164
+ extend Lowkiq::Worker
165
+
166
+ self.shards_count = 24
167
+ self.batch_size = 10
168
+ self.max_retry_count = 5
169
+
170
+ def self.retry_in(count)
171
+ 10 * (count + 1) # (i.e. 10, 20, 30, 40, 50)
172
+ end
173
+
174
+ def self.perform(paylods_by_id)
175
+ # payloads_by_id - хеш
176
+ payloads_by_id.each do |id, payloads|
177
+ # id - идентификатор задачи
178
+ # payloads отсортированы по score, от старых к новым (от минимальных к максимальным)
179
+ payloads.each do |payload|
180
+ do_some_work(id, payload)
181
+ end
182
+ end
183
+ end
184
+ end
185
+ ```
186
+
187
+ Значения по умолчанию:
188
+
189
+ ```ruby
190
+ self.shards_count = 5
191
+ self.batch_size = 1
192
+ self.max_retry_count = 25
193
+ self.queue_name = self.name
194
+
195
+ # i.e. 15, 16, 31, 96, 271, ... seconds + a random amount of time
196
+ def retry_in(retry_count)
197
+ (retry_count ** 4) + 15 + (rand(30) * (retry_count + 1))
198
+ end
199
+ ```
200
+
201
+ ```ruby
202
+ ATestWorker.perform_async [
203
+ { id: 0 },
204
+ { id: 1, payload: { attr: 'v1' } },
205
+ { id: 2, payload: { attr: 'v1' }, score: Time.now.to_i, perform_in: Time.now.to_i },
206
+ ]
207
+ # payload по умолчанию равен ""
208
+ # score и perform_in по умолчанию равны Time.now.to_i
209
+ ```
210
+
211
+ Вы можете переопределить `perform_async` и вычислять `id`, `score` и `perform_in` в воркере:
212
+
213
+ ```ruby
214
+ module ATestWorker
215
+ extend Lowkiq::Worker
216
+
217
+ def self.perform_async(jobs)
218
+ jobs.each do |job|
219
+ job.merge! id: job[:payload][:id]
220
+ end
221
+ super
222
+ end
223
+
224
+ def self.perform(payloads_by_id)
225
+ #...
226
+ end
227
+ end
228
+
229
+ ATestWorker.perform_async 1000.times.map { |id| { payload: {id: id} } }
230
+ ```
231
+
232
+ ### Max retry count
233
+
234
+ Исходя из `retry_in` и `max_retry_count`,
235
+ можно вычислить примерное время, которая задача проведет в очереди.
236
+
237
+ Для `retry_in`, заданного по умолчанию получается следующая таблица:
238
+
239
+ ```ruby
240
+ def retry_in(retry_count)
241
+ (retry_count ** 4) + 15 + (rand(30) * (retry_count + 1))
242
+ end
243
+ ```
244
+
245
+ | `max_retry_count` | кол-во дней жизни задачи |
246
+ | --- | --- |
247
+ | 14 | 1 |
248
+ | 16 | 2 |
249
+ | 18 | 3 |
250
+ | 19 | 5 |
251
+ | 20 | 6 |
252
+ | 21 | 8 |
253
+ | 22 | 10 |
254
+ | 23 | 13 |
255
+ | 24 | 16 |
256
+ | 25 | 20 |
257
+
258
+ `(0...25).map{ |c| retry_in c }.sum / 60 / 60 / 24`
259
+
260
+ ## Ring app
261
+
262
+ `Lowkiq::Web` - ring app.
263
+
264
+ + `/` - dashboard
265
+ + `/api/v1/stats` - длина очереди, длина морга, лаг для каждого воркера и суммарно
266
+
267
+ ## Настройка
268
+
269
+ Опции и значения по умолчанию:
270
+
271
+ + `Lowkiq.poll_interval = 1` - задержка в секундах между опросами очереди на предмет новых задач.
272
+ Используется только если на предыдущей итерации очередь оказалась пуста или случилась ошибка.
273
+ + `Lowkiq.threads_per_node = 5` - кол-во тредов для каждой ноды.
274
+ + `Lowkiq.redis = ->() { Redis.new url: ENV.fetch('REDIS_URL') }` - настройка redis.
275
+ + `Lowkiq.client_pool_size = 5` - размер пула редиса для постановки задач в очередь.
276
+ + `Lowkiq.pool_timeout = 5` - таймаут клиентского и серверного пула редиса
277
+ + `Lowkiq.server_middlewares = []` - список middleware, оборачивающих воркер.
278
+ + `Lowkiq.on_server_init = ->() {}` - выполнения кода при инициализации сервера.
279
+ + `Lowkiq.build_scheduler = ->() { Lowkiq.build_lag_scheduler }` - планировщик.
280
+ + `Lowkiq.build_splitter = ->() { Lowkiq.build_default_splitter }` - сплиттер.
281
+ + `Lowkiq.last_words = ->(ex) {}` - обработчик исключений, потомков `StandardError`, вызвавших остановку процесса.
282
+
283
+ ```ruby
284
+ $logger = Logger.new(STDOUT)
285
+
286
+ Lowkiq.server_middlewares << -> (worker, batch, &block) do
287
+ $logger.info "Started job for #{worker} #{batch}"
288
+ block.call
289
+ $logger.info "Finished job for #{worker} #{batch}"
290
+ end
291
+
292
+ Lowkiq.server_middlewares << -> (worker, batch, &block) do
293
+ begin
294
+ block.call
295
+ rescue => e
296
+ $logger.error "#{e.message} #{worker} #{batch}"
297
+ raise e
298
+ end
299
+ end
300
+ ```
301
+
302
+ ## Splitter
303
+
304
+ У каждого воркера есть несколько шардов:
305
+
306
+ ```
307
+ # worker: shard ids
308
+ worker A: 0, 1, 2
309
+ worker B: 0, 1, 2, 3
310
+ worker C: 0
311
+ worker D: 0, 1
312
+ ```
313
+
314
+ Lowkiq использует фиксированное кол-во тредов для обработки задач, следовательно нужно распределить шарды
315
+ между тредами. Этим занимается Splitter.
316
+
317
+ Чтобы определить набор шардов, которые будет обрабатывать тред, поместим их в один список:
318
+
319
+ ```
320
+ A0, A1, A2, B0, B1, B2, B3, C0, D0, D1
321
+ ```
322
+
323
+ Рассмотрим Default splitter, который равномерно распределяет шарды по тредам единственной ноды.
324
+
325
+ Если `threads_per_node` установлено в 3, то распределение будет таким:
326
+
327
+ ```
328
+ # thread id: shards
329
+ t0: A0, B0, B3, D1
330
+ t1: A1, B1, C0
331
+ t2: A2, B2, D0
332
+ ```
333
+
334
+ Помимо Default есть ByNode splitter. Он позволяет распределить нагрузку по нескольким процессам (нодам).
335
+
336
+
337
+ ```
338
+ Lowkiq.build_splitter = -> () do
339
+ Lowkiq.build_by_node_splitter(
340
+ ENV.fetch('LOWKIQ_NUMBER_OF_NODES').to_i,
341
+ ENV.fetch('LOWKIQ_NODE_NUMBER').to_i
342
+ )
343
+ end
344
+ ```
345
+
346
+ Таким образом, вместо одного процесса нужно запустить несколько и указать переменные окружения:
347
+
348
+ ```
349
+ # process 0
350
+ LOWKIQ_NUMBER_OF_NODES=2 LOWKIQ_NODE_NUMBER=0 bundle exec lowkiq -r ./lib/app.rb
351
+
352
+ # process 1
353
+ LOWKIQ_NUMBER_OF_NODES=2 LOWKIQ_NODE_NUMBER=1 bundle exec lowkiq -r ./lib/app.rb
354
+ ```
355
+
356
+ Отмечу, что общее количество тредов будет равно произведению `ENV.fetch('LOWKIQ_NUMBER_OF_NODES')` и `Lowkiq.threads_per_node`.
357
+
358
+ Вы можете написать свой сплиттер, если ваше приложение требует особого распределения шардов между тредами или нодами.
359
+
360
+ ## Scheduler
361
+
362
+ Каждый тред обрабатывает набор шардов. За выбор шарда для обработки отвечает планировщик.
363
+ Каждый поток имеет свой собственный экземпляр планировщика.
364
+
365
+ Lowkiq имеет 2 планировщика на выбор.
366
+ Первый, `Seq` - последовательно перебирает шарды.
367
+ Второй, `Lag` - выбирает шард с самой старой задачей, т.е. стремится минимизировать лаг.
368
+ Используется по умолчанию.
369
+
370
+ Планировщик задается через настройки:
371
+
372
+ ```
373
+ Lowkiq.build_scheduler = ->() { Lowkiq.build_seq_scheduler }
374
+ # или
375
+ Lowkiq.build_scheduler = ->() { Lowkiq.build_lag_scheduler }
376
+ ```
377
+
378
+ ## Исключения
379
+
380
+ `StandardError` выброшенные воркером обрабатываются с помощью middleware.
381
+ Такие исключения не приводят к остановке процесса.
382
+
383
+ Все прочие исключения приводят к остановке процесса.
384
+ При этом Lowkiq дожидается выполнения задач другими тредами.
385
+
386
+ `StandardError` выброшенные вне воркера передаются в `Lowkiq.last_words`.
387
+ Например это происходит при потере соединения к Redis или при ошибке в коде Lowkiq.
388
+
389
+ ## Изменение количества шардов воркера
390
+
391
+ Старайтесь не менять кол-во шардов.
392
+
393
+ Если вы можете отключить добавление новых заданий,
394
+ то дождитесь опустошения очередей и выкатите новую версию кода с измененным кол-вом шардов.
395
+
396
+ Если такой возможности нет, воспользуйтесь следующим сценарием.
397
+
398
+ Например, есть воркер:
399
+
400
+ ```ruby
401
+ module ATestWorker
402
+ extend Lowkiq::Worker
403
+
404
+ self.shards_count = 5
405
+
406
+ def self.perform(payloads_by_id)
407
+ some_code
408
+ end
409
+ end
410
+ ```
411
+
412
+ Теперь нужно указать новое кол-во шардов и задать новое имя очереди:
413
+
414
+ ```ruby
415
+ module ATestWorker
416
+ extend Lowkiq::Worker
417
+
418
+ self.shards_count = 10
419
+ self.queue_name = "#{self.name}_V2"
420
+
421
+ def self.perform(payloads_by_id)
422
+ some_code
423
+ end
424
+ end
425
+ ```
426
+
427
+ И добавить воркер, перекладывающий задачи из старой очереди в новую:
428
+
429
+ ```ruby
430
+ module ATestMigrationWorker
431
+ extend Lowkiq::Worker
432
+
433
+ self.shards_count = 5
434
+ self.queue_name = "ATestWorker"
435
+
436
+ def self.perform(payloads_by_id)
437
+ jobs = payloads_by_id.each_with_object([]) do |(id, payloads), acc|
438
+ payloads.each do |payload|
439
+ acc << { id: id, payload: payload }
440
+ end
441
+ end
442
+
443
+ ATestWorker.perform_async jobs
444
+ end
445
+ end
446
+ ```
447
+
448
+ ## Запуск
449
+
450
+ `lowkiq -r ./path_to_app`
451
+
452
+ `path_to_app.rb` должен загрузить приложение.
453
+ Ленивая загрузка модулей воркеров недопустима.
454
+
455
+ Redis версии >= 3.2.
456
+
457
+ ## Остановка
458
+
459
+ Послать процессу TERM или INT(Ctrl-C).
460
+ Процесс будет ждать завершения всех задач.
461
+ Обратите внимание, если очередь пуста, то на время завершения влияет величина `poll_interval`.
462
+
463
+ ## Development
464
+
465
+ ```
466
+ docker-compose run --rm --service-port app bash
467
+ bundler
468
+ rspec
469
+ cd examples/dummy ; bundle exec ../../exe/lowkiq -r ./lib/app.rb
470
+ ```
471
+
472
+ ## Debug
473
+
474
+ Получить trace всех тредов приложения:
475
+
476
+ ```
477
+ kill -TTIN <pid>
478
+ cat /tmp/lowkiq_ttin.txt
479
+ ```
480
+
481
+ ## Rails integration
482
+
483
+ ```ruby
484
+ # config/routes.rb
485
+
486
+ Rails.application.routes.draw do
487
+ # ...
488
+ mount Lowkiq::Web => '/lowkiq'
489
+ # ...
490
+ end
491
+ ```
492
+
493
+ ```ruby
494
+ # config/initializers/lowkiq.rb
495
+
496
+ # загружаем все lowkiq воркеры
497
+ Dir["#{Rails.root}/app/lowkiq_workers/**/*.rb"].each { |file| require_dependency file }
498
+
499
+ # конфигурация:
500
+ # Lowkiq.redis = -> { Redis.new url: ENV.fetch('LOWKIQ_REDIS_URL') }
501
+ # Lowkiq.threads_per_node = ENV.fetch('LOWKIQ_THREADS_PER_NODE').to_i
502
+ # Lowkiq.client_pool_size = ENV.fetch('LOWKIQ_CLIENT_POOL_SIZE').to_i
503
+ # ...
504
+
505
+ Lowkiq.server_middlewares << -> (worker, batch, &block) do
506
+ logger = Rails.logger
507
+ tag = "#{worker}-#{Thread.current.object_id}"
508
+
509
+ logger.tagged(tag) do
510
+ time_start = Time.now
511
+ logger.info "#{time_start} Started job, batch: #{batch}"
512
+ begin
513
+ block.call
514
+ rescue => e
515
+ logger.error e.message
516
+ raise e
517
+ ensure
518
+ time_end = Time.now
519
+ logger.info "#{time_end} Finished job, duration: #{time_end - time_start} sec"
520
+ end
521
+ end
522
+ end
523
+
524
+ # Sentry integration
525
+ Lowkiq.server_middlewares << -> (worker, batch, &block) do
526
+ opts = {
527
+ extra: {
528
+ lowkiq: {
529
+ worker: worker.name,
530
+ batch: batch,
531
+ }
532
+ }
533
+ }
534
+
535
+ Raven.capture opts do
536
+ block.call
537
+ end
538
+ end
539
+
540
+ # NewRelic integration
541
+ if defined? NewRelic
542
+ class NewRelicLowkiqMiddleware
543
+ include NewRelic::Agent::Instrumentation::ControllerInstrumentation
544
+
545
+ def call(worker, batch, &block)
546
+ opts = {
547
+ category: 'OtherTransaction/LowkiqJob',
548
+ class_name: worker.name,
549
+ name: :perform,
550
+ }
551
+
552
+ perform_action_with_newrelic_trace opts do
553
+ block.call
554
+ end
555
+ end
556
+ end
557
+
558
+ Lowkiq.server_middlewares << NewRelicLowkiqMiddleware.new
559
+ end
560
+
561
+ # Rails reloader, в том числе отвечает за высвобождение ActiveRecord коннектов
562
+ Lowkiq.server_middlewares << -> (worker, batch, &block) do
563
+ Rails.application.reloader.wrap do
564
+ block.call
565
+ end
566
+ end
567
+
568
+ Lowkiq.on_server_init = ->() do
569
+ [[ActiveRecord::Base, ActiveRecord::Base.configurations[Rails.env]]].each do |(klass, init_config)|
570
+ klass.connection_pool.disconnect!
571
+ config = init_config.merge 'pool' => Lowkiq.threads_per_node
572
+ klass.establish_connection(config)
573
+ end
574
+ end
575
+ ```
576
+
577
+ Запуск: `bundle exec lowkiq -r ./config/environment.rb`