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.
@@ -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
@@ -0,0 +1,4 @@
1
+ module QPush
2
+ VERSION = '0.1.0'
3
+ CODENAME = 'Sun Soaked Salamander'
4
+ end
@@ -0,0 +1,7 @@
1
+ # Base
2
+ require 'qpush/base'
3
+
4
+ # Web
5
+ require 'sinatra/base'
6
+ require 'qpush/web/get'
7
+ require 'qpush/web/server'
@@ -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
@@ -0,0 +1,3 @@
1
+ require 'qpush/web'
2
+
3
+ run QPush::Web::Server