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
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "lowkiq"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/deploy.md
ADDED
data/docker-compose.yml
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
version: '3'
|
2
|
+
|
3
|
+
volumes:
|
4
|
+
bundle:
|
5
|
+
|
6
|
+
services:
|
7
|
+
app:
|
8
|
+
image: ruby
|
9
|
+
environment:
|
10
|
+
REDIS_URL: redis://redis:6379
|
11
|
+
ports:
|
12
|
+
- "8080:8080" # http
|
13
|
+
working_dir: /usr/src/app
|
14
|
+
volumes:
|
15
|
+
- ./:/usr/src/app
|
16
|
+
- bundle:/usr/local/bundle
|
17
|
+
depends_on:
|
18
|
+
- redis
|
19
|
+
redis:
|
20
|
+
image: redis:5-alpine
|
21
|
+
|
22
|
+
frontend:
|
23
|
+
image: node
|
24
|
+
ports:
|
25
|
+
- "8081:8081"
|
26
|
+
working_dir: /usr/src/app
|
27
|
+
volumes:
|
28
|
+
- ./frontend/:/usr/src/app
|
29
|
+
- ./assets:/usr/src/app/build
|
data/exe/lowkiq
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'lowkiq'
|
4
|
+
|
5
|
+
options = Lowkiq::OptionParser.call ARGV
|
6
|
+
server = Lowkiq::Server.build options
|
7
|
+
|
8
|
+
Signal.trap('TERM') { server.stop }
|
9
|
+
|
10
|
+
Signal.trap('INT') do
|
11
|
+
if server.stopped?
|
12
|
+
Process.exit
|
13
|
+
else
|
14
|
+
server.stop
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
Signal.trap('TTIN') do
|
19
|
+
file = "/tmp/lowkiq_ttin.txt"
|
20
|
+
|
21
|
+
File.delete file if File.exists? file
|
22
|
+
|
23
|
+
File.open(file, 'w') do |file|
|
24
|
+
Thread.list.each_with_index do |thread, idx|
|
25
|
+
file.write "== thread #{idx} == \n"
|
26
|
+
if thread.backtrace.nil?
|
27
|
+
file.write "<no backtrace available> \n"
|
28
|
+
else
|
29
|
+
thread.backtrace.each do |line|
|
30
|
+
file.write "#{line} \n"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
begin
|
38
|
+
server.start
|
39
|
+
server.join
|
40
|
+
rescue => ex
|
41
|
+
Lowkiq.last_words.call ex
|
42
|
+
raise ex
|
43
|
+
end
|
data/lib/lowkiq.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
require "connection_pool"
|
2
|
+
require "redis"
|
3
|
+
require "zlib"
|
4
|
+
require "json"
|
5
|
+
require "ostruct"
|
6
|
+
require "optparse"
|
7
|
+
|
8
|
+
require "lowkiq/version"
|
9
|
+
require "lowkiq/utils"
|
10
|
+
|
11
|
+
require "lowkiq/extend_tracker"
|
12
|
+
require "lowkiq/option_parser"
|
13
|
+
|
14
|
+
require "lowkiq/splitters/default"
|
15
|
+
require "lowkiq/splitters/by_node"
|
16
|
+
|
17
|
+
require "lowkiq/schedulers/lag"
|
18
|
+
require "lowkiq/schedulers/seq"
|
19
|
+
|
20
|
+
require "lowkiq/server"
|
21
|
+
|
22
|
+
require "lowkiq/queue/marshal"
|
23
|
+
require "lowkiq/queue/keys"
|
24
|
+
require "lowkiq/queue/fetch"
|
25
|
+
require "lowkiq/queue/queue"
|
26
|
+
require "lowkiq/queue/queue_metrics"
|
27
|
+
require "lowkiq/queue/shard_metrics"
|
28
|
+
require "lowkiq/queue/queries"
|
29
|
+
require "lowkiq/queue/actions"
|
30
|
+
require "lowkiq/worker"
|
31
|
+
require "lowkiq/shard_handler"
|
32
|
+
|
33
|
+
require "lowkiq/redis_info"
|
34
|
+
|
35
|
+
require "lowkiq/web"
|
36
|
+
|
37
|
+
module Lowkiq
|
38
|
+
class << self
|
39
|
+
attr_accessor :poll_interval, :threads_per_node,
|
40
|
+
:redis, :client_pool_size, :pool_timeout,
|
41
|
+
:server_middlewares, :on_server_init,
|
42
|
+
:build_scheduler, :build_splitter,
|
43
|
+
:last_words
|
44
|
+
|
45
|
+
def server_redis_pool
|
46
|
+
@server_redis_pool ||= ConnectionPool.new(size: threads_per_node, timeout: pool_timeout, &redis)
|
47
|
+
end
|
48
|
+
|
49
|
+
def client_redis_pool
|
50
|
+
@client_redis_pool ||= ConnectionPool.new(size: client_pool_size, timeout: pool_timeout, &redis)
|
51
|
+
end
|
52
|
+
|
53
|
+
def server_wrapper
|
54
|
+
null = -> (worker, batch, &block) { block.call }
|
55
|
+
server_middlewares.reduce(null) do |wrapper, m|
|
56
|
+
-> (worker, batch, &block) do
|
57
|
+
wrapper.call worker, batch do
|
58
|
+
m.call worker, batch, &block
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def workers
|
65
|
+
Worker.extended_modules
|
66
|
+
end
|
67
|
+
|
68
|
+
def shard_handlers
|
69
|
+
self.workers.flat_map do |w|
|
70
|
+
ShardHandler.build_many w, self.server_wrapper
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def build_lag_scheduler
|
75
|
+
Schedulers::Lag.new(
|
76
|
+
->() { sleep Lowkiq.poll_interval },
|
77
|
+
Queue::ShardMetrics.new(self.server_redis_pool)
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
def build_seq_scheduler
|
82
|
+
Schedulers::Seq.new(
|
83
|
+
->() { sleep Lowkiq.poll_interval }
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
def build_default_splitter
|
88
|
+
Lowkiq::Splitters::Default.new Lowkiq.threads_per_node
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_by_node_splitter(number_of_nodes, node_number)
|
92
|
+
Lowkiq::Splitters::ByNode.new(
|
93
|
+
number_of_nodes,
|
94
|
+
node_number,
|
95
|
+
Lowkiq.threads_per_node,
|
96
|
+
)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# defaults
|
101
|
+
self.poll_interval = 1
|
102
|
+
self.threads_per_node = 5
|
103
|
+
self.redis = ->() { Redis.new url: ENV.fetch('REDIS_URL') }
|
104
|
+
self.client_pool_size = 5
|
105
|
+
self.pool_timeout = 5
|
106
|
+
self.server_middlewares = []
|
107
|
+
self.on_server_init = ->() {}
|
108
|
+
self.build_scheduler = ->() { Lowkiq.build_lag_scheduler }
|
109
|
+
self.build_splitter = ->() { Lowkiq.build_default_splitter }
|
110
|
+
self.last_words = ->(ex) {}
|
111
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
module OptionParser
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def call(args)
|
6
|
+
options = {
|
7
|
+
}
|
8
|
+
::OptionParser.new do |parser|
|
9
|
+
parser.on("-r", "--require PATH") do |path|
|
10
|
+
options[:require] = path
|
11
|
+
end
|
12
|
+
|
13
|
+
parser.on("-h", "--help", "Prints this help") do
|
14
|
+
puts parser
|
15
|
+
exit
|
16
|
+
end
|
17
|
+
end.parse!(args)
|
18
|
+
|
19
|
+
fail "--require is required option" if options[:require].nil? || options[:require].empty?
|
20
|
+
|
21
|
+
options
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
module Queue
|
3
|
+
class Actions
|
4
|
+
def initialize(queue, queries)
|
5
|
+
@queue = queue
|
6
|
+
@queries = queries
|
7
|
+
|
8
|
+
@pool = queue.pool
|
9
|
+
@keys = Keys.new queue.name
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform_all_jobs_now
|
13
|
+
@pool.with do |redis|
|
14
|
+
uredis = Utils::Redis.new redis
|
15
|
+
redis.multi do
|
16
|
+
uredis.zresetscores @keys.all_ids_scored_by_perform_in_zset
|
17
|
+
@queue.shards.each do |shard|
|
18
|
+
uredis.zresetscores @keys.ids_scored_by_perform_in_zset(shard)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def kill_all_failed_jobs
|
25
|
+
until (jobs = @queries.range_by_retry_count('0', '+inf', limit: 100); jobs.empty?)
|
26
|
+
@queue.push_to_morgue jobs
|
27
|
+
ids = jobs.map { |j| j[:id] }
|
28
|
+
@queue.delete ids
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete_all_failed_jobs
|
33
|
+
until (jobs = @queries.range_by_retry_count('0', '+inf', limit: 100); jobs.empty?)
|
34
|
+
ids = jobs.map { |j| j[:id] }
|
35
|
+
@queue.delete ids
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def morgue_queue_up(ids)
|
40
|
+
jobs = @queries.morgue_fetch ids
|
41
|
+
return if jobs.empty?
|
42
|
+
|
43
|
+
@queue.push_back jobs
|
44
|
+
@queue.morgue_delete ids
|
45
|
+
end
|
46
|
+
|
47
|
+
def morgue_queue_up_all_jobs
|
48
|
+
until (jobs = @queries.morgue_range_by_id('-', '+', limit: 100); jobs.empty?)
|
49
|
+
@queue.push_back jobs
|
50
|
+
ids = jobs.map { |j| j[:id] }
|
51
|
+
@queue.morgue_delete ids
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def morgue_delete_all_jobs
|
56
|
+
until (jobs = @queries.morgue_range_by_id('-', '+', limit: 100); jobs.empty?)
|
57
|
+
ids = jobs.map { |j| j[:id] }
|
58
|
+
@queue.morgue_delete ids
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
module Queue
|
3
|
+
class Fetch
|
4
|
+
def initialize(name)
|
5
|
+
@keys = Keys.new name
|
6
|
+
end
|
7
|
+
|
8
|
+
def fetch(redis, strategy, ids)
|
9
|
+
resp = redis.public_send strategy do
|
10
|
+
ids.each do |id|
|
11
|
+
redis.zscore @keys.all_ids_scored_by_perform_in_zset, id
|
12
|
+
redis.zscore @keys.all_ids_scored_by_retry_count_zset, id
|
13
|
+
redis.zrange @keys.payloads_zset(id), 0, -1, with_scores: true
|
14
|
+
redis.hget @keys.errors_hash, id
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
ids.zip(resp.each_slice(4)).map do |x|
|
19
|
+
next if x[1][0].nil? # пропускаем id, если его уже нет в очереди
|
20
|
+
res = {
|
21
|
+
id: x[0],
|
22
|
+
perform_in: x[1][0],
|
23
|
+
retry_count: x[1][1],
|
24
|
+
payloads: x[1][2].map { |(payload, score)| [Marshal.load_payload(payload), score] },
|
25
|
+
error: x[1][3],
|
26
|
+
}.compact
|
27
|
+
end.compact
|
28
|
+
end
|
29
|
+
|
30
|
+
def morgue_fetch(redis, strategy, ids)
|
31
|
+
resp = redis.public_send strategy do
|
32
|
+
ids.each do |id|
|
33
|
+
redis.zscore @keys.morgue_all_ids_scored_by_updated_at_zset, id
|
34
|
+
redis.zrange @keys.morgue_payloads_zset(id), 0, -1, with_scores: true
|
35
|
+
redis.hget @keys.morgue_errors_hash, id
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
ids.zip(resp.each_slice(3)).map do |x|
|
40
|
+
next if x[1][0].nil? # пропускаем id, если его уже нет в очереди
|
41
|
+
{
|
42
|
+
id: x[0],
|
43
|
+
updated_at: x[1][0],
|
44
|
+
payloads: x[1][1].map { |(payload, score)| [Marshal.load_payload(payload), score] },
|
45
|
+
error: x[1][2],
|
46
|
+
}.compact
|
47
|
+
end.compact
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Lowkiq
|
2
|
+
module Queue
|
3
|
+
class Keys
|
4
|
+
PREFIX = 'lowkiq:A:v1'.freeze
|
5
|
+
|
6
|
+
def initialize(name)
|
7
|
+
@prefix = [PREFIX, name].join(':').freeze
|
8
|
+
end
|
9
|
+
|
10
|
+
def processed_key
|
11
|
+
[@prefix, :processed].join(':')
|
12
|
+
end
|
13
|
+
|
14
|
+
def failed_key
|
15
|
+
[@prefix, :failed].join(':')
|
16
|
+
end
|
17
|
+
|
18
|
+
def all_ids_lex_zset
|
19
|
+
[@prefix, :all_ids_lex].join(':')
|
20
|
+
end
|
21
|
+
|
22
|
+
def all_ids_scored_by_perform_in_zset
|
23
|
+
[@prefix, :all_ids_scored_by_perfrom_in].join(':')
|
24
|
+
end
|
25
|
+
|
26
|
+
def all_ids_scored_by_retry_count_zset
|
27
|
+
[@prefix, :all_ids_scored_by_retry_count].join(':')
|
28
|
+
end
|
29
|
+
|
30
|
+
def ids_scored_by_perform_in_zset(shard)
|
31
|
+
[@prefix, :ids_scored_by_perform_in, shard].join(':')
|
32
|
+
end
|
33
|
+
|
34
|
+
def payloads_zset(id)
|
35
|
+
[@prefix, :payloads, id].join(':')
|
36
|
+
end
|
37
|
+
|
38
|
+
def errors_hash
|
39
|
+
[@prefix, :errors].join(':')
|
40
|
+
end
|
41
|
+
|
42
|
+
def processing_key(shard)
|
43
|
+
[@prefix, :processing, shard].join(':')
|
44
|
+
end
|
45
|
+
|
46
|
+
def processing_length_by_shard_hash
|
47
|
+
[@prefix, :processing_length_by_shard].join(':')
|
48
|
+
end
|
49
|
+
|
50
|
+
def morgue_all_ids_lex_zset
|
51
|
+
[@prefix, :morgue, :all_ids_lex].join(':')
|
52
|
+
end
|
53
|
+
|
54
|
+
def morgue_all_ids_scored_by_updated_at_zset
|
55
|
+
[@prefix, :morgue, :all_ids_scored_by_updated_at].join(':')
|
56
|
+
end
|
57
|
+
|
58
|
+
def morgue_payloads_zset(id)
|
59
|
+
[@prefix, :morgue, :payloads, id].join(':')
|
60
|
+
end
|
61
|
+
|
62
|
+
def morgue_errors_hash
|
63
|
+
[@prefix, :morgue, :errors].join(':')
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|