qpush 0.1.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.
- checksums.yaml +7 -0
- data/.byebug_history +107 -0
- data/.gitignore +9 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +154 -0
- data/Rakefile +10 -0
- data/bin/console +15 -0
- data/bin/qpush-server +6 -0
- data/bin/qpush-web +1 -0
- data/bin/setup +8 -0
- data/lib/qpush.rb +2 -0
- data/lib/qpush/base.rb +12 -0
- data/lib/qpush/config.rb +70 -0
- data/lib/qpush/job.rb +72 -0
- data/lib/qpush/jobs/test_job.rb +5 -0
- data/lib/qpush/redis.rb +17 -0
- data/lib/qpush/server.rb +18 -0
- data/lib/qpush/server/database.rb +13 -0
- data/lib/qpush/server/delay.rb +68 -0
- data/lib/qpush/server/errors.rb +18 -0
- data/lib/qpush/server/execute.rb +92 -0
- data/lib/qpush/server/jobs.rb +139 -0
- data/lib/qpush/server/launcher.rb +47 -0
- data/lib/qpush/server/logger.rb +38 -0
- data/lib/qpush/server/manager.rb +78 -0
- data/lib/qpush/server/perform.rb +39 -0
- data/lib/qpush/server/queue.rb +39 -0
- data/lib/qpush/server/worker.rb +92 -0
- data/lib/qpush/version.rb +4 -0
- data/lib/qpush/web.rb +7 -0
- data/lib/qpush/web/get.rb +60 -0
- data/lib/qpush/web/server.rb +25 -0
- data/lib/qpush/web/server.ru +3 -0
- data/qpush.gemspec +31 -0
- metadata +223 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
module QPush
|
2
|
+
module Server
|
3
|
+
# Handles the start of the QPush server via command line
|
4
|
+
#
|
5
|
+
class Launcher
|
6
|
+
def initialize(argv)
|
7
|
+
@argv = argv
|
8
|
+
end
|
9
|
+
|
10
|
+
# Parses commmand line options and starts the Manager object
|
11
|
+
#
|
12
|
+
def start
|
13
|
+
start_message
|
14
|
+
setup_options
|
15
|
+
boot_manager
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def start_message
|
21
|
+
Server.log.info('QPush Server starting!')
|
22
|
+
Server.log.info("* Version #{QPush::VERSION}, codename: #{QPush::CODENAME}")
|
23
|
+
end
|
24
|
+
|
25
|
+
# Parses the arguments passed through the command line.
|
26
|
+
#
|
27
|
+
def setup_options
|
28
|
+
parser = OptionParser.new do |o|
|
29
|
+
o.banner = 'Usage: bundle exec bin/QPush [options]'
|
30
|
+
|
31
|
+
o.on('-c', '--config PATH', 'Load PATH for config file') do |arg|
|
32
|
+
load(arg)
|
33
|
+
Server.log.info("* Server config: #{arg}")
|
34
|
+
end
|
35
|
+
|
36
|
+
o.on('-h', '--help', 'Prints this help') { puts o && exit }
|
37
|
+
end
|
38
|
+
parser.parse!(@argv)
|
39
|
+
end
|
40
|
+
|
41
|
+
def boot_manager
|
42
|
+
manager = Manager.new(QPush.config.manager_options)
|
43
|
+
manager.start
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module QPush
|
4
|
+
module Server
|
5
|
+
class << self
|
6
|
+
STDOUT.sync = true
|
7
|
+
|
8
|
+
def log
|
9
|
+
@log ||= Log.new
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# The Log is a simple wrapper for the Logger. It outputs log info in a
|
14
|
+
# defined manner to the console.
|
15
|
+
#
|
16
|
+
class Log
|
17
|
+
def initialize
|
18
|
+
@log = ::Logger.new(STDOUT)
|
19
|
+
@log.formatter = proc do |_severity, _datetime, _progname, msg|
|
20
|
+
"#{msg}\n"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Creates a new info log message.
|
25
|
+
#
|
26
|
+
def info(msg)
|
27
|
+
@log.info("[ \e[32mOK\e[0m ] #{msg}")
|
28
|
+
end
|
29
|
+
|
30
|
+
# Creates a new error log message.
|
31
|
+
#
|
32
|
+
def err(msg, action: :no_exit)
|
33
|
+
@log.info("[ \e[31mER\e[0m ] #{msg}")
|
34
|
+
exit 1 if action == :exit
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module QPush
|
2
|
+
module Server
|
3
|
+
# The Manager controls our Worker processes. We use it to instruct each
|
4
|
+
# of them to start and shutdown.
|
5
|
+
#
|
6
|
+
class Manager
|
7
|
+
include ObjectValidator::Validate
|
8
|
+
|
9
|
+
attr_accessor :workers, :options
|
10
|
+
attr_reader :forks
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
options.each { |key, value| send("#{key}=", value) }
|
14
|
+
@master = Process.pid
|
15
|
+
@forks = []
|
16
|
+
at_exit { shutdown }
|
17
|
+
end
|
18
|
+
|
19
|
+
# Instantiates new Worker objects, setting them with our options. We
|
20
|
+
# follow up by booting each of our Workers. Our Manager is then put to
|
21
|
+
# sleep so that our Workers can do their thing.
|
22
|
+
#
|
23
|
+
def start
|
24
|
+
validate!
|
25
|
+
start_messages
|
26
|
+
create_workers
|
27
|
+
Process.wait
|
28
|
+
end
|
29
|
+
|
30
|
+
# Shutsdown our Worker processes.
|
31
|
+
#
|
32
|
+
def shutdown
|
33
|
+
unless @forks.empty?
|
34
|
+
@forks.each { |w| Process.kill('SIGTERM', w[:pid].to_i) }
|
35
|
+
end
|
36
|
+
Process.kill('SIGTERM', @master)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Create the specified number of workers and starts them
|
42
|
+
#
|
43
|
+
def create_workers
|
44
|
+
@workers.times do |id|
|
45
|
+
pid = fork { Worker.new(@options.merge(id: id)).start }
|
46
|
+
@forks << { id: id, pid: pid }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Information about the start process
|
51
|
+
#
|
52
|
+
def start_messages
|
53
|
+
Server.log.info("* Workers: #{@workers}")
|
54
|
+
Server.log.info("* Threads: #{@options[:queue_threads]} queue, #{@options[:perform_threads]} perform, #{@options[:delay_threads]} delay")
|
55
|
+
end
|
56
|
+
|
57
|
+
# Validates our data before starting our Workers. Also instantiates our
|
58
|
+
# connection pool by pinging Redis.
|
59
|
+
#
|
60
|
+
def validate!
|
61
|
+
return if valid?
|
62
|
+
fail ServerError, errors.full_messages.join(' ')
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# The ManagerValidator ensures the data for our manager is valid before
|
67
|
+
# attempting to start it.
|
68
|
+
#
|
69
|
+
class ManagerValidator
|
70
|
+
include ObjectValidator::Validator
|
71
|
+
|
72
|
+
validates :redis, with: { proc: proc { QPush.redis.with { |c| c.ping && c.quit } },
|
73
|
+
msg: 'could not be connected with' }
|
74
|
+
validates :workers, type: Integer, greater_than: 0
|
75
|
+
validates :options, type: Hash
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module QPush
|
2
|
+
module Server
|
3
|
+
# The Perform worker runs any jobs that are queued into our Redis server.
|
4
|
+
# It will perform a 'blocking pop' on our job list until one is added.
|
5
|
+
#
|
6
|
+
class Perform
|
7
|
+
def initialize
|
8
|
+
@done = false
|
9
|
+
@lists = QPush.config.perform_lists
|
10
|
+
end
|
11
|
+
|
12
|
+
# Starts our perform process. This will run until instructed to stop.
|
13
|
+
#
|
14
|
+
def start
|
15
|
+
until @done
|
16
|
+
job = retrieve_job
|
17
|
+
job.api.execute if job
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Shutsdown our perform process.
|
22
|
+
#
|
23
|
+
def shutdown
|
24
|
+
@done = true
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Performs a 'blocking pop' on our redis job list.
|
30
|
+
#
|
31
|
+
def retrieve_job
|
32
|
+
json = QPush.redis.with { |c| c.brpop(@lists) }
|
33
|
+
Job.new(JSON.parse(json.last))
|
34
|
+
rescue => e
|
35
|
+
raise ServerError, e.message
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module QPush
|
2
|
+
module Server
|
3
|
+
# The Queue worker takes any jobs that are queued into our Redis server,
|
4
|
+
# and moves them to the appropriate list within Redis.
|
5
|
+
# It will perform a 'blocking pop' on our queue list until one is added.
|
6
|
+
#
|
7
|
+
class Queue
|
8
|
+
def initialize
|
9
|
+
@done = false
|
10
|
+
end
|
11
|
+
|
12
|
+
# Starts our queue process. This will run until instructed to stop.
|
13
|
+
#
|
14
|
+
def start
|
15
|
+
until @done
|
16
|
+
job = retrieve_job
|
17
|
+
job.api.setup if job
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Shutsdown our queue process.
|
22
|
+
#
|
23
|
+
def shutdown
|
24
|
+
@done = true
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Performs a 'blocking pop' on our redis job list.
|
30
|
+
#
|
31
|
+
def retrieve_job
|
32
|
+
json = QPush.redis.with { |c| c.brpop(QPush.config.queue_namespace) }
|
33
|
+
Job.new(JSON.parse(json.last))
|
34
|
+
rescue => e
|
35
|
+
raise ServerError, e.message
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module QPush
|
2
|
+
module Server
|
3
|
+
# The Worker manages our workers - Queue, Delay, and Perform. Each of these
|
4
|
+
# workers is alloted a number of threads. Each worker object maintains
|
5
|
+
# control of these threads through the aptly named start and shutdown
|
6
|
+
# methods.
|
7
|
+
#
|
8
|
+
class Worker
|
9
|
+
include ObjectValidator::Validate
|
10
|
+
|
11
|
+
attr_accessor :perform_threads, :queue_threads, :delay_threads, :id
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
options.each { |key, value| send("#{key}=", value) }
|
15
|
+
@pid = Process.pid
|
16
|
+
@workers = []
|
17
|
+
@threads = []
|
18
|
+
at_exit { shutdown }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Starts our new worker.
|
22
|
+
#
|
23
|
+
def start
|
24
|
+
validate!
|
25
|
+
start_message
|
26
|
+
build_threads
|
27
|
+
start_threads
|
28
|
+
end
|
29
|
+
|
30
|
+
# Shutsdown our worker as well as its threads.
|
31
|
+
#
|
32
|
+
def shutdown
|
33
|
+
shutdown_message
|
34
|
+
@workers.each(&:shutdown)
|
35
|
+
@threads.each(&:exit)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# Forks the worker and creates the actual threads (@_threads_real) for
|
41
|
+
# our Queue and Retry objects. We then start them and join them to the
|
42
|
+
# main process.
|
43
|
+
#
|
44
|
+
def start_threads
|
45
|
+
@workers.each do |worker|
|
46
|
+
@threads << Thread.new { worker.start }
|
47
|
+
end
|
48
|
+
@threads.map(&:join)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Instantiates our Queue, Perform, and Delay objects based on the number
|
52
|
+
# of threads specified for each process type. We store these objects as
|
53
|
+
# an array in @threads.
|
54
|
+
#
|
55
|
+
def build_threads
|
56
|
+
@perform_threads.times { @workers << Perform.new }
|
57
|
+
@queue_threads.times { @workers << Queue.new }
|
58
|
+
@delay_threads.times { @workers << Delay.new }
|
59
|
+
end
|
60
|
+
|
61
|
+
# Information about the start process
|
62
|
+
#
|
63
|
+
def start_message
|
64
|
+
Server.log.info("* Worker #{@id} started, pid: #{@pid}")
|
65
|
+
end
|
66
|
+
|
67
|
+
# Information about the shutdown process
|
68
|
+
#
|
69
|
+
def shutdown_message
|
70
|
+
Server.log.info("* Worker #{@id} shutdown, pid: #{@pid}")
|
71
|
+
end
|
72
|
+
|
73
|
+
# Validates our data before starting the worker.
|
74
|
+
#
|
75
|
+
def validate!
|
76
|
+
return if valid?
|
77
|
+
fail ServerError, errors.full_messages.join(' ')
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# The WorkerValidator ensures the data for our worker is valid before
|
82
|
+
# attempting to start it.
|
83
|
+
#
|
84
|
+
class WorkerValidator
|
85
|
+
include ObjectValidator::Validator
|
86
|
+
|
87
|
+
validates :perform_threads, type: Integer, greater_than: 0
|
88
|
+
validates :queue_threads, type: Integer, greater_than: 0
|
89
|
+
validates :delay_threads, type: Integer, greater_than: 0
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/qpush/web.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
module QPush
|
2
|
+
module Web
|
3
|
+
class Get
|
4
|
+
STATS = [:delayed,
|
5
|
+
:queued,
|
6
|
+
:performed,
|
7
|
+
:retries,
|
8
|
+
:success,
|
9
|
+
:failed].freeze
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def stats
|
13
|
+
stats = {}
|
14
|
+
namespace = QPush.config.stats_namespace
|
15
|
+
QPush.redis.with do |conn|
|
16
|
+
STATS.each { |s| stats[s] = conn.get("#{namespace}:#{s}") }
|
17
|
+
end
|
18
|
+
stats
|
19
|
+
end
|
20
|
+
|
21
|
+
def delays(s, e)
|
22
|
+
jobs = Get.all_delays[s, e]
|
23
|
+
|
24
|
+
jobs.map! { |job| JSON.parse(job.first).merge(perform_at: job.last) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def crons(s, e)
|
28
|
+
jobs = Get.all_delays
|
29
|
+
|
30
|
+
jobs.map! do |job|
|
31
|
+
hash = JSON.parse(job.first).merge(perform_at: job.last)
|
32
|
+
hash['cron'].empty? ? next : hash
|
33
|
+
end
|
34
|
+
|
35
|
+
jobs.compact![s, e]
|
36
|
+
|
37
|
+
{ total: jobs.count, jobs: jobs }
|
38
|
+
end
|
39
|
+
|
40
|
+
def fails(s, e)
|
41
|
+
jobs = Get.all_delays
|
42
|
+
|
43
|
+
jobs.map! do |job|
|
44
|
+
hash = JSON.parse(job.first).merge(perform_at: job.last)
|
45
|
+
hash['cron'].empty? && hash['total_fail'] > 0 ? hash : next
|
46
|
+
end
|
47
|
+
jobs.compact![s, e]
|
48
|
+
|
49
|
+
{ total: jobs.count, jobs: jobs }
|
50
|
+
end
|
51
|
+
|
52
|
+
def all_delays
|
53
|
+
QPush.redis.with do |conn|
|
54
|
+
conn.zrange(QPush.config.delay_namespace, 0, -1, with_scores: true)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module QPush
|
2
|
+
module Web
|
3
|
+
class Server < Sinatra::Base
|
4
|
+
before do
|
5
|
+
content_type :json
|
6
|
+
end
|
7
|
+
|
8
|
+
get '/stats' do
|
9
|
+
Get.stats.to_json
|
10
|
+
end
|
11
|
+
|
12
|
+
get '/delays' do
|
13
|
+
Get.delays(params[:start].to_i, params[:end].to_i).to_json
|
14
|
+
end
|
15
|
+
|
16
|
+
get '/crons' do
|
17
|
+
Get.crons(params[:start].to_i, params[:end].to_i).to_json
|
18
|
+
end
|
19
|
+
|
20
|
+
get '/fails' do
|
21
|
+
Get.fails(params[:start].to_i, params[:end].to_i).to_json
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|