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,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
|