lowkiq 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,3 @@
1
+ + bump version in `lib/lowkiq/version.rb`
2
+ + build frontend: `npm run build`
3
+ + build gem: `gem build lowkiq.gemspec`
@@ -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
@@ -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
@@ -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,13 @@
1
+ module Lowkiq
2
+ module ExtendTracker
3
+ def extended(mod)
4
+ @extended_modules ||= []
5
+ @extended_modules << mod
6
+ @extended_modules.sort_by! &:name
7
+ end
8
+
9
+ def extended_modules
10
+ @extended_modules
11
+ end
12
+ end
13
+ 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