lowkiq 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|