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
data/lib/qpush/job.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
module QPush
|
2
|
+
class << self
|
3
|
+
def job(options)
|
4
|
+
job = Job::Wrapper.new(options)
|
5
|
+
job.queue
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module Job
|
10
|
+
class << self
|
11
|
+
def included(base)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def queue(options)
|
18
|
+
QPush.job(options.merge(klass: name))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module Base
|
23
|
+
def to_json
|
24
|
+
{ klass: @klass,
|
25
|
+
id: @id,
|
26
|
+
priority: @priority,
|
27
|
+
created_at: @created_at,
|
28
|
+
start_at: @start_at,
|
29
|
+
cron: @cron,
|
30
|
+
retry_max: @retry_max,
|
31
|
+
total_fail: @total_fail,
|
32
|
+
total_success: @total_success,
|
33
|
+
args: @args }.to_json
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def defaults
|
39
|
+
{ id: SecureRandom.urlsafe_base64,
|
40
|
+
args: {},
|
41
|
+
priority: 3,
|
42
|
+
created_at: Time.now.to_i,
|
43
|
+
start_at: Time.now.to_i - 1,
|
44
|
+
cron: '',
|
45
|
+
retry_max: 10,
|
46
|
+
total_fail: 0,
|
47
|
+
total_success: 0
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Wrapper
|
53
|
+
include QPush::Job::Base
|
54
|
+
|
55
|
+
attr_accessor :klass, :id, :priority, :created_at, :start_at,
|
56
|
+
:cron, :retry_max, :total_success, :total_fail,
|
57
|
+
:args
|
58
|
+
|
59
|
+
def initialize(options = {})
|
60
|
+
options = defaults.merge(options)
|
61
|
+
options.each { |key, value| send("#{key}=", value) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def queue
|
65
|
+
QPush.redis.with do |conn|
|
66
|
+
conn.incr("#{QPush.config.stats_namespace}:queued")
|
67
|
+
conn.lpush("#{QPush.config.queue_namespace}", to_json)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/lib/qpush/redis.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module QPush
|
2
|
+
class << self
|
3
|
+
attr_reader :redis_pool
|
4
|
+
|
5
|
+
def redis
|
6
|
+
@redis_pool ||= RedisPool.create
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class RedisPool
|
11
|
+
def self.create
|
12
|
+
::ConnectionPool.new(size: QPush.config.redis_pool) do
|
13
|
+
::Redis.new(url: QPush.config.redis_url)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/qpush/server.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Base
|
2
|
+
require 'qpush/base'
|
3
|
+
|
4
|
+
# Server
|
5
|
+
require 'sequel'
|
6
|
+
require 'object_validator'
|
7
|
+
require 'parse-cron'
|
8
|
+
require 'qpush/server/database'
|
9
|
+
require 'qpush/server/delay'
|
10
|
+
require 'qpush/server/errors'
|
11
|
+
require 'qpush/server/execute'
|
12
|
+
require 'qpush/server/jobs'
|
13
|
+
require 'qpush/server/launcher'
|
14
|
+
require 'qpush/server/logger'
|
15
|
+
require 'qpush/server/manager'
|
16
|
+
require 'qpush/server/perform'
|
17
|
+
require 'qpush/server/queue'
|
18
|
+
require 'qpush/server/worker'
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module QPush
|
2
|
+
module Server
|
3
|
+
# The Delay worker requeues any jobs that have been delayed on our Redis
|
4
|
+
# server. Delayed jobs are pulled by a 'zrangebyscore', with the score
|
5
|
+
# representing the time the job should be performed.
|
6
|
+
#
|
7
|
+
class Delay
|
8
|
+
def initialize
|
9
|
+
@done = false
|
10
|
+
@conn = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
# Starts our delay process. This will run until instructed to stop.
|
14
|
+
#
|
15
|
+
def start
|
16
|
+
until @done
|
17
|
+
QPush.redis.with do |conn|
|
18
|
+
@conn = conn
|
19
|
+
watch_delay { retrieve_delays }
|
20
|
+
end
|
21
|
+
sleep 2
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Shutsdown our dleay process.
|
26
|
+
#
|
27
|
+
def shutdown
|
28
|
+
@done = true
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Retrieves delayed jobs based on the time they should be performed.
|
34
|
+
# If any are found, begin to update them.
|
35
|
+
#
|
36
|
+
def retrieve_delays
|
37
|
+
delays = @conn.zrangebyscore(QPush.config.delay_namespace, 0, Time.now.to_i)
|
38
|
+
delays.any? ? update_delays(delays) : @conn.unwatch
|
39
|
+
end
|
40
|
+
|
41
|
+
# Removes jobs that have been retrieved and sets them up to be performed.
|
42
|
+
#
|
43
|
+
def update_delays(delays)
|
44
|
+
@conn.multi do |multi|
|
45
|
+
multi.zrem(QPush.config.delay_namespace, delays)
|
46
|
+
delays.each { |job| perform_job(job) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Add a delayed job to the appropriate perform list.
|
51
|
+
#
|
52
|
+
def perform_job(json)
|
53
|
+
job = Job.new(JSON.parse(json))
|
54
|
+
job.api.perform
|
55
|
+
rescue => e
|
56
|
+
raise ServerError, e.message
|
57
|
+
end
|
58
|
+
|
59
|
+
# Performs a watch on our delay list
|
60
|
+
#
|
61
|
+
def watch_delay
|
62
|
+
@conn.watch(QPush.config.delay_namespace) do
|
63
|
+
yield if block_given?
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module QPush
|
2
|
+
class ServerError < StandardError
|
3
|
+
def initialize(msg = nil)
|
4
|
+
@message = msg
|
5
|
+
log_error
|
6
|
+
end
|
7
|
+
|
8
|
+
def message
|
9
|
+
"The following error occured: #{@message}"
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def log_error
|
15
|
+
Server.log.err("Server Error - #{@message}")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module QPush
|
2
|
+
module Server
|
3
|
+
class Execute
|
4
|
+
def initialize(job)
|
5
|
+
@job = job
|
6
|
+
end
|
7
|
+
|
8
|
+
def call
|
9
|
+
measure_run_time { job_object.call }
|
10
|
+
ExecutionSuccess.call(@job)
|
11
|
+
rescue => e
|
12
|
+
ExecutionFail.call(@job, e)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def measure_run_time
|
18
|
+
start = Time.now
|
19
|
+
yield
|
20
|
+
finish = Time.now
|
21
|
+
@job.run_time = "#{((finish - start) * 1000.0).round(3)} ms"
|
22
|
+
end
|
23
|
+
|
24
|
+
def job_object
|
25
|
+
klass = Object.const_get(@job.klass)
|
26
|
+
@job.args.empty? ? klass.new : klass.new(@job.args)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class ExecutionFail
|
31
|
+
def self.call(*args)
|
32
|
+
failed = ExecutionFail.new(*args)
|
33
|
+
failed.call
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(job, error)
|
37
|
+
@job = job
|
38
|
+
@error = error
|
39
|
+
end
|
40
|
+
|
41
|
+
def call
|
42
|
+
@job.bump_fail
|
43
|
+
@job.api.retry if @job.retry_job?
|
44
|
+
stat_increment
|
45
|
+
log_error
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def stat_increment
|
51
|
+
QPush.redis.with do |c|
|
52
|
+
c.incr("#{QPush.config.stats_namespace}:dead") if @job.dead_job?
|
53
|
+
c.incr("#{QPush.config.stats_namespace}:failed")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def log_error
|
58
|
+
Server.log.err("Job FAILED | #{@job.klass} | #{@job.id} | #{@error.message}")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class ExecutionSuccess
|
63
|
+
def self.call(*args)
|
64
|
+
success = ExecutionSuccess.new(*args)
|
65
|
+
success.call
|
66
|
+
end
|
67
|
+
|
68
|
+
def initialize(job)
|
69
|
+
@job = job
|
70
|
+
end
|
71
|
+
|
72
|
+
def call
|
73
|
+
@job.bump_success
|
74
|
+
@job.api.delay if @job.delay_job?
|
75
|
+
stat_increment
|
76
|
+
log_success
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def stat_increment
|
82
|
+
QPush.redis.with do |c|
|
83
|
+
c.incr("#{QPush.config.stats_namespace}:success")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def log_success
|
88
|
+
Server.log.info("Job SUCCESS | #{@job.klass} with ID: #{@job.id} | #{@job.run_time}")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module QPush
|
2
|
+
module Server
|
3
|
+
module JobHelpers
|
4
|
+
def bump_success
|
5
|
+
@total_success += 1
|
6
|
+
end
|
7
|
+
|
8
|
+
def bump_fail
|
9
|
+
@total_fail += 1
|
10
|
+
end
|
11
|
+
|
12
|
+
def retry_job?
|
13
|
+
@retry_max > @total_fail
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform_job?
|
17
|
+
@start_at < Time.now.to_i && @cron.empty?
|
18
|
+
end
|
19
|
+
|
20
|
+
def delay_job?
|
21
|
+
(@start_at > Time.now.to_i && @cron.empty?) || cron_job?
|
22
|
+
end
|
23
|
+
|
24
|
+
def cron_job?
|
25
|
+
@start_at < Time.now.to_i && !@cron.empty?
|
26
|
+
end
|
27
|
+
|
28
|
+
def dead_job?
|
29
|
+
@total_fail >= @retry_max
|
30
|
+
end
|
31
|
+
|
32
|
+
def cron_at
|
33
|
+
CronParser.new(@cron).next(Time.now).to_i
|
34
|
+
rescue => e
|
35
|
+
raise ServerError, e.message
|
36
|
+
end
|
37
|
+
|
38
|
+
def delay_until
|
39
|
+
@cron.empty? ? @start_at : cron_at
|
40
|
+
end
|
41
|
+
|
42
|
+
def retry_at
|
43
|
+
Time.now.to_i + ((@total_fail**4) + 15 + (rand(30) * (@total_fail + 1)))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class Job
|
48
|
+
include QPush::Job::Base
|
49
|
+
include QPush::Server::JobHelpers
|
50
|
+
include ObjectValidator::Validate
|
51
|
+
|
52
|
+
attr_accessor :klass, :id, :priority, :created_at, :start_at,
|
53
|
+
:cron, :retry_max, :total_success, :total_fail,
|
54
|
+
:run_time
|
55
|
+
attr_reader :args, :api
|
56
|
+
|
57
|
+
def initialize(options = {})
|
58
|
+
options = defaults.merge(options)
|
59
|
+
options.each { |key, value| send("#{key}=", value) }
|
60
|
+
@api = JobApi.new(self)
|
61
|
+
end
|
62
|
+
|
63
|
+
def args=(args)
|
64
|
+
@args =
|
65
|
+
if args.is_a?(String) then JSON.parse(args)
|
66
|
+
else args
|
67
|
+
end
|
68
|
+
rescue JSON::ParserError
|
69
|
+
@args = nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class JobValidator
|
74
|
+
include ObjectValidator::Validator
|
75
|
+
|
76
|
+
validates :klass,
|
77
|
+
with: { proc: proc { |j| Object.const_defined?(j.klass) },
|
78
|
+
msg: 'has not been defined' }
|
79
|
+
validates :cron,
|
80
|
+
with: { proc: proc { |j| j.cron.empty? ? true : CronParser.new(j.cron) },
|
81
|
+
msg: 'is not a valid expression' }
|
82
|
+
validates :id, type: String
|
83
|
+
validates :args, type: Hash
|
84
|
+
validates :created_at, type: Integer
|
85
|
+
validates :start_at, type: Integer
|
86
|
+
validates :retry_max, type: Integer
|
87
|
+
validates :total_fail, type: Integer
|
88
|
+
validates :total_success, type: Integer
|
89
|
+
end
|
90
|
+
|
91
|
+
class JobApi
|
92
|
+
def initialize(job)
|
93
|
+
@job = job
|
94
|
+
@config = QPush.config
|
95
|
+
end
|
96
|
+
|
97
|
+
def delay
|
98
|
+
QPush.redis.with do |conn|
|
99
|
+
conn.incr("#{@config.stats_namespace}:delayed")
|
100
|
+
conn.zadd(@config.delay_namespace, @job.delay_until, @job.to_json)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def queue
|
105
|
+
QPush.redis.with do |conn|
|
106
|
+
conn.incr("#{@config.stats_namespace}:queued")
|
107
|
+
conn.lpush("#{@config.queue_namespace}", @job.to_json)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def execute
|
112
|
+
execute = Execute.new(@job)
|
113
|
+
execute.call
|
114
|
+
end
|
115
|
+
|
116
|
+
def perform
|
117
|
+
QPush.redis.with do |conn|
|
118
|
+
conn.incr("#{@config.stats_namespace}:performed")
|
119
|
+
conn.lpush("#{@config.perform_namespace}:#{@job.priority}", @job.to_json)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def retry
|
124
|
+
QPush.redis.with do |conn|
|
125
|
+
conn.incr("#{@config.stats_namespace}:retries")
|
126
|
+
conn.zadd(@config.delay_namespace, @job.retry_at, @job.to_json)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def setup
|
131
|
+
fail unless @job.valid?
|
132
|
+
perform if @job.perform_job?
|
133
|
+
delay if @job.delay_job?
|
134
|
+
rescue
|
135
|
+
raise ServerError, 'Invalid job: ' + @job.errors.full_messages.join(' ')
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|