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,21 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
class RedisInfo
|
3
|
+
def initialize(redis_pool)
|
4
|
+
@redis_pool = redis_pool
|
5
|
+
end
|
6
|
+
|
7
|
+
def call
|
8
|
+
@redis_pool.with do |redis|
|
9
|
+
info = redis.info
|
10
|
+
{
|
11
|
+
url: redis.connection[:id],
|
12
|
+
version: info["redis_version"],
|
13
|
+
uptime_in_days: info["uptime_in_days"],
|
14
|
+
connected_clients: info["connected_clients"],
|
15
|
+
used_memory_human: info["used_memory_human"],
|
16
|
+
used_memory_peak_human: info["used_memory_peak_human"],
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
module Schedulers
|
3
|
+
class Lag
|
4
|
+
def initialize(wait, metrics)
|
5
|
+
@metrics = metrics
|
6
|
+
@wait = wait
|
7
|
+
end
|
8
|
+
|
9
|
+
def build_job(shard_handlers)
|
10
|
+
Proc.new do
|
11
|
+
identifiers = shard_handlers.map { |sh| { queue_name: sh.queue_name, shard: sh.shard_index } }
|
12
|
+
metrics = @metrics.call identifiers
|
13
|
+
shard_handler, _lag =
|
14
|
+
shard_handlers.zip(metrics.map(&:lag))
|
15
|
+
.select { |(_, lag)| lag > 0 }
|
16
|
+
.max_by { |(_, lag)| lag }
|
17
|
+
|
18
|
+
if shard_handler
|
19
|
+
shard_handler.process
|
20
|
+
else
|
21
|
+
@wait.call
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
module Schedulers
|
3
|
+
class Seq
|
4
|
+
def initialize(wait)
|
5
|
+
@wait = wait
|
6
|
+
end
|
7
|
+
|
8
|
+
def build_job(shard_handlers)
|
9
|
+
shard_enumerator = shard_handlers.cycle
|
10
|
+
processed = []
|
11
|
+
|
12
|
+
lambda do
|
13
|
+
if processed.length == shard_handlers.length
|
14
|
+
all_failed = processed.all? { |ok| !ok }
|
15
|
+
processed.clear
|
16
|
+
if all_failed
|
17
|
+
@wait.call
|
18
|
+
return
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
shard_handler = shard_enumerator.next
|
23
|
+
processed << shard_handler.process
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
class Server
|
3
|
+
def self.build(options)
|
4
|
+
require options[:require]
|
5
|
+
Lowkiq.on_server_init.call
|
6
|
+
|
7
|
+
splitter = Lowkiq.build_splitter.call
|
8
|
+
shard_handlers_by_thread = splitter.call Lowkiq.shard_handlers
|
9
|
+
scheduler = Lowkiq.build_scheduler.call
|
10
|
+
new shard_handlers_by_thread, scheduler
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(shard_handlers_by_thread, scheduler)
|
14
|
+
@shard_handlers_by_thread = shard_handlers_by_thread
|
15
|
+
@scheduler = scheduler
|
16
|
+
@threads = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def start
|
20
|
+
@shard_handlers_by_thread.each do |handlers|
|
21
|
+
handlers.each(&:restore)
|
22
|
+
end
|
23
|
+
|
24
|
+
@threads = @shard_handlers_by_thread.map do |handlers|
|
25
|
+
job = @scheduler.build_job handlers
|
26
|
+
Thread.new do
|
27
|
+
job.call until exit_from_thread?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def stop
|
33
|
+
@stopped = true
|
34
|
+
end
|
35
|
+
|
36
|
+
def join
|
37
|
+
@threads.each(&:join)
|
38
|
+
end
|
39
|
+
|
40
|
+
def exit_from_thread?
|
41
|
+
stopped? || failed?
|
42
|
+
end
|
43
|
+
|
44
|
+
def stopped?
|
45
|
+
@stopped
|
46
|
+
end
|
47
|
+
|
48
|
+
def failed?
|
49
|
+
@threads.map(&:status).any? do |status|
|
50
|
+
status != "run" && status != "sleep"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
class ShardHandler
|
3
|
+
def self.build_many(worker, wrapper)
|
4
|
+
(0...worker.shards_count).map do |shard_index|
|
5
|
+
new shard_index, worker, wrapper
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :shard_index, :queue_name, :worker
|
10
|
+
|
11
|
+
def initialize(shard_index, worker, wrapper)
|
12
|
+
@shard_index = shard_index
|
13
|
+
@queue_name = worker.queue_name
|
14
|
+
@worker = worker
|
15
|
+
@wrapper = wrapper
|
16
|
+
@timestamp = Utils::Timestamp.method(:now)
|
17
|
+
@queue = Queue::Queue.new Lowkiq.server_redis_pool,
|
18
|
+
worker.queue_name,
|
19
|
+
worker.shards_count
|
20
|
+
end
|
21
|
+
|
22
|
+
def process
|
23
|
+
data = @queue.pop @shard_index, limit: @worker.batch_size
|
24
|
+
|
25
|
+
return false if data.empty?
|
26
|
+
|
27
|
+
begin
|
28
|
+
batch = batch_from_data data
|
29
|
+
|
30
|
+
@wrapper.call @worker, batch do
|
31
|
+
@worker.perform batch
|
32
|
+
end
|
33
|
+
|
34
|
+
@queue.ack @shard_index, :success
|
35
|
+
true
|
36
|
+
rescue => ex
|
37
|
+
fail! data, ex
|
38
|
+
back, morgue = separate data
|
39
|
+
|
40
|
+
@queue.push_back back
|
41
|
+
@queue.push_to_morgue morgue
|
42
|
+
@queue.ack @shard_index, :fail
|
43
|
+
false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def restore
|
48
|
+
data = @queue.processing_data @shard_index
|
49
|
+
return if data.nil?
|
50
|
+
@queue.push_back data
|
51
|
+
@queue.ack @shard_index
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def batch_from_data(data)
|
57
|
+
data.each_with_object({}) do |job, h|
|
58
|
+
id = job.fetch(:id)
|
59
|
+
payloads = job.fetch(:payloads).map(&:first)
|
60
|
+
h[id] = payloads
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def fail!(data, ex)
|
65
|
+
data.map! do |job|
|
66
|
+
job[:retry_count] += 1
|
67
|
+
job[:perform_in] = @timestamp.call + @worker.retry_in(job[:retry_count])
|
68
|
+
job[:error] = ex.message
|
69
|
+
job
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def separate(data)
|
74
|
+
back = []
|
75
|
+
morgue = []
|
76
|
+
|
77
|
+
data.each do |job|
|
78
|
+
id = job.fetch(:id)
|
79
|
+
payloads = job.fetch(:payloads)
|
80
|
+
retry_count = job.fetch(:retry_count)
|
81
|
+
perform_in = job.fetch(:perform_in)
|
82
|
+
error = job.fetch(:error, nil)
|
83
|
+
|
84
|
+
morgue_payload = payloads.shift if retry_count >= @worker.max_retry_count
|
85
|
+
|
86
|
+
if payloads.any?
|
87
|
+
job = {
|
88
|
+
id: id,
|
89
|
+
payloads: payloads,
|
90
|
+
retry_count: morgue_payload ? 0 : retry_count,
|
91
|
+
perform_in: morgue_payload ? @timestamp.call : perform_in,
|
92
|
+
error: error,
|
93
|
+
}.compact
|
94
|
+
back << job
|
95
|
+
end
|
96
|
+
|
97
|
+
if morgue_payload
|
98
|
+
job = {
|
99
|
+
id: id,
|
100
|
+
payloads: [morgue_payload],
|
101
|
+
error: error,
|
102
|
+
}.compact
|
103
|
+
morgue << job
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
[back, morgue]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
module Splitters
|
3
|
+
class ByNode
|
4
|
+
def initialize(number_of_nodes, node_number, threads_per_node)
|
5
|
+
@number_of_nodes = number_of_nodes
|
6
|
+
@node_number = node_number
|
7
|
+
@threads_per_node = threads_per_node
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(shard_handlers)
|
11
|
+
groups_for_nodes = Utils::Array.new(shard_handlers).in_transposed_groups(@number_of_nodes)
|
12
|
+
groups_for_node = groups_for_nodes[@node_number]
|
13
|
+
Utils::Array.new(groups_for_node)
|
14
|
+
.in_transposed_groups(@threads_per_node)
|
15
|
+
.reject(&:empty?)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
module Splitters
|
3
|
+
class Default
|
4
|
+
def initialize(threads_per_node)
|
5
|
+
@threads_per_node = threads_per_node
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(shard_handlers)
|
9
|
+
Utils::Array.new(shard_handlers)
|
10
|
+
.in_transposed_groups(@threads_per_node)
|
11
|
+
.reject(&:empty?)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/lowkiq/utils.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
module Utils
|
3
|
+
class Array
|
4
|
+
def initialize(array)
|
5
|
+
@array = array.to_a
|
6
|
+
end
|
7
|
+
|
8
|
+
def in_transposed_groups(number)
|
9
|
+
result = number.times.map { [] }
|
10
|
+
|
11
|
+
@array.each_with_index do |item, index|
|
12
|
+
group = index % number
|
13
|
+
result[group] << item
|
14
|
+
end
|
15
|
+
|
16
|
+
result
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Redis
|
21
|
+
def initialize(redis)
|
22
|
+
@redis = redis
|
23
|
+
end
|
24
|
+
|
25
|
+
def zresetscores(key)
|
26
|
+
@redis.zunionstore key, [key], weights: [0.0]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module Timestamp
|
31
|
+
def self.now
|
32
|
+
Time.now.to_i
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/lowkiq/web.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require "rack"
|
2
|
+
require "lowkiq/web/action"
|
3
|
+
require "lowkiq/web/api"
|
4
|
+
|
5
|
+
module Lowkiq
|
6
|
+
module Web
|
7
|
+
HTML = Proc.new do |env|
|
8
|
+
root_path = env['SCRIPT_NAME']
|
9
|
+
[200, {}, [<<-HTML]]
|
10
|
+
<!DOCTYPE html>
|
11
|
+
<html>
|
12
|
+
<head>
|
13
|
+
<meta charset="UTF-8">
|
14
|
+
<title>Lowkiq</title>
|
15
|
+
</head>
|
16
|
+
<body>
|
17
|
+
<div id="root"></div>
|
18
|
+
<script type="text/javascript">
|
19
|
+
window.lowkiqRoot="#{root_path}";
|
20
|
+
</script>
|
21
|
+
<script type="text/javascript" src="#{root_path}/assets/#{VERSION}/app.js"></script>
|
22
|
+
</body>
|
23
|
+
</html>
|
24
|
+
HTML
|
25
|
+
end
|
26
|
+
|
27
|
+
ASSETS = File.expand_path("#{File.dirname(__FILE__)}/../../assets")
|
28
|
+
|
29
|
+
APP = Rack::Builder.new do
|
30
|
+
map "/api" do
|
31
|
+
run Api
|
32
|
+
end
|
33
|
+
|
34
|
+
map "/assets/#{VERSION}" do
|
35
|
+
run Rack::File.new ASSETS, { 'Cache-Control' => 'public, max-age=86400' }
|
36
|
+
end
|
37
|
+
|
38
|
+
run HTML
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.call(env)
|
42
|
+
APP.call env
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
module Web
|
3
|
+
class Action
|
4
|
+
def self.segments_to_regex(segments)
|
5
|
+
prepared = segments.map do |segment|
|
6
|
+
case segment
|
7
|
+
when Symbol
|
8
|
+
"(?<#{segment}>[^\/]+)"
|
9
|
+
else
|
10
|
+
segment
|
11
|
+
end
|
12
|
+
end.join( '/' )
|
13
|
+
Regexp.new '\A' + '/' + prepared + '\z'
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(request_method, segments, &body)
|
17
|
+
@request_method = request_method
|
18
|
+
@url_pattern = self.class.segments_to_regex(segments)
|
19
|
+
@body = body
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(req)
|
23
|
+
return if @request_method != req.request_method
|
24
|
+
match = @url_pattern.match req.path_info
|
25
|
+
return unless match
|
26
|
+
data = @body.call req, match
|
27
|
+
[200, {}, [JSON.generate(data)]]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
module Web
|
3
|
+
module Api
|
4
|
+
GET = 'GET'.freeze
|
5
|
+
POST = 'POST'.freeze
|
6
|
+
|
7
|
+
ACTIONS = [
|
8
|
+
Action.new(GET, ['v1', 'stats']) do |_, _|
|
9
|
+
worker_names = Lowkiq.workers.map(&:name)
|
10
|
+
queue_names = Lowkiq.workers.map(&:queue_name)
|
11
|
+
|
12
|
+
metrics = Lowkiq::Queue::QueueMetrics
|
13
|
+
.new(Lowkiq.client_redis_pool)
|
14
|
+
.call(queue_names)
|
15
|
+
by_worker = worker_names.zip(metrics).each_with_object({}) do |(name, m), o|
|
16
|
+
o[name] = m.to_h.slice(:length, :morgue_length, :lag)
|
17
|
+
end
|
18
|
+
total = {
|
19
|
+
length: metrics.map(&:length).reduce(&:+).to_i,
|
20
|
+
morgue_length: metrics.map(&:morgue_length).reduce(&:+).to_i,
|
21
|
+
lag: metrics.map(&:lag).max.to_i,
|
22
|
+
}
|
23
|
+
{
|
24
|
+
total: total,
|
25
|
+
by_worker: by_worker,
|
26
|
+
}
|
27
|
+
end,
|
28
|
+
|
29
|
+
Action.new(GET, ['web', 'dashboard']) do |_, _|
|
30
|
+
worker_names = Lowkiq.workers.map(&:name)
|
31
|
+
queue_names = Lowkiq.workers.map(&:queue_name)
|
32
|
+
|
33
|
+
metrics = Lowkiq::Queue::QueueMetrics
|
34
|
+
.new(Lowkiq.client_redis_pool)
|
35
|
+
.call(queue_names)
|
36
|
+
|
37
|
+
queues = worker_names.zip(metrics).map do |(name, m)|
|
38
|
+
{
|
39
|
+
name: name,
|
40
|
+
lag: m.lag,
|
41
|
+
processed: m.processed,
|
42
|
+
failed: m.failed,
|
43
|
+
busy: m.busy,
|
44
|
+
enqueued: m.length, # fresh + retries
|
45
|
+
fresh: m.fresh,
|
46
|
+
retries: m.retries,
|
47
|
+
dead: m.morgue_length,
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
redis_info = Lowkiq::RedisInfo.new(Lowkiq.client_redis_pool).call
|
52
|
+
|
53
|
+
{
|
54
|
+
queues: queues,
|
55
|
+
redis_info: redis_info,
|
56
|
+
}
|
57
|
+
end,
|
58
|
+
|
59
|
+
%w[ range_by_id range_by_perform_in range_by_retry_count
|
60
|
+
morgue_range_by_id morgue_range_by_updated_at
|
61
|
+
].map do |method|
|
62
|
+
Action.new(GET, ['web', :worker, method]) do |req, match|
|
63
|
+
min = req.params['min']
|
64
|
+
max = req.params['max']
|
65
|
+
|
66
|
+
queries = match_to_worker(match).client_queries
|
67
|
+
queries.public_send method, min, max, limit: 100
|
68
|
+
end
|
69
|
+
end,
|
70
|
+
|
71
|
+
%w[ rev_range_by_id rev_range_by_perform_in rev_range_by_retry_count
|
72
|
+
morgue_rev_range_by_id morgue_rev_range_by_updated_at
|
73
|
+
].map do |method|
|
74
|
+
Action.new(GET, ['web', :worker, method]) do |req, match|
|
75
|
+
min = req.params['min']
|
76
|
+
max = req.params['max']
|
77
|
+
|
78
|
+
queries = match_to_worker(match).client_queries
|
79
|
+
queries.public_send method, max, min, limit: 100
|
80
|
+
end
|
81
|
+
end,
|
82
|
+
|
83
|
+
Action.new(GET, ['web', :worker, 'processing_data']) do |_, match|
|
84
|
+
queue = match_to_worker(match).client_queue
|
85
|
+
|
86
|
+
queue.shards.flat_map do |shard|
|
87
|
+
queue.processing_data shard
|
88
|
+
end
|
89
|
+
end,
|
90
|
+
|
91
|
+
%w[ morgue_delete ].map do |method|
|
92
|
+
Action.new(POST, ['web', :worker, method]) do |req, match|
|
93
|
+
ids = req.params['ids']
|
94
|
+
Thread.new do
|
95
|
+
queue = match_to_worker(match).client_queue
|
96
|
+
queue.public_send method, ids
|
97
|
+
end
|
98
|
+
:ok
|
99
|
+
end
|
100
|
+
end,
|
101
|
+
|
102
|
+
%w[ morgue_queue_up ].map do |method|
|
103
|
+
Action.new(POST, ['web', :worker, method]) do |req, match|
|
104
|
+
ids = req.params['ids']
|
105
|
+
Thread.new do
|
106
|
+
actions = match_to_worker(match).client_actions
|
107
|
+
actions.public_send method, ids
|
108
|
+
end
|
109
|
+
:ok
|
110
|
+
end
|
111
|
+
end,
|
112
|
+
|
113
|
+
%w[ morgue_queue_up_all_jobs morgue_delete_all_jobs
|
114
|
+
perform_all_jobs_now kill_all_failed_jobs delete_all_failed_jobs].map do |method|
|
115
|
+
Action.new(POST, ['web', :worker, method]) do |_, match|
|
116
|
+
Thread.new do
|
117
|
+
actions = match_to_worker(match).client_actions
|
118
|
+
actions.public_send method
|
119
|
+
end
|
120
|
+
:ok
|
121
|
+
end
|
122
|
+
end,
|
123
|
+
|
124
|
+
].flatten
|
125
|
+
|
126
|
+
def self.match_to_worker(match)
|
127
|
+
Lowkiq.workers.find { |w| w.name == match[:worker] }
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.call(env)
|
131
|
+
req = Rack::Request.new env
|
132
|
+
|
133
|
+
ACTIONS.each do |action|
|
134
|
+
resp = action.call req
|
135
|
+
return resp if resp
|
136
|
+
end
|
137
|
+
|
138
|
+
[404, {}, ["not found"]]
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|