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.
@@ -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