qpush 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,5 @@
1
+ class TestJob
2
+ def call
3
+ puts 'hello'
4
+ end
5
+ end
@@ -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
@@ -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,13 @@
1
+ module QPush
2
+ class << self
3
+ def db
4
+ @db ||= Database.create
5
+ end
6
+ end
7
+
8
+ class Database
9
+ def self.create
10
+ Sequel.connect(QPush.config.database_url, max_connections: QPush.config.database_pool)
11
+ end
12
+ end
13
+ end
@@ -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