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,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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Lowkiq
2
+ VERSION = "1.0.0"
3
+ end
@@ -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