lowkiq 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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