lowkiq 1.0.0

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