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
@@ -0,0 +1,23 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
module Queue
|
3
|
+
module Marshal
|
4
|
+
class << self
|
5
|
+
def dump_payload(data)
|
6
|
+
::Marshal.dump data
|
7
|
+
end
|
8
|
+
|
9
|
+
def load_payload(str)
|
10
|
+
::Marshal.load str
|
11
|
+
end
|
12
|
+
|
13
|
+
def dump_data(data)
|
14
|
+
::Marshal.dump data
|
15
|
+
end
|
16
|
+
|
17
|
+
def load_data(str)
|
18
|
+
::Marshal.load str
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
module Queue
|
3
|
+
class Queries
|
4
|
+
def initialize(redis_pool, name)
|
5
|
+
@pool = redis_pool
|
6
|
+
@keys = Keys.new name
|
7
|
+
@fetch = Fetch.new name
|
8
|
+
end
|
9
|
+
|
10
|
+
def range_by_id(min, max, limit: 10)
|
11
|
+
@pool.with do |redis|
|
12
|
+
ids = redis.zrangebylex(
|
13
|
+
@keys.all_ids_lex_zset,
|
14
|
+
min, max,
|
15
|
+
limit: [0, limit]
|
16
|
+
)
|
17
|
+
_fetch redis, ids
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def rev_range_by_id(max, min, limit: 10)
|
22
|
+
@pool.with do |redis|
|
23
|
+
ids = redis.zrevrangebylex(
|
24
|
+
@keys.all_ids_lex_zset,
|
25
|
+
max, min,
|
26
|
+
limit: [0, limit]
|
27
|
+
)
|
28
|
+
_fetch redis, ids
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def range_by_perform_in(min, max, limit: 10)
|
33
|
+
@pool.with do |redis|
|
34
|
+
ids = redis.zrangebyscore(
|
35
|
+
@keys.all_ids_scored_by_perform_in_zset,
|
36
|
+
min, max,
|
37
|
+
limit: [0, limit]
|
38
|
+
)
|
39
|
+
_fetch redis, ids
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def rev_range_by_perform_in(max, min, limit: 10)
|
44
|
+
@pool.with do |redis|
|
45
|
+
ids = redis.zrevrangebyscore(
|
46
|
+
@keys.all_ids_scored_by_perform_in_zset,
|
47
|
+
max, min,
|
48
|
+
limit: [0, limit]
|
49
|
+
)
|
50
|
+
_fetch redis, ids
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def range_by_retry_count(min, max, limit: 10)
|
55
|
+
@pool.with do |redis|
|
56
|
+
ids = redis.zrangebyscore(
|
57
|
+
@keys.all_ids_scored_by_retry_count_zset,
|
58
|
+
min, max,
|
59
|
+
limit: [0, limit]
|
60
|
+
)
|
61
|
+
_fetch redis, ids
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def rev_range_by_retry_count(max, min, limit: 10)
|
66
|
+
@pool.with do |redis|
|
67
|
+
ids = redis.zrevrangebyscore(
|
68
|
+
@keys.all_ids_scored_by_retry_count_zset,
|
69
|
+
max, min,
|
70
|
+
limit: [0, limit]
|
71
|
+
)
|
72
|
+
_fetch redis, ids
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def morgue_range_by_id(min, max, limit: 10)
|
77
|
+
@pool.with do |redis|
|
78
|
+
ids = redis.zrangebylex(
|
79
|
+
@keys.morgue_all_ids_lex_zset,
|
80
|
+
min, max,
|
81
|
+
limit: [0, limit]
|
82
|
+
)
|
83
|
+
_morgue_fetch redis, ids
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def morgue_rev_range_by_id(max, min, limit: 10)
|
88
|
+
@pool.with do |redis|
|
89
|
+
ids = redis.zrevrangebylex(
|
90
|
+
@keys.morgue_all_ids_lex_zset,
|
91
|
+
max, min,
|
92
|
+
limit: [0, limit]
|
93
|
+
)
|
94
|
+
_morgue_fetch redis, ids
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def morgue_range_by_updated_at(min, max, limit: 10)
|
99
|
+
@pool.with do |redis|
|
100
|
+
ids = redis.zrangebyscore(
|
101
|
+
@keys.morgue_all_ids_scored_by_updated_at_zset,
|
102
|
+
min, max,
|
103
|
+
limit: [0, limit]
|
104
|
+
)
|
105
|
+
_morgue_fetch redis, ids
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def morgue_rev_range_by_updated_at(max, min, limit: 10)
|
110
|
+
@pool.with do |redis|
|
111
|
+
ids = redis.zrevrangebyscore(
|
112
|
+
@keys.morgue_all_ids_scored_by_updated_at_zset,
|
113
|
+
max, min,
|
114
|
+
limit: [0, limit]
|
115
|
+
)
|
116
|
+
_morgue_fetch redis, ids
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def fetch(ids)
|
121
|
+
@pool.with do |redis|
|
122
|
+
_fetch redis, ids
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def morgue_fetch(ids)
|
127
|
+
@pool.with do |redis|
|
128
|
+
_morgue_fetch redis, ids
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def _fetch(redis, ids)
|
135
|
+
@fetch.fetch(redis, :multi, ids)
|
136
|
+
end
|
137
|
+
|
138
|
+
def _morgue_fetch(redis, ids)
|
139
|
+
@fetch.morgue_fetch(redis, :multi, ids)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
module Queue
|
3
|
+
class Queue
|
4
|
+
attr_reader :name, :pool
|
5
|
+
|
6
|
+
def initialize(redis_pool, name, shards_count)
|
7
|
+
@pool = redis_pool
|
8
|
+
@name = name
|
9
|
+
@shards_count = shards_count
|
10
|
+
@timestamp = Utils::Timestamp.method(:now)
|
11
|
+
@keys = Keys.new name
|
12
|
+
@fetch = Fetch.new name
|
13
|
+
end
|
14
|
+
|
15
|
+
def push(batch)
|
16
|
+
@pool.with do |redis|
|
17
|
+
redis.multi do
|
18
|
+
batch.each do |job|
|
19
|
+
id = job.fetch(:id)
|
20
|
+
perform_in = job.fetch(:perform_in, @timestamp.call)
|
21
|
+
retry_count = job.fetch(:retry_count, -1) # for testing
|
22
|
+
payload = job.fetch(:payload, "")
|
23
|
+
score = job.fetch(:score, @timestamp.call)
|
24
|
+
|
25
|
+
shard = id_to_shard id
|
26
|
+
|
27
|
+
redis.zadd @keys.all_ids_lex_zset, 0, id
|
28
|
+
redis.zadd @keys.all_ids_scored_by_perform_in_zset, perform_in, id, nx: true
|
29
|
+
redis.zadd @keys.all_ids_scored_by_retry_count_zset, retry_count, id, nx: true
|
30
|
+
|
31
|
+
redis.zadd @keys.ids_scored_by_perform_in_zset(shard), perform_in, id, nx: true
|
32
|
+
redis.zadd @keys.payloads_zset(id), score, Marshal.dump_payload(payload), nx: true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def pop(shard, limit:)
|
39
|
+
@pool.with do |redis|
|
40
|
+
data = nil
|
41
|
+
tx = redis.watch @keys.ids_scored_by_perform_in_zset(shard) do
|
42
|
+
ids = redis.zrangebyscore @keys.ids_scored_by_perform_in_zset(shard),
|
43
|
+
0, @timestamp.call,
|
44
|
+
limit: [0, limit]
|
45
|
+
|
46
|
+
if ids.empty?
|
47
|
+
redis.unwatch
|
48
|
+
return []
|
49
|
+
end
|
50
|
+
|
51
|
+
data = @fetch.fetch(redis, :pipelined, ids)
|
52
|
+
|
53
|
+
redis.multi do
|
54
|
+
_delete redis, ids
|
55
|
+
redis.set @keys.processing_key(shard), Marshal.dump_data(data)
|
56
|
+
redis.hset @keys.processing_length_by_shard_hash, shard, data.length
|
57
|
+
end
|
58
|
+
end until tx
|
59
|
+
|
60
|
+
data
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def push_back(batch)
|
65
|
+
@pool.with do |redis|
|
66
|
+
batch.each do |job|
|
67
|
+
id = job.fetch(:id)
|
68
|
+
perform_in = job.fetch(:perform_in, @timestamp.call)
|
69
|
+
retry_count = job.fetch(:retry_count, -1)
|
70
|
+
payloads = job.fetch(:payloads).map do |(payload, score)|
|
71
|
+
[score, Marshal.dump_payload(payload)]
|
72
|
+
end
|
73
|
+
error = job.fetch(:error, nil)
|
74
|
+
|
75
|
+
shard = id_to_shard id
|
76
|
+
|
77
|
+
redis.multi do
|
78
|
+
redis.zadd @keys.all_ids_lex_zset, 0, id
|
79
|
+
redis.zadd @keys.all_ids_scored_by_perform_in_zset, perform_in, id
|
80
|
+
redis.zadd @keys.all_ids_scored_by_retry_count_zset, retry_count, id
|
81
|
+
|
82
|
+
redis.zadd @keys.ids_scored_by_perform_in_zset(shard), perform_in, id
|
83
|
+
redis.zadd @keys.payloads_zset(id), payloads, nx: true
|
84
|
+
|
85
|
+
redis.hset @keys.errors_hash, id, error unless error.nil?
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def ack(shard, result = nil)
|
92
|
+
@pool.with do |redis|
|
93
|
+
length = redis.hget(@keys.processing_length_by_shard_hash, shard).to_i
|
94
|
+
redis.multi do
|
95
|
+
redis.del @keys.processing_key(shard)
|
96
|
+
redis.hdel @keys.processing_length_by_shard_hash, shard
|
97
|
+
|
98
|
+
redis.incrby @keys.processed_key, length if result == :success
|
99
|
+
redis.incrby @keys.failed_key, length if result == :fail
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def processing_data(shard)
|
105
|
+
data = @pool.with do |redis|
|
106
|
+
redis.get @keys.processing_key(shard)
|
107
|
+
end
|
108
|
+
return [] if data.nil?
|
109
|
+
|
110
|
+
Marshal.load_data data
|
111
|
+
end
|
112
|
+
|
113
|
+
def push_to_morgue(batch)
|
114
|
+
@pool.with do |redis|
|
115
|
+
batch.each do |job|
|
116
|
+
id = job.fetch(:id)
|
117
|
+
payloads = job.fetch(:payloads).map do |(payload, score)|
|
118
|
+
[score, Marshal.dump_payload(payload)]
|
119
|
+
end
|
120
|
+
error = job.fetch(:error, nil)
|
121
|
+
|
122
|
+
redis.multi do
|
123
|
+
redis.zadd @keys.morgue_all_ids_lex_zset, 0, id
|
124
|
+
redis.zadd @keys.morgue_all_ids_scored_by_updated_at_zset, @timestamp.call, id
|
125
|
+
redis.zadd @keys.morgue_payloads_zset(id), payloads, nx: true
|
126
|
+
|
127
|
+
redis.hset @keys.morgue_errors_hash, id, error unless error.nil?
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def morgue_delete(ids)
|
134
|
+
@pool.with do |redis|
|
135
|
+
redis.multi do
|
136
|
+
ids.each do |id|
|
137
|
+
redis.zrem @keys.morgue_all_ids_lex_zset, id
|
138
|
+
redis.zrem @keys.morgue_all_ids_scored_by_updated_at_zset, id
|
139
|
+
redis.del @keys.morgue_payloads_zset(id)
|
140
|
+
redis.hdel @keys.morgue_errors_hash, id
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def delete(ids)
|
147
|
+
@pool.with do |redis|
|
148
|
+
redis.multi do
|
149
|
+
_delete redis, ids
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def shards
|
155
|
+
(0...@shards_count)
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
def id_to_shard(id)
|
161
|
+
Zlib.crc32(id.to_s) % @shards_count
|
162
|
+
end
|
163
|
+
|
164
|
+
def _delete(redis, ids)
|
165
|
+
ids.each do |id|
|
166
|
+
shard = id_to_shard id
|
167
|
+
redis.zrem @keys.all_ids_lex_zset, id
|
168
|
+
redis.zrem @keys.all_ids_scored_by_perform_in_zset, id
|
169
|
+
redis.zrem @keys.all_ids_scored_by_retry_count_zset, id
|
170
|
+
redis.zrem @keys.ids_scored_by_perform_in_zset(shard), id
|
171
|
+
redis.del @keys.payloads_zset(id)
|
172
|
+
redis.hdel @keys.errors_hash, id
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
module Queue
|
3
|
+
class QueueMetrics
|
4
|
+
def initialize(redis_pool)
|
5
|
+
@redis_pool = redis_pool
|
6
|
+
@timestamp = Utils::Timestamp.method(:now)
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(queues)
|
10
|
+
result = @redis_pool.with do |redis|
|
11
|
+
#redis.pipelined do
|
12
|
+
redis.multi do
|
13
|
+
queues.each { |queue| pipeline redis, queue }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
result.each_slice(pipeline_count).map do |res|
|
18
|
+
coerce(res)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def pipeline(redis, name)
|
25
|
+
keys = Keys.new name
|
26
|
+
|
27
|
+
# fresh
|
28
|
+
redis.zcount keys.all_ids_scored_by_retry_count_zset, -1, -1
|
29
|
+
|
30
|
+
# retries
|
31
|
+
redis.zcount keys.all_ids_scored_by_retry_count_zset, 0, '+inf'
|
32
|
+
|
33
|
+
# morgue_length
|
34
|
+
redis.zcard keys.morgue_all_ids_scored_by_updated_at_zset
|
35
|
+
|
36
|
+
# lag [id, score]
|
37
|
+
redis.zrange keys.all_ids_scored_by_perform_in_zset,
|
38
|
+
0, 0, with_scores: true
|
39
|
+
# processed
|
40
|
+
redis.get keys.processed_key
|
41
|
+
|
42
|
+
# failed
|
43
|
+
redis.get keys.failed_key
|
44
|
+
|
45
|
+
# busy []
|
46
|
+
redis.hvals keys.processing_length_by_shard_hash
|
47
|
+
end
|
48
|
+
|
49
|
+
def pipeline_count
|
50
|
+
7
|
51
|
+
end
|
52
|
+
|
53
|
+
def coerce(result)
|
54
|
+
length = result[0] + result[1]
|
55
|
+
OpenStruct.new length: length,
|
56
|
+
fresh: result[0],
|
57
|
+
retries: result[1],
|
58
|
+
morgue_length: result[2],
|
59
|
+
lag: coerce_lag(result[3]),
|
60
|
+
processed: result[4].to_i,
|
61
|
+
failed: result[5].to_i,
|
62
|
+
busy: coerce_busy(result[6])
|
63
|
+
end
|
64
|
+
|
65
|
+
def coerce_lag(res)
|
66
|
+
_id, score = res.first
|
67
|
+
|
68
|
+
return 0 if score.nil?
|
69
|
+
return 1 if score == 0 # на случай Actions#perform_all_jobs_now
|
70
|
+
lag = @timestamp.call - score.to_i
|
71
|
+
return 0 if lag < 0
|
72
|
+
lag
|
73
|
+
end
|
74
|
+
|
75
|
+
def coerce_busy(res)
|
76
|
+
res.map(&:to_i).reduce(0, &:+)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
module Queue
|
3
|
+
class ShardMetrics
|
4
|
+
def initialize(redis_pool)
|
5
|
+
@redis_pool = redis_pool
|
6
|
+
@timestamp = Utils::Timestamp.method(:now)
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(queues)
|
10
|
+
result = @redis_pool.with do |redis|
|
11
|
+
redis.pipelined do
|
12
|
+
queues.each { |queue| pipeline redis, queue }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
result.each_slice(pipeline_count).map do |res|
|
17
|
+
coerce(res)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def pipeline(redis, id)
|
24
|
+
name = id[:queue_name]
|
25
|
+
shard = id[:shard]
|
26
|
+
|
27
|
+
keys = Keys.new name
|
28
|
+
|
29
|
+
# lag [id, score]
|
30
|
+
redis.zrange keys.ids_scored_by_perform_in_zset(shard),
|
31
|
+
0, 0, with_scores: true
|
32
|
+
end
|
33
|
+
|
34
|
+
def pipeline_count
|
35
|
+
1
|
36
|
+
end
|
37
|
+
|
38
|
+
def coerce(result)
|
39
|
+
OpenStruct.new lag: coerce_lag(result[0])
|
40
|
+
end
|
41
|
+
|
42
|
+
def coerce_lag(res)
|
43
|
+
_id, score = res.first
|
44
|
+
|
45
|
+
return 0 if score.nil?
|
46
|
+
lag = @timestamp.call - score.to_i
|
47
|
+
return 0 if lag < 0
|
48
|
+
lag
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|