delayed_cron_job 0.5.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 +15 -0
- data/.gitignore +22 -0
- data/.rspec +3 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +63 -0
- data/Rakefile +6 -0
- data/delayed_cron_job.gemspec +31 -0
- data/lib/delayed_cron_job.rb +15 -0
- data/lib/delayed_cron_job/cronline.rb +465 -0
- data/lib/delayed_cron_job/plugin.rb +58 -0
- data/lib/delayed_cron_job/version.rb +3 -0
- data/lib/generators/delayed_job/cron_generator.rb +22 -0
- data/lib/generators/delayed_job/templates/cron_migration.rb +9 -0
- data/spec/delayed_cron_job_spec.rb +163 -0
- data/spec/spec_helper.rb +109 -0
- metadata +149 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ODRhZWVmNmIyMGVjMzEyOTIzZWVmNzUzMWYzNjZjMTZkNTM3YmJkOA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZTQ5MDhjZTE1NjMyNjcxZTg3Yzk1YjA3OGJjYTgwN2M0OWU4YzJmYQ==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZmEwOGJjMGU3ZWE4NmU2ZDQ0MGQ4Y2I2NWFiOThkMTgwMWUwZDllNzE0NWQ0
|
10
|
+
MmU4YTczZTM1OGUwZmMwOWZkZGZhZDViY2M5YjA2YmEzODJjMmNkMzMwY2Iz
|
11
|
+
M2ZkOGQyN2M1ZDRmZWY0ZTJmZjExZjE5NmZlODRiNTIwNzMzNWQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
YTY4NGZiNjE1Njg1NGViZTU4NjkxZTIyNzE4OTYzYjFkMGJjMjc4NzMzNzJj
|
14
|
+
ODA0NDg3NjYzM2EzZmYxNjhiMDdiOTE4ODAyYjY3Y2Q4YTFjMGE2NzQ3ZmNm
|
15
|
+
ZDA0MGQxN2MxMDVkZmU0ZWEzNjBjNjU0NDk0YzQ3MGEwZDZkMTc=
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Pascal Zumkehr
|
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,63 @@
|
|
1
|
+
# Delayed::Cron::Job
|
2
|
+
|
3
|
+
Delayed::Cron::Job is an extension to Delayed::Job that allows you to set
|
4
|
+
cron expressions for your jobs to run repeatedly.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
gem 'delayed_cron_job'
|
11
|
+
|
12
|
+
And then execute:
|
13
|
+
|
14
|
+
$ bundle
|
15
|
+
|
16
|
+
If you are using `delayed_job_active_record`, generate a migration (after the
|
17
|
+
original delayed job migration) to add the `cron` column to the `delayed_jobs`
|
18
|
+
table:
|
19
|
+
|
20
|
+
$ rails generate delayed_jobs:cron
|
21
|
+
$ rake db:migrate
|
22
|
+
|
23
|
+
There are no additional steps for `delayed_job_mongoid`.
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
When enqueuing a job, simply pass the `cron` option, e.g.:
|
28
|
+
|
29
|
+
Delayed::Job.enqueue(MyRepeatedJob.new, cron: '15 */6 * * 1-5')
|
30
|
+
|
31
|
+
Any crontab compatible cron expressions are supported (see `man 5 crontab`).
|
32
|
+
The credits for the `Cronline` class used go to
|
33
|
+
[rufus-scheduler](https://github.com/jmettraux/rufus-scheduler).
|
34
|
+
|
35
|
+
## Details
|
36
|
+
|
37
|
+
The initial `run_at` value is computed during the `#enqueue` method call.
|
38
|
+
If you create `Delayed::Job` database entries directly, make sure to set
|
39
|
+
`run_at` accordingly.
|
40
|
+
|
41
|
+
You may use the `id` of the `Delayed::Job` as returned by the `#enqueue` method
|
42
|
+
to reference and/or remove the scheduled job in the future.
|
43
|
+
|
44
|
+
The subsequent run of a job is only scheduled after the current run has
|
45
|
+
terminated. If a single run takes longer than the given execution interval,
|
46
|
+
some runs may be skipped. E.g., if a run takes five minutes, but the job is
|
47
|
+
scheduled to be executed every second minute, it will actually only execute
|
48
|
+
every sixth minute: With a cron of `*/2 * * * *`, if the current run starts at
|
49
|
+
`:00` and finishes at `:05`, then the next scheduled execution time is at `:06`,
|
50
|
+
and so on.
|
51
|
+
|
52
|
+
If you do not want longer running jobs to skip executions, simply create a
|
53
|
+
lightweight master job that enqueues the actual workload as separate jobs.
|
54
|
+
Of course you have to make sure to start enough workers to handle all these
|
55
|
+
jobs.
|
56
|
+
|
57
|
+
## Contributing
|
58
|
+
|
59
|
+
1. Fork it ( https://github.com/codez/delayed_cron_job/fork )
|
60
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
61
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
62
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
63
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'delayed_cron_job/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "delayed_cron_job"
|
8
|
+
spec.version = DelayedCronJob::VERSION
|
9
|
+
spec.authors = ["Pascal Zumkehr"]
|
10
|
+
spec.email = ["spam@codez.ch"]
|
11
|
+
spec.summary = %q{An extension to Delayed::Job that allows you to set
|
12
|
+
cron expressions for your jobs to run regularly.}
|
13
|
+
spec.description = %q{Delayed Cron Job is an extension to Delayed::Job
|
14
|
+
that allows you to set cron expressions for your
|
15
|
+
jobs to run regularly.}
|
16
|
+
spec.homepage = "https://github.com/codez/delayed_cron_job"
|
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_dependency "delayed_job"
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
27
|
+
spec.add_development_dependency "rake"
|
28
|
+
spec.add_development_dependency "rspec"
|
29
|
+
spec.add_development_dependency "sqlite3"
|
30
|
+
spec.add_development_dependency "delayed_job_active_record"
|
31
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'delayed_job'
|
2
|
+
require 'english'
|
3
|
+
require 'delayed_cron_job/cronline'
|
4
|
+
require 'delayed_cron_job/plugin'
|
5
|
+
require 'delayed_cron_job/version'
|
6
|
+
|
7
|
+
module DelayedCronJob
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
if defined?(Delayed::Backend::Mongoid)
|
12
|
+
Delayed::Backend::Mongoid::Job.field :cron, :type => String
|
13
|
+
end
|
14
|
+
|
15
|
+
DelayedCronJob::Plugin.callback_block.call(Delayed::Worker.lifecycle)
|
@@ -0,0 +1,465 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2006-2014, John Mettraux, jmettraux@gmail.com
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
9
|
+
# furnished to do so, subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
12
|
+
# all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
# THE SOFTWARE.
|
21
|
+
#
|
22
|
+
# Made in Japan.
|
23
|
+
#++
|
24
|
+
|
25
|
+
|
26
|
+
module DelayedCronJob
|
27
|
+
|
28
|
+
#
|
29
|
+
# A 'cron line' is a line in the sense of a crontab
|
30
|
+
# (man 5 crontab) file line.
|
31
|
+
#
|
32
|
+
class Cronline
|
33
|
+
|
34
|
+
# The string used for creating this cronline instance.
|
35
|
+
#
|
36
|
+
attr_reader :original
|
37
|
+
|
38
|
+
attr_reader :seconds
|
39
|
+
attr_reader :minutes
|
40
|
+
attr_reader :hours
|
41
|
+
attr_reader :days
|
42
|
+
attr_reader :months
|
43
|
+
attr_reader :weekdays
|
44
|
+
attr_reader :monthdays
|
45
|
+
attr_reader :timezone
|
46
|
+
|
47
|
+
def initialize(line)
|
48
|
+
|
49
|
+
raise ArgumentError.new(
|
50
|
+
"not a string: #{line.inspect}"
|
51
|
+
) unless line.is_a?(String)
|
52
|
+
|
53
|
+
@original = line
|
54
|
+
|
55
|
+
items = line.split
|
56
|
+
|
57
|
+
@timezone = (TZInfo::Timezone.get(items.last) rescue nil)
|
58
|
+
items.pop if @timezone
|
59
|
+
|
60
|
+
raise ArgumentError.new(
|
61
|
+
"not a valid cronline : '#{line}'"
|
62
|
+
) unless items.length == 5 or items.length == 6
|
63
|
+
|
64
|
+
offset = items.length - 5
|
65
|
+
|
66
|
+
@seconds = offset == 1 ? parse_item(items[0], 0, 59) : [ 0 ]
|
67
|
+
@minutes = parse_item(items[0 + offset], 0, 59)
|
68
|
+
@hours = parse_item(items[1 + offset], 0, 24)
|
69
|
+
@days = parse_item(items[2 + offset], 1, 31)
|
70
|
+
@months = parse_item(items[3 + offset], 1, 12)
|
71
|
+
@weekdays, @monthdays = parse_weekdays(items[4 + offset])
|
72
|
+
|
73
|
+
[ @seconds, @minutes, @hours, @months ].each do |es|
|
74
|
+
|
75
|
+
raise ArgumentError.new(
|
76
|
+
"invalid cronline: '#{line}'"
|
77
|
+
) if es && es.find { |e| ! e.is_a?(Fixnum) }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns true if the given time matches this cron line.
|
82
|
+
#
|
83
|
+
def matches?(time)
|
84
|
+
|
85
|
+
time = Time.at(time) unless time.kind_of?(Time)
|
86
|
+
|
87
|
+
time = @timezone.utc_to_local(time.getutc) if @timezone
|
88
|
+
|
89
|
+
return false unless sub_match?(time, :sec, @seconds)
|
90
|
+
return false unless sub_match?(time, :min, @minutes)
|
91
|
+
return false unless sub_match?(time, :hour, @hours)
|
92
|
+
return false unless date_match?(time)
|
93
|
+
true
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns the next time that this cron line is supposed to 'fire'
|
97
|
+
#
|
98
|
+
# This is raw, 3 secs to iterate over 1 year on my macbook :( brutal.
|
99
|
+
# (Well, I was wrong, takes 0.001 sec on 1.8.7 and 1.9.1)
|
100
|
+
#
|
101
|
+
# This method accepts an optional Time parameter. It's the starting point
|
102
|
+
# for the 'search'. By default, it's Time.now
|
103
|
+
#
|
104
|
+
# Note that the time instance returned will be in the same time zone that
|
105
|
+
# the given start point Time (thus a result in the local time zone will
|
106
|
+
# be passed if no start time is specified (search start time set to
|
107
|
+
# Time.now))
|
108
|
+
#
|
109
|
+
# Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
|
110
|
+
# Time.mktime(2008, 10, 24, 7, 29))
|
111
|
+
# #=> Fri Oct 24 07:30:00 -0500 2008
|
112
|
+
#
|
113
|
+
# Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
|
114
|
+
# Time.utc(2008, 10, 24, 7, 29))
|
115
|
+
# #=> Fri Oct 24 07:30:00 UTC 2008
|
116
|
+
#
|
117
|
+
# Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
|
118
|
+
# Time.utc(2008, 10, 24, 7, 29)).localtime
|
119
|
+
# #=> Fri Oct 24 02:30:00 -0500 2008
|
120
|
+
#
|
121
|
+
# (Thanks to K Liu for the note and the examples)
|
122
|
+
#
|
123
|
+
def next_time(from=Time.now)
|
124
|
+
|
125
|
+
time = local_time(from)
|
126
|
+
time = round_to_seconds(time)
|
127
|
+
|
128
|
+
# start at the next second
|
129
|
+
time = time + 1
|
130
|
+
|
131
|
+
loop do
|
132
|
+
unless date_match?(time)
|
133
|
+
dst = time.isdst
|
134
|
+
time += (24 - time.hour) * 3600 - time.min * 60 - time.sec
|
135
|
+
time -= 3600 if time.isdst != dst # not necessary for winter, but...
|
136
|
+
next
|
137
|
+
end
|
138
|
+
unless sub_match?(time, :hour, @hours)
|
139
|
+
time += (60 - time.min) * 60 - time.sec; next
|
140
|
+
end
|
141
|
+
unless sub_match?(time, :min, @minutes)
|
142
|
+
time += 60 - time.sec; next
|
143
|
+
end
|
144
|
+
unless sub_match?(time, :sec, @seconds)
|
145
|
+
time += 1; next
|
146
|
+
end
|
147
|
+
|
148
|
+
break
|
149
|
+
end
|
150
|
+
|
151
|
+
global_time(time, from.utc?)
|
152
|
+
|
153
|
+
rescue TZInfo::PeriodNotFound
|
154
|
+
|
155
|
+
next_time(from + 3600)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Returns the previous time the cronline matched. It's like next_time, but
|
159
|
+
# for the past.
|
160
|
+
#
|
161
|
+
def previous_time(from=Time.now)
|
162
|
+
|
163
|
+
time = local_time(from)
|
164
|
+
time = round_to_seconds(time)
|
165
|
+
|
166
|
+
# start at the previous second
|
167
|
+
time = time - 1
|
168
|
+
|
169
|
+
loop do
|
170
|
+
unless date_match?(time)
|
171
|
+
time -= time.hour * 3600 + time.min * 60 + time.sec + 1; next
|
172
|
+
end
|
173
|
+
unless sub_match?(time, :hour, @hours)
|
174
|
+
time -= time.min * 60 + time.sec + 1; next
|
175
|
+
end
|
176
|
+
unless sub_match?(time, :min, @minutes)
|
177
|
+
time -= time.sec + 1; next
|
178
|
+
end
|
179
|
+
unless sub_match?(time, :sec, @seconds)
|
180
|
+
time -= 1; next
|
181
|
+
end
|
182
|
+
|
183
|
+
break
|
184
|
+
end
|
185
|
+
|
186
|
+
global_time(time, from.utc?)
|
187
|
+
|
188
|
+
rescue TZInfo::PeriodNotFound
|
189
|
+
|
190
|
+
previous_time(time)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Returns an array of 6 arrays (seconds, minutes, hours, days,
|
194
|
+
# months, weekdays).
|
195
|
+
# This method is used by the cronline unit tests.
|
196
|
+
#
|
197
|
+
def to_array
|
198
|
+
|
199
|
+
[
|
200
|
+
@seconds,
|
201
|
+
@minutes,
|
202
|
+
@hours,
|
203
|
+
@days,
|
204
|
+
@months,
|
205
|
+
@weekdays,
|
206
|
+
@monthdays,
|
207
|
+
@timezone ? @timezone.name : nil
|
208
|
+
]
|
209
|
+
end
|
210
|
+
|
211
|
+
# Returns a quickly computed approximation of the frequency for this
|
212
|
+
# cron line.
|
213
|
+
#
|
214
|
+
# #brute_frequency, on the other hand, will compute the frequency by
|
215
|
+
# examining a whole, that can take more than seconds for a seconds
|
216
|
+
# level cron...
|
217
|
+
#
|
218
|
+
def frequency
|
219
|
+
|
220
|
+
return brute_frequency unless @seconds && @seconds.length > 1
|
221
|
+
|
222
|
+
delta = 60
|
223
|
+
prev = @seconds[0]
|
224
|
+
|
225
|
+
@seconds[1..-1].each do |sec|
|
226
|
+
d = sec - prev
|
227
|
+
delta = d if d < delta
|
228
|
+
end
|
229
|
+
|
230
|
+
delta
|
231
|
+
end
|
232
|
+
|
233
|
+
# Returns the shortest delta between two potential occurences of the
|
234
|
+
# schedule described by this cronline.
|
235
|
+
#
|
236
|
+
# .
|
237
|
+
#
|
238
|
+
# For a simple cronline like "*/5 * * * *", obviously the frequency is
|
239
|
+
# five minutes. Why does this method look at a whole year of #next_time ?
|
240
|
+
#
|
241
|
+
# Consider "* * * * sun#2,sun#3", the computed frequency is 1 week
|
242
|
+
# (the shortest delta is the one between the second sunday and the third
|
243
|
+
# sunday). This method takes no chance and runs next_time for the span
|
244
|
+
# of a whole year and keeps the shortest.
|
245
|
+
#
|
246
|
+
# Of course, this method can get VERY slow if you call on it a second-
|
247
|
+
# based cronline...
|
248
|
+
#
|
249
|
+
# Since it's a rarely used method, I haven't taken the time to make it
|
250
|
+
# smarter/faster.
|
251
|
+
#
|
252
|
+
# One obvious improvement would be to cache the result once computed...
|
253
|
+
#
|
254
|
+
# See https://github.com/jmettraux/rufus-scheduler/issues/89
|
255
|
+
# for a discussion about this method.
|
256
|
+
#
|
257
|
+
def brute_frequency
|
258
|
+
|
259
|
+
delta = 366 * DAY_S
|
260
|
+
|
261
|
+
t0 = previous_time(Time.local(2000, 1, 1))
|
262
|
+
|
263
|
+
loop do
|
264
|
+
|
265
|
+
break if delta <= 1
|
266
|
+
break if delta <= 60 && @seconds && @seconds.size == 1
|
267
|
+
|
268
|
+
t1 = next_time(t0)
|
269
|
+
d = t1 - t0
|
270
|
+
delta = d if d < delta
|
271
|
+
|
272
|
+
break if @months == nil && t1.month == 2
|
273
|
+
break if t1.year == 2001
|
274
|
+
|
275
|
+
t0 = t1
|
276
|
+
end
|
277
|
+
|
278
|
+
delta
|
279
|
+
end
|
280
|
+
|
281
|
+
protected
|
282
|
+
|
283
|
+
WEEKDAYS = %w[ sun mon tue wed thu fri sat ]
|
284
|
+
DAY_S = 24 * 3600
|
285
|
+
WEEK_S = 7 * DAY_S
|
286
|
+
|
287
|
+
def parse_weekdays(item)
|
288
|
+
|
289
|
+
return nil if item == '*'
|
290
|
+
|
291
|
+
items = item.downcase.split(',')
|
292
|
+
|
293
|
+
weekdays = nil
|
294
|
+
monthdays = nil
|
295
|
+
|
296
|
+
items.each do |it|
|
297
|
+
|
298
|
+
if m = it.match(/^(.+)#(l|-?[12345])$/)
|
299
|
+
|
300
|
+
raise ArgumentError.new(
|
301
|
+
"ranges are not supported for monthdays (#{it})"
|
302
|
+
) if m[1].index('-')
|
303
|
+
|
304
|
+
expr = it.gsub(/#l/, '#-1')
|
305
|
+
|
306
|
+
(monthdays ||= []) << expr
|
307
|
+
|
308
|
+
else
|
309
|
+
|
310
|
+
expr = it.dup
|
311
|
+
WEEKDAYS.each_with_index { |a, i| expr.gsub!(/#{a}/, i.to_s) }
|
312
|
+
|
313
|
+
raise ArgumentError.new(
|
314
|
+
"invalid weekday expression (#{it})"
|
315
|
+
) if expr !~ /^0*[0-7](-0*[0-7])?$/
|
316
|
+
|
317
|
+
its = expr.index('-') ? parse_range(expr, 0, 7) : [ Integer(expr) ]
|
318
|
+
its = its.collect { |i| i == 7 ? 0 : i }
|
319
|
+
|
320
|
+
(weekdays ||= []).concat(its)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
weekdays = weekdays.uniq if weekdays
|
325
|
+
|
326
|
+
[ weekdays, monthdays ]
|
327
|
+
end
|
328
|
+
|
329
|
+
def parse_item(item, min, max)
|
330
|
+
|
331
|
+
return nil if item == '*'
|
332
|
+
|
333
|
+
r = item.split(',').map { |i| parse_range(i.strip, min, max) }.flatten
|
334
|
+
|
335
|
+
raise ArgumentError.new(
|
336
|
+
"found duplicates in #{item.inspect}"
|
337
|
+
) if r.uniq.size < r.size
|
338
|
+
|
339
|
+
r
|
340
|
+
end
|
341
|
+
|
342
|
+
RANGE_REGEX = /^(\*|\d{1,2})(?:-(\d{1,2}))?(?:\/(\d{1,2}))?$/
|
343
|
+
|
344
|
+
def parse_range(item, min, max)
|
345
|
+
|
346
|
+
return %w[ L ] if item == 'L'
|
347
|
+
|
348
|
+
item = '*' + item if item.match(/^\//)
|
349
|
+
|
350
|
+
m = item.match(RANGE_REGEX)
|
351
|
+
|
352
|
+
raise ArgumentError.new(
|
353
|
+
"cannot parse #{item.inspect}"
|
354
|
+
) unless m
|
355
|
+
|
356
|
+
sta = m[1]
|
357
|
+
sta = sta == '*' ? min : sta.to_i
|
358
|
+
|
359
|
+
edn = m[2]
|
360
|
+
edn = edn ? edn.to_i : sta
|
361
|
+
edn = max if m[1] == '*'
|
362
|
+
|
363
|
+
inc = m[3]
|
364
|
+
inc = inc ? inc.to_i : 1
|
365
|
+
|
366
|
+
raise ArgumentError.new(
|
367
|
+
"#{item.inspect} is not in range #{min}..#{max}"
|
368
|
+
) if sta < min || edn > max
|
369
|
+
|
370
|
+
r = []
|
371
|
+
val = sta
|
372
|
+
|
373
|
+
loop do
|
374
|
+
v = val
|
375
|
+
v = 0 if max == 24 && v == 24
|
376
|
+
r << v
|
377
|
+
break if inc == 1 && val == edn
|
378
|
+
val += inc
|
379
|
+
break if inc > 1 && val > edn
|
380
|
+
val = min if val > max
|
381
|
+
end
|
382
|
+
|
383
|
+
r.uniq
|
384
|
+
end
|
385
|
+
|
386
|
+
def sub_match?(time, accessor, values)
|
387
|
+
|
388
|
+
value = time.send(accessor)
|
389
|
+
|
390
|
+
return true if values.nil?
|
391
|
+
return true if values.include?('L') && (time + DAY_S).day == 1
|
392
|
+
|
393
|
+
return true if value == 0 && accessor == :hour && values.include?(24)
|
394
|
+
|
395
|
+
values.include?(value)
|
396
|
+
end
|
397
|
+
|
398
|
+
def monthday_match?(date, values)
|
399
|
+
|
400
|
+
return true if values.nil?
|
401
|
+
|
402
|
+
today_values = monthdays(date)
|
403
|
+
|
404
|
+
(today_values & values).any?
|
405
|
+
end
|
406
|
+
|
407
|
+
def date_match?(date)
|
408
|
+
|
409
|
+
return false unless sub_match?(date, :day, @days)
|
410
|
+
return false unless sub_match?(date, :month, @months)
|
411
|
+
return false unless sub_match?(date, :wday, @weekdays)
|
412
|
+
return false unless monthday_match?(date, @monthdays)
|
413
|
+
true
|
414
|
+
end
|
415
|
+
|
416
|
+
def monthdays(date)
|
417
|
+
|
418
|
+
pos = 1
|
419
|
+
d = date.dup
|
420
|
+
|
421
|
+
loop do
|
422
|
+
d = d - WEEK_S
|
423
|
+
break if d.month != date.month
|
424
|
+
pos = pos + 1
|
425
|
+
end
|
426
|
+
|
427
|
+
neg = -1
|
428
|
+
d = date.dup
|
429
|
+
|
430
|
+
loop do
|
431
|
+
d = d + WEEK_S
|
432
|
+
break if d.month != date.month
|
433
|
+
neg = neg - 1
|
434
|
+
end
|
435
|
+
|
436
|
+
[ "#{WEEKDAYS[date.wday]}##{pos}", "#{WEEKDAYS[date.wday]}##{neg}" ]
|
437
|
+
end
|
438
|
+
|
439
|
+
def local_time(time)
|
440
|
+
|
441
|
+
@timezone ? @timezone.utc_to_local(time.getutc) : time
|
442
|
+
end
|
443
|
+
|
444
|
+
def global_time(time, from_in_utc)
|
445
|
+
|
446
|
+
if @timezone
|
447
|
+
time =
|
448
|
+
begin
|
449
|
+
@timezone.local_to_utc(time)
|
450
|
+
rescue TZInfo::AmbiguousTime
|
451
|
+
@timezone.local_to_utc(time, time.isdst)
|
452
|
+
end
|
453
|
+
time = time.getlocal unless from_in_utc
|
454
|
+
end
|
455
|
+
|
456
|
+
time
|
457
|
+
end
|
458
|
+
|
459
|
+
def round_to_seconds(time)
|
460
|
+
|
461
|
+
# Ruby 1.8 doesn't have #round
|
462
|
+
time.respond_to?(:round) ? time.round : time - time.usec * 1e-6
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module DelayedCronJob
|
2
|
+
class Plugin < Delayed::Plugin
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def next_run_at(job)
|
6
|
+
job.run_at = Cronline.new(job.cron).next_time(Delayed::Job.db_time_now)
|
7
|
+
end
|
8
|
+
|
9
|
+
def cron?(job)
|
10
|
+
job.cron.present?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
callbacks do |lifecycle|
|
15
|
+
# Calculate the next run_at based on the cron attribute before enqueue.
|
16
|
+
lifecycle.before(:enqueue) do |job|
|
17
|
+
next_run_at(job) if cron?(job)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Prevent rescheduling of failed jobs as this is already done
|
21
|
+
# after perform.
|
22
|
+
lifecycle.around(:error) do |worker, job, &block|
|
23
|
+
if cron?(job)
|
24
|
+
job.last_error = "#{$ERROR_INFO.message}\n#{$ERROR_INFO.backtrace.join("\n")}"
|
25
|
+
worker.job_say(job,
|
26
|
+
"FAILED with #{$ERROR_INFO.class.name}: #{$ERROR_INFO.message}",
|
27
|
+
Logger::ERROR)
|
28
|
+
job.destroy
|
29
|
+
else
|
30
|
+
# No cron job - proceed as normal
|
31
|
+
block.call(worker, job)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Reset the last_error to have the correct status of the last run.
|
36
|
+
lifecycle.before(:perform) do |worker, job|
|
37
|
+
if cron?(job)
|
38
|
+
job.last_error = nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Schedule the next run based on the cron attribute.
|
43
|
+
lifecycle.after(:perform) do |worker, job|
|
44
|
+
if cron?(job)
|
45
|
+
next_job = job.dup
|
46
|
+
next_job.id = job.id
|
47
|
+
next_job.created_at = job.created_at
|
48
|
+
next_job.locked_at = nil
|
49
|
+
next_job.locked_by = nil
|
50
|
+
next_job.attempts += 1
|
51
|
+
next_run_at(next_job)
|
52
|
+
next_job.save!
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'generators/delayed_job/delayed_job_generator'
|
2
|
+
require 'generators/delayed_job/next_migration_version'
|
3
|
+
require 'rails/generators/migration'
|
4
|
+
require 'rails/generators/active_record'
|
5
|
+
|
6
|
+
# Extend the DelayedJobGenerator so that it creates an AR migration
|
7
|
+
module DelayedJob
|
8
|
+
class CronGenerator < ::DelayedJobGenerator
|
9
|
+
include Rails::Generators::Migration
|
10
|
+
extend NextMigrationVersion
|
11
|
+
|
12
|
+
self.source_paths << File.join(File.dirname(__FILE__), 'templates')
|
13
|
+
|
14
|
+
def create_migration_file
|
15
|
+
migration_template('cron_migration.rb', 'db/migrate/add_cron_to_delayed_jobs.rb')
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.next_migration_number(dirname)
|
19
|
+
ActiveRecord::Generators::Base.next_migration_number(dirname)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
describe DelayedCronJob do
|
4
|
+
|
5
|
+
class TestJob
|
6
|
+
def perform; end
|
7
|
+
end
|
8
|
+
|
9
|
+
before { Delayed::Job.delete_all }
|
10
|
+
|
11
|
+
let(:cron) { '5 1 * * *' }
|
12
|
+
let(:handler) { TestJob.new }
|
13
|
+
let(:job) { Delayed::Job.enqueue(handler, cron: cron) }
|
14
|
+
let(:worker) { Delayed::Worker.new }
|
15
|
+
let(:now) { Delayed::Job.db_time_now }
|
16
|
+
let(:next_run) do
|
17
|
+
run = now.hour * 60 + now.min >= 65 ? now + 1.day : now
|
18
|
+
Time.utc(run.year, run.month, run.day, 1, 5)
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'with cron' do
|
22
|
+
it 'sets run_at on enqueue' do
|
23
|
+
expect { job }.to change { Delayed::Job.count }.by(1)
|
24
|
+
expect(job.run_at).to eq(next_run)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'enqueue fails with invalid cron' do
|
28
|
+
expect { Delayed::Job.enqueue(handler, cron: 'no valid cron') }.
|
29
|
+
to raise_error(ArgumentError)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'schedules a new job after success' do
|
33
|
+
job.update_column(:run_at, now)
|
34
|
+
|
35
|
+
worker.work_off
|
36
|
+
|
37
|
+
expect(Delayed::Job.count).to eq(1)
|
38
|
+
j = Delayed::Job.first
|
39
|
+
expect(j.id).to eq(job.id)
|
40
|
+
expect(j.cron).to eq(job.cron)
|
41
|
+
expect(j.run_at).to eq(next_run)
|
42
|
+
expect(j.attempts).to eq(1)
|
43
|
+
expect(j.last_error).to eq(nil)
|
44
|
+
expect(j.created_at).to eq(job.created_at)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'schedules a new job after failure' do
|
48
|
+
allow_any_instance_of(TestJob).to receive(:perform).and_raise('Fail!')
|
49
|
+
job.update(run_at: now)
|
50
|
+
|
51
|
+
worker.work_off
|
52
|
+
|
53
|
+
expect(Delayed::Job.count).to eq(1)
|
54
|
+
j = Delayed::Job.first
|
55
|
+
expect(j.id).to eq(job.id)
|
56
|
+
expect(j.cron).to eq(job.cron)
|
57
|
+
expect(j.run_at).to eq(next_run)
|
58
|
+
expect(j.last_error).to match('Fail!')
|
59
|
+
expect(j.created_at).to eq(job.created_at)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'schedules a new job after timeout' do
|
63
|
+
Delayed::Worker.max_run_time = 1.second
|
64
|
+
job.update_column(:run_at, now)
|
65
|
+
allow_any_instance_of(TestJob).to receive(:perform) { sleep 2 }
|
66
|
+
|
67
|
+
worker.work_off
|
68
|
+
|
69
|
+
expect(Delayed::Job.count).to eq(1)
|
70
|
+
j = Delayed::Job.first
|
71
|
+
expect(j.id).to eq(job.id)
|
72
|
+
expect(j.cron).to eq(job.cron)
|
73
|
+
expect(j.run_at).to eq(next_run)
|
74
|
+
expect(j.attempts).to eq(1)
|
75
|
+
expect(j.last_error).to match("execution expired")
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'schedules new job after deserialization error' do
|
79
|
+
Delayed::Worker.max_run_time = 1.second
|
80
|
+
job.update_column(:run_at, now)
|
81
|
+
allow_any_instance_of(TestJob).to receive(:perform).and_raise(Delayed::DeserializationError)
|
82
|
+
|
83
|
+
worker.work_off
|
84
|
+
|
85
|
+
expect(Delayed::Job.count).to eq(1)
|
86
|
+
j = Delayed::Job.first
|
87
|
+
expect(j.last_error).to match("Delayed::DeserializationError")
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'has empty last_error after success' do
|
91
|
+
job.update(run_at: now, last_error: 'Last error')
|
92
|
+
|
93
|
+
worker.work_off
|
94
|
+
|
95
|
+
j = Delayed::Job.first
|
96
|
+
expect(j.last_error).to eq(nil)
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'has correct last_error after success' do
|
100
|
+
allow_any_instance_of(TestJob).to receive(:perform).and_raise('Fail!')
|
101
|
+
job.update(run_at: now, last_error: 'Last error')
|
102
|
+
|
103
|
+
worker.work_off
|
104
|
+
|
105
|
+
j = Delayed::Job.first
|
106
|
+
expect(j.last_error).to match('Fail!')
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'uses correct db time for next run' do
|
110
|
+
if Time.now != now
|
111
|
+
job = Delayed::Job.enqueue(handler, cron: '* * * * *')
|
112
|
+
run = now.hour == 23 && now.min == 59 ? now + 1.day : now
|
113
|
+
hour = now.min == 59 ? (now.hour + 1) % 24 : now.hour
|
114
|
+
run_at = Time.utc(run.year, run.month, run.day, hour, (now.min + 1) % 60)
|
115
|
+
expect(job.run_at).to eq(run_at)
|
116
|
+
else
|
117
|
+
pending "This test only makes sense in non-UTC time zone"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'increases attempts on each run' do
|
122
|
+
job.update(run_at: now, attempts: 3)
|
123
|
+
|
124
|
+
worker.work_off
|
125
|
+
|
126
|
+
j = Delayed::Job.first
|
127
|
+
expect(j.attempts).to eq(4)
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'is not stopped by max attempts' do
|
131
|
+
job.update(run_at: now, attempts: Delayed::Worker.max_attempts + 1)
|
132
|
+
|
133
|
+
worker.work_off
|
134
|
+
|
135
|
+
expect(Delayed::Job.count).to eq(1)
|
136
|
+
j = Delayed::Job.first
|
137
|
+
expect(j.attempts).to eq(job.attempts + 1)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context 'without cron' do
|
142
|
+
it 'reschedules the original job after a single failure' do
|
143
|
+
allow_any_instance_of(TestJob).to receive(:perform).and_raise('Fail!')
|
144
|
+
job = Delayed::Job.enqueue(handler)
|
145
|
+
|
146
|
+
worker.work_off
|
147
|
+
|
148
|
+
expect(Delayed::Job.count).to eq(1)
|
149
|
+
j = Delayed::Job.first
|
150
|
+
expect(j.id).to eq(job.id)
|
151
|
+
expect(j.cron).to eq(nil)
|
152
|
+
expect(j.last_error).to match('Fail!')
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'does not reschedule a job after a successful run' do
|
156
|
+
job = Delayed::Job.enqueue(handler)
|
157
|
+
|
158
|
+
worker.work_off
|
159
|
+
|
160
|
+
expect(Delayed::Job.count).to eq(0)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# The generated `.rspec` file contains `--require spec_helper` which will cause this
|
4
|
+
# file to always be loaded, without a need to explicitly require it in any files.
|
5
|
+
#
|
6
|
+
# Given that it is always loaded, you are encouraged to keep this file as
|
7
|
+
# light-weight as possible. Requiring heavyweight dependencies from this file
|
8
|
+
# will add to the boot time of your test suite on EVERY test run, even for an
|
9
|
+
# individual file that may not need all of that loaded. Instead, make a
|
10
|
+
# separate helper file that requires this one and then use it only in the specs
|
11
|
+
# that actually need it.
|
12
|
+
#
|
13
|
+
# The `.rspec` file also contains a few flags that are not defaults but that
|
14
|
+
# users commonly want.
|
15
|
+
#
|
16
|
+
|
17
|
+
require 'delayed_job_active_record'
|
18
|
+
require 'delayed_cron_job'
|
19
|
+
require 'pry'
|
20
|
+
|
21
|
+
Delayed::Worker.logger = Logger.new('/tmp/dj.log')
|
22
|
+
ENV['RAILS_ENV'] = 'test'
|
23
|
+
|
24
|
+
ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
|
25
|
+
ActiveRecord::Base.logger = Delayed::Worker.logger
|
26
|
+
ActiveRecord::Migration.verbose = false
|
27
|
+
|
28
|
+
ActiveRecord::Schema.define do
|
29
|
+
create_table :delayed_jobs, :force => true do |t|
|
30
|
+
t.integer :priority, :default => 0
|
31
|
+
t.integer :attempts, :default => 0
|
32
|
+
t.text :handler
|
33
|
+
t.text :last_error
|
34
|
+
t.datetime :run_at
|
35
|
+
t.datetime :locked_at
|
36
|
+
t.datetime :failed_at
|
37
|
+
t.string :locked_by
|
38
|
+
t.string :queue
|
39
|
+
t.string :cron
|
40
|
+
t.timestamps
|
41
|
+
end
|
42
|
+
|
43
|
+
add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority'
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
48
|
+
RSpec.configure do |config|
|
49
|
+
# The settings below are suggested to provide a good initial experience
|
50
|
+
# with RSpec, but feel free to customize to your heart's content.
|
51
|
+
=begin
|
52
|
+
# These two settings work together to allow you to limit a spec run
|
53
|
+
# to individual examples or groups you care about by tagging them with
|
54
|
+
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
55
|
+
# get run.
|
56
|
+
config.filter_run :focus
|
57
|
+
config.run_all_when_everything_filtered = true
|
58
|
+
|
59
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
60
|
+
# file, and it's useful to allow more verbose output when running an
|
61
|
+
# individual spec file.
|
62
|
+
if config.files_to_run.one?
|
63
|
+
# Use the documentation formatter for detailed output,
|
64
|
+
# unless a formatter has already been configured
|
65
|
+
# (e.g. via a command-line flag).
|
66
|
+
config.default_formatter = 'doc'
|
67
|
+
end
|
68
|
+
|
69
|
+
# Print the 10 slowest examples and example groups at the
|
70
|
+
# end of the spec run, to help surface which specs are running
|
71
|
+
# particularly slow.
|
72
|
+
config.profile_examples = 10
|
73
|
+
|
74
|
+
# Run specs in random order to surface order dependencies. If you find an
|
75
|
+
# order dependency and want to debug it, you can fix the order by providing
|
76
|
+
# the seed, which is printed after each run.
|
77
|
+
# --seed 1234
|
78
|
+
config.order = :random
|
79
|
+
|
80
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
81
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
82
|
+
# test failures related to randomization by passing the same `--seed` value
|
83
|
+
# as the one that triggered the failure.
|
84
|
+
Kernel.srand config.seed
|
85
|
+
|
86
|
+
# rspec-expectations config goes here. You can use an alternate
|
87
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
88
|
+
# assertions if you prefer.
|
89
|
+
config.expect_with :rspec do |expectations|
|
90
|
+
# Enable only the newer, non-monkey-patching expect syntax.
|
91
|
+
# For more details, see:
|
92
|
+
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
93
|
+
expectations.syntax = :expect
|
94
|
+
end
|
95
|
+
|
96
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
97
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
98
|
+
config.mock_with :rspec do |mocks|
|
99
|
+
# Enable only the newer, non-monkey-patching expect syntax.
|
100
|
+
# For more details, see:
|
101
|
+
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
102
|
+
mocks.syntax = :expect
|
103
|
+
|
104
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
105
|
+
# a real object. This is generally recommended.
|
106
|
+
mocks.verify_partial_doubles = true
|
107
|
+
end
|
108
|
+
=end
|
109
|
+
end
|
metadata
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: delayed_cron_job
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Pascal Zumkehr
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-07-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: delayed_job
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.6'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.6'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: delayed_job_active_record
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ! '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: ! "Delayed Cron Job is an extension to Delayed::Job\n that
|
98
|
+
allows you to set cron expressions for your\n jobs to run
|
99
|
+
regularly."
|
100
|
+
email:
|
101
|
+
- spam@codez.ch
|
102
|
+
executables: []
|
103
|
+
extensions: []
|
104
|
+
extra_rdoc_files: []
|
105
|
+
files:
|
106
|
+
- .gitignore
|
107
|
+
- .rspec
|
108
|
+
- Gemfile
|
109
|
+
- LICENSE.txt
|
110
|
+
- README.md
|
111
|
+
- Rakefile
|
112
|
+
- delayed_cron_job.gemspec
|
113
|
+
- lib/delayed_cron_job.rb
|
114
|
+
- lib/delayed_cron_job/cronline.rb
|
115
|
+
- lib/delayed_cron_job/plugin.rb
|
116
|
+
- lib/delayed_cron_job/version.rb
|
117
|
+
- lib/generators/delayed_job/cron_generator.rb
|
118
|
+
- lib/generators/delayed_job/templates/cron_migration.rb
|
119
|
+
- spec/delayed_cron_job_spec.rb
|
120
|
+
- spec/spec_helper.rb
|
121
|
+
homepage: https://github.com/codez/delayed_cron_job
|
122
|
+
licenses:
|
123
|
+
- MIT
|
124
|
+
metadata: {}
|
125
|
+
post_install_message:
|
126
|
+
rdoc_options: []
|
127
|
+
require_paths:
|
128
|
+
- lib
|
129
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ! '>='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
requirements: []
|
140
|
+
rubyforge_project:
|
141
|
+
rubygems_version: 2.2.2
|
142
|
+
signing_key:
|
143
|
+
specification_version: 4
|
144
|
+
summary: An extension to Delayed::Job that allows you to set cron expressions for
|
145
|
+
your jobs to run regularly.
|
146
|
+
test_files:
|
147
|
+
- spec/delayed_cron_job_spec.rb
|
148
|
+
- spec/spec_helper.rb
|
149
|
+
has_rdoc:
|