recurring_job 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +15 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +157 -0
- data/Rakefile +7 -0
- data/lib/recurring_job.rb +5 -0
- data/lib/recurring_job/class_decl.rb +3 -0
- data/lib/recurring_job/recurring_job.rb +221 -0
- data/lib/recurring_job/version.rb +5 -0
- data/recurring_job.gemspec +34 -0
- data/test/recurring_job_test.rb +208 -0
- data/test/support/db.rb +21 -0
- data/test/test_helper.rb +30 -0
- metadata +209 -0
data/.gitignore
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-1.9.3-p327
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 OL2, Inc.
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
# RecurringJob
|
2
|
+
|
3
|
+
RecurringJob creates a framework for creating custom DelayedJob jobs that are automatically rescheduled to run again.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
RecurringJob requires delayed_job_active_record ( > 4.0).
|
7
|
+
Follow the instructions to [install DelayedJob](https://github.com/collectiveidea/delayed_job_active_record) first.
|
8
|
+
|
9
|
+
Then add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'recurring_job'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install recurring_job
|
22
|
+
|
23
|
+
By default, RecurringJob logs to STDOUT. If you want it to log to your rails logger, put the following somewhere in your rails configuration files
|
24
|
+
(I use environment.rb)
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
RecurringJob.logger = Rails.logger
|
28
|
+
```
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
RecurringJob extends the functionality of [Custom Jobs](https://github.com/collectiveidea/delayed_job#custom-jobs)
|
33
|
+
within DelayedJob. It uses the job's queue field to identify each type of recurring job, and to ensure a single instance of each
|
34
|
+
one is scheduled in the job queue at a time.
|
35
|
+
|
36
|
+
To use RecurringJob, you need to create a custom job class for each type of job you wish to schedule.
|
37
|
+
I like to put job classes in a `lib/jobs` folder in my rails app, but you're free to put them anywhere you like.
|
38
|
+
|
39
|
+
This example, which is similar to how we use RecurringJob at OnLive, shows how to subclass the RecurringJob class and provide a `perform` method that does the actual work.
|
40
|
+
We can send in our own options (in this example an `app_id` for a database Model named App) and those will be passed on each
|
41
|
+
time when the job is scheduled. In addition we are automatically passed in the job id of the DelayedJob job, which in this
|
42
|
+
case we use for locking (Of course, you can also just ignore the job id if you don't need it!).
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
class AppStatusJob < RecurringJob
|
46
|
+
|
47
|
+
def perform
|
48
|
+
return unless options
|
49
|
+
app_id = options[:app_id]
|
50
|
+
recurring_job_id = options[:delayed_job_id]
|
51
|
+
|
52
|
+
apps_to_process = app_id ? App.where(id:app_id) : App.all
|
53
|
+
|
54
|
+
apps_to_process.each do |app|
|
55
|
+
app.lock_for_status_check(recurring_job_id) do
|
56
|
+
# if no one else is modifying the app right now
|
57
|
+
# this block gets executed
|
58
|
+
app.do_whatever_it_means_to_check_status
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def after(job)
|
64
|
+
super # have to allow RecurringJob to do its work!!
|
65
|
+
send_email_about_job_success(job)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
```
|
70
|
+
We can run this job a single time to check a single app
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
AppStatusJob.queue_once(app_id:App.first.id)
|
74
|
+
```
|
75
|
+
|
76
|
+
Or we can set it up to run as a scheduled job to check all apps every hour
|
77
|
+
```ruby
|
78
|
+
AppStatusJob.schedule_job(interval:1.hour)
|
79
|
+
```
|
80
|
+
|
81
|
+
Or we can set it up to run for each particular app on a schedule, using the (unique)
|
82
|
+
name of the app as the name of the queue
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
App.all.each do |app|
|
86
|
+
AppStatusJob.schedule_job(interval:1.hour, app_id:app.id, queue:app.name)
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
Once AppStatusJob has been set up as a RecurringJob, the scheduled jobs will automatically add themselves back into the
|
91
|
+
queue to run an hour after they finish (or whatever interval you choose), continuing indefinitely! And like all DelayedJobs,
|
92
|
+
you can specify actions to happen when these jobs succeed, or fail, and the jobs live in the DelayedJob queue between runs.
|
93
|
+
(See more info about [DelayedJob hooks](https://github.com/collectiveidea/delayed_job#hooks)).
|
94
|
+
|
95
|
+
**Important:** If you implement any of the DelayedJob hooks (`before`, `after`, `success`, `error`, `failure`, or `enqueue`) in your RecurringJob, you must call super to allow the RecurringJob hooks
|
96
|
+
to do its work!
|
97
|
+
|
98
|
+
## More info
|
99
|
+
If you want to change the interval of a job, you can call schedule_job again and it will change the
|
100
|
+
currently scheduled job.
|
101
|
+
```ruby
|
102
|
+
AppStatusJob.schedule_job(interval:30.minutes)
|
103
|
+
```
|
104
|
+
You can stop a job from running anymore by taking it out of the queue
|
105
|
+
```ruby
|
106
|
+
AppStatusJob.unschedule_job
|
107
|
+
```
|
108
|
+
To get a list of all the RecurringJobs you have scheduled
|
109
|
+
```ruby
|
110
|
+
RecurringJob.all
|
111
|
+
```
|
112
|
+
|
113
|
+
## Another example
|
114
|
+
You might want to use RecurringJob to send emails in batches,
|
115
|
+
for example if your email service has limits to the number of API calls per day, like iPost does. If you had
|
116
|
+
a queue of pending emails in a table called pending_emails, it might look something like this (*all actual email
|
117
|
+
details left as an exercise for the reader*).
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
class BatchEmailJob < RecurringJob
|
121
|
+
|
122
|
+
def perform
|
123
|
+
# send any pending emails
|
124
|
+
@email_status_hash = PendingEmail.send_all # returns a hash of # emails were sent, any failures, etc
|
125
|
+
end
|
126
|
+
|
127
|
+
def success(job)
|
128
|
+
super # allow RecurringJob to do its work!!
|
129
|
+
|
130
|
+
notify_job_succeeded(@email_status_hash)
|
131
|
+
end
|
132
|
+
|
133
|
+
def max_attempts
|
134
|
+
3
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
```
|
139
|
+
And then set it up to run as often as you want to send the emails, say every 5 hours.
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
BatchEmailJob.schedule_job(interval:5.hours)
|
143
|
+
```
|
144
|
+
|
145
|
+
|
146
|
+
## Contributing
|
147
|
+
|
148
|
+
1. Fork it ( https://github.com/[my-github-username]/recurring_job/fork )
|
149
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
150
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
151
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
152
|
+
5. Create a new Pull Request
|
153
|
+
|
154
|
+
## License and Copying
|
155
|
+
|
156
|
+
Recurring Job is released under the MIT License by OL2, Inc.
|
157
|
+
Please see the file LICENSE.txt for a copy of this license.
|
data/Rakefile
ADDED
@@ -0,0 +1,221 @@
|
|
1
|
+
# Copyright (C) 2014-2015 OL2, Inc. See LICENSE.txt for details.
|
2
|
+
require 'active_record'
|
3
|
+
require 'delayed_job_active_record'
|
4
|
+
|
5
|
+
require "recurring_job/class_decl"
|
6
|
+
|
7
|
+
# Class declaration in recurring_job/class_decl gives the parent class,
|
8
|
+
# which is a new Struct().
|
9
|
+
class RecurringJob
|
10
|
+
# (parts inspired by https://gist.github.com/JoshMcKin/1648242)
|
11
|
+
|
12
|
+
def self.logger=(new_logger)
|
13
|
+
@@logger = new_logger
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.logger
|
17
|
+
@@logger ||= Logger.new(STDOUT)
|
18
|
+
@@logger
|
19
|
+
end
|
20
|
+
|
21
|
+
def logger
|
22
|
+
RecurringJob.logger
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.schedule_job(options = {}, this_job=nil)
|
26
|
+
# schedule this job (if you just want job to run once, just use queue_once )
|
27
|
+
# this_job is currently running instance (if any)(so we can check against it)
|
28
|
+
# options -
|
29
|
+
# :interval => number of seconds between job runs (from end of one to beginning of next)
|
30
|
+
# default, once a day
|
31
|
+
# :queue => name of queue to use
|
32
|
+
# default: the name of this class
|
33
|
+
# only one job will be scheduled at a time for any given queue
|
34
|
+
# :first_start_time => specify a specific time for this run, then use interval after that
|
35
|
+
# Plus any other options (if any) you want sent through to the underlying job.
|
36
|
+
|
37
|
+
options ||= {} # in case sent in explicitly as nil
|
38
|
+
options[:interval] ||= default_interval
|
39
|
+
options[:queue] ||= default_queue
|
40
|
+
|
41
|
+
queue_name = options[:queue]
|
42
|
+
other_job = next_scheduled_job(this_job, queue_name)
|
43
|
+
if other_job
|
44
|
+
logger.info "#{queue_name} job is already scheduled for #{other_job.run_at}."
|
45
|
+
# Still set any new start time or interval options for next time.
|
46
|
+
if job_interval(other_job) != options[:interval].to_i
|
47
|
+
logger.info " Updating interval to #{options[:interval]}"
|
48
|
+
set_job_interval(other_job, options[:interval])
|
49
|
+
end
|
50
|
+
if options[:first_start_time] && options[:first_start_time] != other_job.run_at
|
51
|
+
logger.info " Updating start time to #{options[:first_start_time]}"
|
52
|
+
|
53
|
+
other_job.run_at = options[:first_start_time]
|
54
|
+
other_job.save
|
55
|
+
end
|
56
|
+
else
|
57
|
+
# if start time is specified, use it ONLY this time (to start), don't pass on in options
|
58
|
+
run_time = options.delete(:first_start_time)
|
59
|
+
run_time ||= Time.now + options[:interval].to_i # make sure it's an integer (e.g. if sent in as 1.day)
|
60
|
+
other_job = Delayed::Job.enqueue self.new(options), :run_at => run_time, :queue=> queue_name
|
61
|
+
logger.info "The next #{queue_name} job has been scheduled for #{other_job.run_at}."
|
62
|
+
end
|
63
|
+
other_job
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.unschedule_job
|
67
|
+
# shortcut for deleting the job from Delayed Job
|
68
|
+
# returns true if there was a job to delete, false otherwise
|
69
|
+
recurring_job = self.next_scheduled_job
|
70
|
+
recurring_job.destroy if recurring_job
|
71
|
+
return recurring_job # true if there was a job to unschedule
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
def self.queue_once(options = {})
|
76
|
+
# just run this to add the queue to run one time only (not scheduled)
|
77
|
+
# IMPORTANT: don't put in same queue name as recurring job and DON'T specify an interval in the options!
|
78
|
+
# Can use the queue field, but do not use the job name!
|
79
|
+
queue = options[:queue]
|
80
|
+
raise "Can't run Recurring Job once in queue: #{default_queue}" if queue == default_queue
|
81
|
+
Delayed::Job.enqueue(self.new(options), :queue => queue)
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.in_queue_or_running?(queue)
|
85
|
+
# is this job currently in progress?
|
86
|
+
# will return false if the job is already done
|
87
|
+
# (the job is out of the queue when it's done.)
|
88
|
+
queue ||= default_queue
|
89
|
+
job = Delayed::Job.find_by(queue:queue)
|
90
|
+
job
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.default_interval
|
94
|
+
1.day
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.default_queue
|
98
|
+
self.name
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.next_scheduled_job(this_job=nil, queue_name = nil)
|
102
|
+
# return job if it exists
|
103
|
+
queue_name ||= default_queue
|
104
|
+
conditions = ['queue = ? AND failed_at IS NULL', queue_name]
|
105
|
+
|
106
|
+
unless this_job.blank?
|
107
|
+
conditions[0] << " AND id != ?"
|
108
|
+
conditions << this_job.id
|
109
|
+
end
|
110
|
+
|
111
|
+
Delayed::Job.where(conditions).first
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
def self.running?(queue=nil)
|
116
|
+
# is this job currently running?
|
117
|
+
queue ||= default_queue
|
118
|
+
job = Delayed::Job.find_by(queue:queue)
|
119
|
+
job && job.locked_by
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.job_id_running?(job_id)
|
123
|
+
# is a job with the given id running?
|
124
|
+
job = Delayed::Job.find_by(id:job_id) # don't use find, don't want to raise an error if not found
|
125
|
+
#logger.debug("Is job #{job_id} running? #{job.inspect}")
|
126
|
+
job && job.locked_by
|
127
|
+
end
|
128
|
+
|
129
|
+
def self.set_option(job, option, value)
|
130
|
+
# given a job from the queue,
|
131
|
+
# parse the handler yaml and set options[option] to value
|
132
|
+
y = YAML.load(job.handler)
|
133
|
+
y.options ||= {}
|
134
|
+
y.options[option] = value
|
135
|
+
job.handler = y.to_yaml
|
136
|
+
job.save!
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.get_option(job, option)
|
140
|
+
# given a job from the queue,
|
141
|
+
# parse the handler yaml and return options[option]
|
142
|
+
y = YAML.load(job.handler)
|
143
|
+
y.options && y.options[option]
|
144
|
+
end
|
145
|
+
|
146
|
+
def self.job_interval(job)
|
147
|
+
# given a job from the queue
|
148
|
+
# parse the handler yaml and give back the current interval
|
149
|
+
# nil means no interval set
|
150
|
+
get_option(job, :interval)
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.set_job_interval(job, interval)
|
154
|
+
# given a job from the queue
|
155
|
+
# parse the handler yaml and set the job interval
|
156
|
+
interval ||= default_interval
|
157
|
+
set_option(job, :interval, interval.to_i)
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.all
|
161
|
+
# Return all jobs with named queues (may get extra jobs if other jobs use named queues)
|
162
|
+
Delayed::Job.all.where.not(queue:nil)
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.list_job_intervals
|
166
|
+
# lists all jobs that have a queue associated with them and intervals (if any)
|
167
|
+
# {queue_name => {interval:interval, next_run:<run_at>}}
|
168
|
+
job_list = {}
|
169
|
+
self.all.each do |job|
|
170
|
+
queue = job.queue
|
171
|
+
next unless queue
|
172
|
+
job_list[queue] = {interval:self.job_interval(job), next_run:job.run_at}
|
173
|
+
end
|
174
|
+
job_list
|
175
|
+
end
|
176
|
+
|
177
|
+
def perform
|
178
|
+
# should be overridden by real job to do the actual work!
|
179
|
+
# (don't call super for this...)
|
180
|
+
raise "Must override perform in #{self.class.name}"
|
181
|
+
end
|
182
|
+
|
183
|
+
def before(job)
|
184
|
+
# Remember to call super if you implement this hook in your own job!
|
185
|
+
|
186
|
+
# Send in the job_id so that the RecurringJob can use it if it needs it.
|
187
|
+
# (during perform we otherwise don't have access to "job")
|
188
|
+
options[:delayed_job_id] = job.id.to_s
|
189
|
+
end
|
190
|
+
|
191
|
+
def after(job)
|
192
|
+
# Remember to call super if you implement this hook in your own job!
|
193
|
+
|
194
|
+
# Reschedule this job when we're done. Whether there's an error in this run
|
195
|
+
# or not, we always want to reschedule if an interval was specified
|
196
|
+
if options[:interval]
|
197
|
+
# if an interval was specified, make sure there's a future job using the same options as before.
|
198
|
+
# Otherwise, this is a one time run so don't reschedule.
|
199
|
+
options.delete(:delayed_job_id) # don't pass on the previous delayed job id
|
200
|
+
# logger.debug("Scheduling again: #{options.inspect}")
|
201
|
+
self.class.schedule_job(options, job)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def success(job)
|
206
|
+
# Remember to call super if you implement this hook in your own job!
|
207
|
+
end
|
208
|
+
|
209
|
+
def error(job, exception)
|
210
|
+
# Remember to call super if you implement this hook in your own job!
|
211
|
+
end
|
212
|
+
|
213
|
+
def failure(job)
|
214
|
+
# Remember to call super if you implement this hook in your own job!
|
215
|
+
end
|
216
|
+
|
217
|
+
def enqueue(job)
|
218
|
+
# Remember to call super if you implement this hook in your own job!
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'recurring_job/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "recurring_job"
|
8
|
+
spec.version = RecurringJob::VERSION
|
9
|
+
spec.authors = ["Ruth Helfinstein", "Noah Gibbs"]
|
10
|
+
spec.email = ["ruth.helfinstein@onlive.com", "noah@onlive.com"]
|
11
|
+
spec.summary = %q{Schedule DelayedJob tasks to repeat after a given interval.}
|
12
|
+
spec.description = <<DESC
|
13
|
+
Recurring_job creates a framework for creating custom DelayedJob jobs
|
14
|
+
that are automatically rescheduled to run again at a given interval.
|
15
|
+
DESC
|
16
|
+
spec.homepage = ""
|
17
|
+
spec.license = "MIT"
|
18
|
+
|
19
|
+
spec.files = `git ls-files -z`.split("\x0")
|
20
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
21
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_runtime_dependency 'activesupport', ['>= 3.0', '< 5.0']
|
25
|
+
spec.add_runtime_dependency 'activerecord', '>= 3.0', '< 5.0'
|
26
|
+
spec.add_runtime_dependency 'delayed_job_active_record', '~> 4.0'
|
27
|
+
|
28
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
29
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
30
|
+
spec.add_development_dependency "minitest", "~> 5.4"
|
31
|
+
spec.add_development_dependency "rr", '~> 1.1'
|
32
|
+
spec.add_dependency 'sqlite3', '~> 1'
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# Copyright (C) 2014-2015 OL2, Inc. See LICENSE.txt for details.
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class RecurringJobTest < ActiveSupport::TestCase
|
6
|
+
@@last_job_id = nil
|
7
|
+
|
8
|
+
class MyRecurringJob < RecurringJob
|
9
|
+
def perform
|
10
|
+
raise "Must have options!" unless options
|
11
|
+
case options[:action]
|
12
|
+
when :error
|
13
|
+
raise 'FAILING'
|
14
|
+
when :delay
|
15
|
+
logger.debug("Sleeping")
|
16
|
+
sleep(5)
|
17
|
+
when :test_id
|
18
|
+
logger.debug("Job id is #{options[:delayed_job_id]}")
|
19
|
+
RecurringJobTest.last_job_id = options[:delayed_job_id]
|
20
|
+
else
|
21
|
+
logger.debug("Performing")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.last_job_id=(value)
|
27
|
+
@@last_job_id = value
|
28
|
+
end
|
29
|
+
|
30
|
+
def setup
|
31
|
+
RecurringJob.logger.debug("------------------------------------")
|
32
|
+
Delayed::Job.delete_all
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_schedule_job
|
36
|
+
# nothing is scheduled when we start
|
37
|
+
assert_nil(MyRecurringJob.next_scheduled_job)
|
38
|
+
# schedule a job which will run immediately and reschedule itself
|
39
|
+
# (since not running automatically this will work)
|
40
|
+
job = MyRecurringJob.schedule_job({interval:1, action: :perform, first_start_time: Time.now})
|
41
|
+
# now this job is scheduled
|
42
|
+
assert_equal(job, MyRecurringJob.next_scheduled_job)
|
43
|
+
assert_equal(RecurringJob.all, [job])
|
44
|
+
# now run the job from the queue
|
45
|
+
Delayed::Worker.new.work_off
|
46
|
+
|
47
|
+
# should have added a new job that's different from this one
|
48
|
+
job2 = MyRecurringJob.next_scheduled_job
|
49
|
+
assert(job2)
|
50
|
+
refute_equal(job2, job)
|
51
|
+
|
52
|
+
# if we try to schedule a new one now it should be the same one
|
53
|
+
job3 = MyRecurringJob.schedule_job({interval:1, first_start_time: Time.now})
|
54
|
+
assert_equal(job2, job3)
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_failed_job
|
58
|
+
Delayed::Worker.max_attempts = 3 # delete after third failed attempt
|
59
|
+
|
60
|
+
worker = Delayed::Worker.new
|
61
|
+
|
62
|
+
job = MyRecurringJob.schedule_job({interval:1, action: :error})
|
63
|
+
|
64
|
+
# make sure the jobs will run now
|
65
|
+
RecurringJob.all.each do |j|
|
66
|
+
j.run_at = Time.now
|
67
|
+
j.save!
|
68
|
+
end
|
69
|
+
# run job, will fail
|
70
|
+
worker.work_off
|
71
|
+
RecurringJob.all.each do |j|
|
72
|
+
j.run_at = Time.now
|
73
|
+
j.save!
|
74
|
+
end
|
75
|
+
|
76
|
+
# there should now be two jobs in the queue
|
77
|
+
# since it adds the next job even if there's an error
|
78
|
+
assert_equal(2,RecurringJob.all.size, "Should be the new job in the queue")
|
79
|
+
jobs = RecurringJob.all
|
80
|
+
# they should both run this time and both get errors
|
81
|
+
worker.work_off
|
82
|
+
|
83
|
+
RecurringJob.all.each do |j|
|
84
|
+
j.run_at = Time.now
|
85
|
+
j.save!
|
86
|
+
end
|
87
|
+
|
88
|
+
assert_equal(jobs, RecurringJob.all)
|
89
|
+
# There should still only be the same two jobs in the queue.
|
90
|
+
assert_equal(2,RecurringJob.all.size)
|
91
|
+
|
92
|
+
worker.work_off
|
93
|
+
# it's been 3 attempts so original job should be deleted
|
94
|
+
|
95
|
+
RecurringJob.all.each do |j|
|
96
|
+
j.run_at = Time.now
|
97
|
+
j.save!
|
98
|
+
end
|
99
|
+
|
100
|
+
refute_includes(RecurringJob.all, job)
|
101
|
+
|
102
|
+
# The second job will fail a third time and get deleted, but make sure it puts a new job in the
|
103
|
+
# queue before it does (to show we will always have at least one job in the queue)
|
104
|
+
worker.work_off
|
105
|
+
|
106
|
+
refute_empty(RecurringJob.all)
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_uses_different_queues
|
111
|
+
job = MyRecurringJob.schedule_job({interval:1, queue:'queue1'})
|
112
|
+
job2 = MyRecurringJob.schedule_job({interval:1})
|
113
|
+
refute_equal(job, job2)
|
114
|
+
assert_equal(job, MyRecurringJob.next_scheduled_job(nil, 'queue1'))
|
115
|
+
assert_equal(job2, MyRecurringJob.next_scheduled_job(nil))
|
116
|
+
end
|
117
|
+
|
118
|
+
def test_first_start_time
|
119
|
+
first_start_time = Date.today.midnight
|
120
|
+
job = MyRecurringJob.schedule_job({interval:1, first_start_time:first_start_time})
|
121
|
+
assert_equal(first_start_time, job.run_at)
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_job_with_no_schedule
|
125
|
+
# run MyRecurringJob one time only with no interval set and check that it's not rescheduled
|
126
|
+
job = MyRecurringJob.queue_once(action: :something)
|
127
|
+
|
128
|
+
assert_equal(Delayed::Job.all, [job])
|
129
|
+
# now run the job from the queue
|
130
|
+
Delayed::Worker.new.work_off
|
131
|
+
|
132
|
+
# There should be no jobs in the queue (wasn't rescheduled)
|
133
|
+
assert_empty(Delayed::Job.all, "Queue should be empty")
|
134
|
+
end
|
135
|
+
|
136
|
+
def test_get_and_set_interval
|
137
|
+
job = MyRecurringJob.schedule_job
|
138
|
+
interval = MyRecurringJob.job_interval(job)
|
139
|
+
assert_equal(interval, MyRecurringJob.default_interval)
|
140
|
+
|
141
|
+
refute_equal(interval, 0)
|
142
|
+
|
143
|
+
MyRecurringJob.set_job_interval(job, 0)
|
144
|
+
job.reload # make sure it saved the change
|
145
|
+
assert_equal(0, MyRecurringJob.job_interval(job))
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
def test_schedule_job_new_interval
|
150
|
+
# nothing is scheduled when we start
|
151
|
+
assert_nil(MyRecurringJob.next_scheduled_job)
|
152
|
+
# schedule a job which will run immediately and reschedule itself immediately
|
153
|
+
# (since not running automatically this will work)
|
154
|
+
job = MyRecurringJob.schedule_job({interval:1})
|
155
|
+
# now this job is scheduled
|
156
|
+
assert_equal(job, MyRecurringJob.next_scheduled_job)
|
157
|
+
assert_equal(1, MyRecurringJob.job_interval(job))
|
158
|
+
|
159
|
+
job2 = MyRecurringJob.schedule_job({interval:0})
|
160
|
+
assert_equal(job, job2) # same object
|
161
|
+
assert_equal(0, MyRecurringJob.job_interval(job2))
|
162
|
+
|
163
|
+
job3 = MyRecurringJob.schedule_job({interval:1})
|
164
|
+
assert_equal(job, job3) # same object
|
165
|
+
assert_equal(1, MyRecurringJob.job_interval(job3))
|
166
|
+
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
def test_schedule_job_new_first_start_time
|
171
|
+
# nothing is scheduled when we start
|
172
|
+
assert_nil(MyRecurringJob.next_scheduled_job)
|
173
|
+
|
174
|
+
time1 = Time.now + 1.day
|
175
|
+
job = MyRecurringJob.schedule_job({first_start_time:time1})
|
176
|
+
# now this job is scheduled
|
177
|
+
assert_equal(job, MyRecurringJob.next_scheduled_job)
|
178
|
+
assert_equal(time1.to_i, job.run_at.to_i)
|
179
|
+
|
180
|
+
job2 = MyRecurringJob.schedule_job({interval:0}) # no start time
|
181
|
+
assert_equal(job, job2) # same object
|
182
|
+
assert_equal(time1.to_i, job2.run_at.to_i)
|
183
|
+
|
184
|
+
time2 = Time.now
|
185
|
+
job3 = MyRecurringJob.schedule_job({first_start_time:time2})
|
186
|
+
assert_equal(job, job3) # same object
|
187
|
+
assert_equal(time2.to_i, job3.run_at.to_i)
|
188
|
+
end
|
189
|
+
|
190
|
+
def test_job_id
|
191
|
+
# make sure the delayed job id is available to the job.
|
192
|
+
assert_empty(RecurringJob.all)
|
193
|
+
|
194
|
+
RecurringJobTest.last_job_id = nil
|
195
|
+
job = MyRecurringJob.schedule_job({interval:1, action: :test_id, first_start_time: Time.now})
|
196
|
+
assert_equal(RecurringJob.all, [job])
|
197
|
+
|
198
|
+
refute(MyRecurringJob.get_option(job, :delayed_job_id)) # it's set only when the job is running
|
199
|
+
assert_equal(:test_id, MyRecurringJob.get_option(job, :action))
|
200
|
+
|
201
|
+
# now run the job from the queue
|
202
|
+
Delayed::Worker.new.work_off
|
203
|
+
|
204
|
+
assert_equal(job.id, @@last_job_id.to_i)
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
data/test/support/db.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# Copyright (C) 2015 OL2, Inc. See LICENSE.txt for details.
|
2
|
+
|
3
|
+
# set up database just for testing. When using the gem, users will set up
|
4
|
+
# the database structure when setting up delayed job to work with their code and
|
5
|
+
# we use that same database.
|
6
|
+
ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
|
7
|
+
|
8
|
+
# set up the DelayedJob schema in our test database
|
9
|
+
ActiveRecord::Base.connection.create_table "delayed_jobs", force: true do |t|
|
10
|
+
t.integer "priority", default: 0, null: false
|
11
|
+
t.integer "attempts", default: 0, null: false
|
12
|
+
t.text "handler", null: false
|
13
|
+
t.text "last_error"
|
14
|
+
t.datetime "run_at"
|
15
|
+
t.datetime "locked_at"
|
16
|
+
t.datetime "failed_at"
|
17
|
+
t.string "locked_by"
|
18
|
+
t.string "queue"
|
19
|
+
t.datetime "created_at"
|
20
|
+
t.datetime "updated_at"
|
21
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# Copyright (C) 2015 OL2, Inc. See LICENSE.txt for details.
|
2
|
+
|
3
|
+
# Test local copy first
|
4
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), "..", "lib")
|
5
|
+
|
6
|
+
require "rr"
|
7
|
+
require 'active_support/dependencies'
|
8
|
+
require 'active_record'
|
9
|
+
require 'logger'
|
10
|
+
|
11
|
+
require "support/db"
|
12
|
+
require "recurring_job"
|
13
|
+
require 'minitest/autorun'
|
14
|
+
|
15
|
+
require 'tempfile'
|
16
|
+
|
17
|
+
if !Dir.exists?('tmp')
|
18
|
+
Dir.mkdir('tmp')
|
19
|
+
end
|
20
|
+
RecurringJob.logger = Logger.new('tmp/rj_test.log')
|
21
|
+
|
22
|
+
ENV['RAILS_ENV'] = 'test'
|
23
|
+
|
24
|
+
|
25
|
+
ActiveSupport::TestCase.test_order = :random
|
26
|
+
ActiveRecord::Base.logger = RecurringJob.logger
|
27
|
+
ActiveSupport::LogSubscriber.colorize_logging = false
|
28
|
+
|
29
|
+
|
30
|
+
|
metadata
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: recurring_job
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ruth Helfinstein
|
9
|
+
- Noah Gibbs
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2015-03-05 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activesupport
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '3.0'
|
23
|
+
- - <
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '5.0'
|
26
|
+
type: :runtime
|
27
|
+
prerelease: false
|
28
|
+
version_requirements: !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
- - <
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '5.0'
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: activerecord
|
39
|
+
requirement: !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '3.0'
|
45
|
+
- - <
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '3.0'
|
56
|
+
- - <
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '5.0'
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: delayed_job_active_record
|
61
|
+
requirement: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ~>
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '4.0'
|
67
|
+
type: :runtime
|
68
|
+
prerelease: false
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ~>
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '4.0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: bundler
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.6'
|
83
|
+
type: :development
|
84
|
+
prerelease: false
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ~>
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '1.6'
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: rake
|
93
|
+
requirement: !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ~>
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '10.0'
|
99
|
+
type: :development
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ~>
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '10.0'
|
107
|
+
- !ruby/object:Gem::Dependency
|
108
|
+
name: minitest
|
109
|
+
requirement: !ruby/object:Gem::Requirement
|
110
|
+
none: false
|
111
|
+
requirements:
|
112
|
+
- - ~>
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '5.4'
|
115
|
+
type: :development
|
116
|
+
prerelease: false
|
117
|
+
version_requirements: !ruby/object:Gem::Requirement
|
118
|
+
none: false
|
119
|
+
requirements:
|
120
|
+
- - ~>
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '5.4'
|
123
|
+
- !ruby/object:Gem::Dependency
|
124
|
+
name: rr
|
125
|
+
requirement: !ruby/object:Gem::Requirement
|
126
|
+
none: false
|
127
|
+
requirements:
|
128
|
+
- - ~>
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '1.1'
|
131
|
+
type: :development
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
none: false
|
135
|
+
requirements:
|
136
|
+
- - ~>
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '1.1'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: sqlite3
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
none: false
|
143
|
+
requirements:
|
144
|
+
- - ~>
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '1'
|
147
|
+
type: :runtime
|
148
|
+
prerelease: false
|
149
|
+
version_requirements: !ruby/object:Gem::Requirement
|
150
|
+
none: false
|
151
|
+
requirements:
|
152
|
+
- - ~>
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '1'
|
155
|
+
description: ! 'Recurring_job creates a framework for creating custom DelayedJob jobs
|
156
|
+
|
157
|
+
that are automatically rescheduled to run again at a given interval.
|
158
|
+
|
159
|
+
'
|
160
|
+
email:
|
161
|
+
- ruth.helfinstein@onlive.com
|
162
|
+
- noah@onlive.com
|
163
|
+
executables: []
|
164
|
+
extensions: []
|
165
|
+
extra_rdoc_files: []
|
166
|
+
files:
|
167
|
+
- .gitignore
|
168
|
+
- .ruby-version
|
169
|
+
- Gemfile
|
170
|
+
- LICENSE.txt
|
171
|
+
- README.md
|
172
|
+
- Rakefile
|
173
|
+
- lib/recurring_job.rb
|
174
|
+
- lib/recurring_job/class_decl.rb
|
175
|
+
- lib/recurring_job/recurring_job.rb
|
176
|
+
- lib/recurring_job/version.rb
|
177
|
+
- recurring_job.gemspec
|
178
|
+
- test/recurring_job_test.rb
|
179
|
+
- test/support/db.rb
|
180
|
+
- test/test_helper.rb
|
181
|
+
homepage: ''
|
182
|
+
licenses:
|
183
|
+
- MIT
|
184
|
+
post_install_message:
|
185
|
+
rdoc_options: []
|
186
|
+
require_paths:
|
187
|
+
- lib
|
188
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
189
|
+
none: false
|
190
|
+
requirements:
|
191
|
+
- - ! '>='
|
192
|
+
- !ruby/object:Gem::Version
|
193
|
+
version: '0'
|
194
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
195
|
+
none: false
|
196
|
+
requirements:
|
197
|
+
- - ! '>='
|
198
|
+
- !ruby/object:Gem::Version
|
199
|
+
version: '0'
|
200
|
+
requirements: []
|
201
|
+
rubyforge_project:
|
202
|
+
rubygems_version: 1.8.23
|
203
|
+
signing_key:
|
204
|
+
specification_version: 3
|
205
|
+
summary: Schedule DelayedJob tasks to repeat after a given interval.
|
206
|
+
test_files:
|
207
|
+
- test/recurring_job_test.rb
|
208
|
+
- test/support/db.rb
|
209
|
+
- test/test_helper.rb
|