recurring_job 0.0.4
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.
- 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
|