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