lowkiq 1.0.0
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.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.rspec +3 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +45 -0
- data/LICENSE.md +133 -0
- data/README.md +577 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/deploy.md +3 -0
- data/docker-compose.yml +29 -0
- data/exe/lowkiq +43 -0
- data/lib/lowkiq.rb +111 -0
- data/lib/lowkiq/extend_tracker.rb +13 -0
- data/lib/lowkiq/option_parser.rb +24 -0
- data/lib/lowkiq/queue/actions.rb +63 -0
- data/lib/lowkiq/queue/fetch.rb +51 -0
- data/lib/lowkiq/queue/keys.rb +67 -0
- data/lib/lowkiq/queue/marshal.rb +23 -0
- data/lib/lowkiq/queue/queries.rb +143 -0
- data/lib/lowkiq/queue/queue.rb +177 -0
- data/lib/lowkiq/queue/queue_metrics.rb +80 -0
- data/lib/lowkiq/queue/shard_metrics.rb +52 -0
- data/lib/lowkiq/redis_info.rb +21 -0
- data/lib/lowkiq/schedulers/lag.rb +27 -0
- data/lib/lowkiq/schedulers/seq.rb +28 -0
- data/lib/lowkiq/server.rb +54 -0
- data/lib/lowkiq/shard_handler.rb +110 -0
- data/lib/lowkiq/splitters/by_node.rb +19 -0
- data/lib/lowkiq/splitters/default.rb +15 -0
- data/lib/lowkiq/utils.rb +36 -0
- data/lib/lowkiq/version.rb +3 -0
- data/lib/lowkiq/web.rb +45 -0
- data/lib/lowkiq/web/action.rb +31 -0
- data/lib/lowkiq/web/api.rb +142 -0
- data/lib/lowkiq/worker.rb +43 -0
- data/lowkiq.gemspec +36 -0
- metadata +206 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -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
|
data/LICENSE.md
ADDED
@@ -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>
|
data/README.md
ADDED
@@ -0,0 +1,577 @@
|
|
1
|
+
# Lowkiq
|
2
|
+
|
3
|
+
Упорядоченная обработка фоновых задач.
|
4
|
+
|
5
|
+

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