delayed_cron_job 0.7.1 → 0.8.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 +5 -5
- data/.gitignore +2 -1
- data/.travis.yml +3 -1
- data/{LICENSE.txt → LICENSE} +1 -1
- data/README.md +121 -7
- data/delayed_cron_job.gemspec +1 -0
- data/lib/delayed_cron_job/backend/active_record/railtie.rb +14 -0
- data/lib/delayed_cron_job/backend/updatable_cron.rb +15 -2
- data/lib/delayed_cron_job/plugin.rb +11 -17
- data/lib/delayed_cron_job/version.rb +1 -1
- data/lib/delayed_cron_job.rb +19 -5
- data/lib/generators/delayed_job/cron_generator.rb +13 -2
- data/lib/generators/delayed_job/templates/cron_migration.rb +2 -2
- data/spec/active_job_spec.rb +2 -2
- data/spec/delayed_cron_job_spec.rb +59 -17
- data/spec/spec_helper.rb +3 -1
- metadata +22 -9
- data/lib/delayed_cron_job/cronline.rb +0 -465
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 06776f6ef3c8e55b7e737ecd6e2eb0739c8185c72f108bd6bde3ec59aa44780e
|
4
|
+
data.tar.gz: 50af71b765e892796d0b04866e2a99558664f0c236ebaca532b3d15355843eee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: efffba2bfd9c55a156e649fa70adb9150717f1ed41831b8031791155054d6740c42119f82f57224323c47908d24a50621698bf872157ebb71a3bc1c4d3d2c61d
|
7
|
+
data.tar.gz: 4869d5b81626d2b29e8037cc371ce02deb846981f6bdf26a463fa893c60dc7e6577bc24312dc96056f38d3aac522280a0f56672b5642f9aea80ce4f26f22358b
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/{LICENSE.txt → LICENSE}
RENAMED
data/README.md
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
# Delayed::Cron::Job
|
2
2
|
|
3
|
+
[](https://app.travis-ci.com/codez/delayed_cron_job)
|
4
|
+
|
3
5
|
Delayed::Cron::Job is an extension to Delayed::Job that allows you to set
|
4
6
|
cron expressions for your jobs to run repeatedly.
|
5
7
|
|
6
8
|
## Installation
|
7
9
|
|
8
|
-
Add
|
10
|
+
Add the following line to your application's Gemfile. Add it after the lines for all other `delayed_job*` gems so the gem can properly integrate with the Delayed::Job code.
|
9
11
|
|
10
12
|
gem 'delayed_cron_job'
|
11
13
|
|
@@ -26,17 +28,123 @@ There are no additional steps for `delayed_job_mongoid`.
|
|
26
28
|
|
27
29
|
When enqueuing a job, simply pass the `cron` option, e.g.:
|
28
30
|
|
29
|
-
|
31
|
+
```ruby
|
32
|
+
Delayed::Job.enqueue(MyRepeatedJob.new, cron: '15 */6 * * 1-5')
|
33
|
+
```
|
30
34
|
|
31
|
-
Or, when using
|
35
|
+
Or, when using ActiveJob:
|
32
36
|
|
33
|
-
|
37
|
+
```ruby
|
38
|
+
MyJob.set(cron: '*/5 * * * *').perform_later
|
39
|
+
```
|
34
40
|
|
35
41
|
Any crontab compatible cron expressions are supported (see `man 5 crontab`).
|
36
|
-
|
37
|
-
|
42
|
+
Cron parsing is handled by [Fugit](https://github.com/floraison/fugit).
|
43
|
+
|
44
|
+
## Scheduling
|
45
|
+
|
46
|
+
Usually, you want to schedule all existing cron jobs when deploying the
|
47
|
+
application. Using a common super class makes this simple.
|
48
|
+
|
49
|
+
### Custom CronJob superclass
|
50
|
+
|
51
|
+
`app/jobs/cron_job.rb`:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
# Default configuration in `app/jobs/application_job.rb`, or subclass
|
55
|
+
# ActiveJob::Base .
|
56
|
+
class CronJob < ApplicationJob
|
57
|
+
|
58
|
+
class_attribute :cron_expression
|
59
|
+
|
60
|
+
class << self
|
61
|
+
|
62
|
+
def schedule
|
63
|
+
set(cron: cron_expression).perform_later unless scheduled?
|
64
|
+
end
|
65
|
+
|
66
|
+
def remove
|
67
|
+
delayed_job.destroy if scheduled?
|
68
|
+
end
|
69
|
+
|
70
|
+
def scheduled?
|
71
|
+
delayed_job.present?
|
72
|
+
end
|
73
|
+
|
74
|
+
def delayed_job
|
75
|
+
Delayed::Job
|
76
|
+
.where('handler LIKE ?', "%job_class: #{name}%")
|
77
|
+
.first
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
### Example Job inheriting from CronJob
|
85
|
+
|
86
|
+
Then, an example job that triggers E-Mail-sending with default cron time at
|
87
|
+
noon every day:
|
38
88
|
|
39
|
-
|
89
|
+
`app/jobs/noon_job.rb`:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
|
93
|
+
# Note that it inherits from `CronJob`
|
94
|
+
class NoonJob < CronJob
|
95
|
+
# set the (default) cron expression
|
96
|
+
self.cron_expression = '0 12 * * *'
|
97
|
+
|
98
|
+
# will enqueue the mailing delivery job
|
99
|
+
def perform
|
100
|
+
UserMailer.daily_notice(User.first).deliver_later
|
101
|
+
end
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
### Automatic Scheduling after db:migrate
|
106
|
+
|
107
|
+
Jobs with a `cron` definition are rescheduled automatically only when a job
|
108
|
+
instance finished its work. So there needs to be an initial scheduling of all
|
109
|
+
cron jobs. If you do not want to do this manually (e.g. using `rails console`)
|
110
|
+
or with your application logic, you can e.g. hook into the `rails db:*` rake
|
111
|
+
tasks:
|
112
|
+
|
113
|
+
`lib/tasks/jobs.rake`:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
namespace :db do
|
117
|
+
desc 'Schedule all cron jobs'
|
118
|
+
task :schedule_jobs => :environment do
|
119
|
+
# Need to load all jobs definitions in order to find subclasses
|
120
|
+
glob = Rails.root.join('app', 'jobs', '**', '*_job.rb')
|
121
|
+
Dir.glob(glob).each { |file| require file }
|
122
|
+
CronJob.subclasses.each(&:schedule)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# invoke schedule_jobs automatically after every migration and schema load.
|
127
|
+
%w(db:migrate db:schema:load).each do |task|
|
128
|
+
Rake::Task[task].enhance do
|
129
|
+
Rake::Task['db:schedule_jobs'].invoke
|
130
|
+
end
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
Now, if you run `rails db:migrate`, `rails db:schema:load` or `rails
|
135
|
+
db:schedule_jobs` all jobs inheriting from `CronJob` are scheduled.
|
136
|
+
|
137
|
+
*If you are not using ActiveJob, the same approach may be used with minor
|
138
|
+
adjustments.*
|
139
|
+
|
140
|
+
### Changing the schedule
|
141
|
+
|
142
|
+
Note that if you have a CronJob scheduled and change its `cron_expression` in
|
143
|
+
its source file, you will have to remove any scheduled instances of the Job and
|
144
|
+
reschedule it (e.g. with the snippet above: `rails db:migrate`). This is because
|
145
|
+
the `cron_expression` is already persisted in the database (as `cron`).
|
146
|
+
|
147
|
+
## Details
|
40
148
|
|
41
149
|
The initial `run_at` value is computed during the `#enqueue` method call.
|
42
150
|
If you create `Delayed::Job` database entries directly, make sure to set
|
@@ -65,3 +173,9 @@ jobs.
|
|
65
173
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
66
174
|
4. Push to the branch (`git push origin my-new-feature`)
|
67
175
|
5. Create a new Pull Request
|
176
|
+
|
177
|
+
## License
|
178
|
+
|
179
|
+
Delayed::Cron::Job is released under the terms of the MIT License.
|
180
|
+
Copyright 2014-2021 Pascal Zumkehr. See [LICENSE](LICENSE) for further
|
181
|
+
information.
|
data/delayed_cron_job.gemspec
CHANGED
@@ -0,0 +1,14 @@
|
|
1
|
+
module DelayedCronJob
|
2
|
+
module Backend
|
3
|
+
module ActiveRecord
|
4
|
+
class Railtie < ::Rails::Railtie
|
5
|
+
config.after_initialize do
|
6
|
+
Delayed::Backend::ActiveRecord::Job.send(:include, DelayedCronJob::Backend::UpdatableCron)
|
7
|
+
if Delayed::Backend::ActiveRecord::Job.respond_to?(:attr_accessible)
|
8
|
+
Delayed::Backend::ActiveRecord::Job.attr_accessible(:cron)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -4,14 +4,27 @@ module DelayedCronJob
|
|
4
4
|
|
5
5
|
def self.included(klass)
|
6
6
|
klass.send(:before_save, :set_next_run_at, :if => :cron_changed?)
|
7
|
+
klass.attr_accessor :schedule_instead_of_destroy
|
7
8
|
end
|
8
9
|
|
9
10
|
def set_next_run_at
|
10
11
|
if cron.present?
|
11
|
-
|
12
|
+
now = Delayed::Job.db_time_now
|
13
|
+
self.run_at = Fugit::Cron.do_parse(cron).next_time(now).to_local_time
|
12
14
|
end
|
13
15
|
end
|
14
16
|
|
17
|
+
def destroy
|
18
|
+
super unless schedule_instead_of_destroy
|
19
|
+
end
|
20
|
+
|
21
|
+
def schedule_next_run
|
22
|
+
self.attempts += 1
|
23
|
+
unlock
|
24
|
+
set_next_run_at
|
25
|
+
save!
|
26
|
+
end
|
27
|
+
|
15
28
|
end
|
16
29
|
end
|
17
|
-
end
|
30
|
+
end
|
@@ -17,7 +17,6 @@ module DelayedCronJob
|
|
17
17
|
worker.job_say(job,
|
18
18
|
"FAILED with #{$ERROR_INFO.class.name}: #{$ERROR_INFO.message}",
|
19
19
|
Logger::ERROR)
|
20
|
-
job.destroy
|
21
20
|
else
|
22
21
|
# No cron job - proceed as normal
|
23
22
|
block.call(worker, job)
|
@@ -26,31 +25,26 @@ module DelayedCronJob
|
|
26
25
|
|
27
26
|
# Reset the last_error to have the correct status of the last run.
|
28
27
|
lifecycle.before(:perform) do |worker, job|
|
29
|
-
if cron?(job)
|
30
|
-
job.last_error = nil
|
31
|
-
end
|
28
|
+
job.last_error = nil if cron?(job)
|
32
29
|
end
|
33
30
|
|
34
|
-
#
|
31
|
+
# Prevent destruction of cron jobs
|
35
32
|
lifecycle.after(:invoke_job) do |job|
|
36
|
-
if cron?(job)
|
37
|
-
job.cron = job.class.where(:id => job.id).pluck(:cron).first
|
38
|
-
end
|
33
|
+
job.schedule_instead_of_destroy = true if cron?(job)
|
39
34
|
end
|
40
35
|
|
41
36
|
# Schedule the next run based on the cron attribute.
|
42
37
|
lifecycle.after(:perform) do |worker, job|
|
43
|
-
if cron?(job)
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
38
|
+
if cron?(job) && !job.destroyed?
|
39
|
+
job.cron = job.class.where(:id => job.id).pluck(:cron).first
|
40
|
+
if job.cron.present?
|
41
|
+
job.schedule_next_run
|
42
|
+
else
|
43
|
+
job.schedule_instead_of_destroy = false
|
44
|
+
job.destroy
|
45
|
+
end
|
51
46
|
end
|
52
47
|
end
|
53
48
|
end
|
54
|
-
|
55
49
|
end
|
56
50
|
end
|
data/lib/delayed_cron_job.rb
CHANGED
@@ -1,10 +1,18 @@
|
|
1
1
|
require 'delayed_job'
|
2
2
|
require 'English'
|
3
|
-
require '
|
3
|
+
require 'fugit'
|
4
4
|
require 'delayed_cron_job/plugin'
|
5
5
|
require 'delayed_cron_job/version'
|
6
6
|
require 'delayed_cron_job/backend/updatable_cron'
|
7
7
|
|
8
|
+
begin
|
9
|
+
require 'delayed_job_active_record'
|
10
|
+
rescue LoadError; end
|
11
|
+
|
12
|
+
begin
|
13
|
+
require 'delayed_job_mongoid'
|
14
|
+
rescue LoadError; end
|
15
|
+
|
8
16
|
module DelayedCronJob
|
9
17
|
|
10
18
|
end
|
@@ -16,9 +24,15 @@ if defined?(Delayed::Backend::Mongoid)
|
|
16
24
|
end
|
17
25
|
|
18
26
|
if defined?(Delayed::Backend::ActiveRecord)
|
19
|
-
|
20
|
-
|
21
|
-
|
27
|
+
if defined?(Rails::Railtie)
|
28
|
+
# Postpone initialization to railtie for correct order
|
29
|
+
require 'delayed_cron_job/backend/active_record/railtie'
|
30
|
+
else
|
31
|
+
# Do the same as in the railtie
|
32
|
+
Delayed::Backend::ActiveRecord::Job.send(:include, DelayedCronJob::Backend::UpdatableCron)
|
33
|
+
if Delayed::Backend::ActiveRecord::Job.respond_to?(:attr_accessible)
|
34
|
+
Delayed::Backend::ActiveRecord::Job.attr_accessible(:cron)
|
35
|
+
end
|
22
36
|
end
|
23
37
|
end
|
24
38
|
|
@@ -35,4 +49,4 @@ if defined?(::ActiveJob)
|
|
35
49
|
ActiveJob::QueueAdapters::DelayedJobAdapter.send(:include, DelayedCronJob::ActiveJob::QueueAdapter)
|
36
50
|
end
|
37
51
|
|
38
|
-
end
|
52
|
+
end
|
@@ -12,11 +12,22 @@ module DelayedJob
|
|
12
12
|
self.source_paths << File.join(File.dirname(__FILE__), 'templates')
|
13
13
|
|
14
14
|
def create_migration_file
|
15
|
-
migration_template('cron_migration.rb',
|
15
|
+
migration_template('cron_migration.rb',
|
16
|
+
'db/migrate/add_cron_to_delayed_jobs.rb',
|
17
|
+
migration_version: migration_version)
|
16
18
|
end
|
17
19
|
|
18
20
|
def self.next_migration_number(dirname)
|
19
21
|
ActiveRecord::Generators::Base.next_migration_number(dirname)
|
20
22
|
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def migration_version
|
27
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
28
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
21
32
|
end
|
22
|
-
end
|
33
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
class AddCronToDelayedJobs < ActiveRecord::Migration
|
1
|
+
class AddCronToDelayedJobs < ActiveRecord::Migration<%= migration_version %>
|
2
2
|
def self.up
|
3
3
|
add_column :delayed_jobs, :cron, :string
|
4
4
|
end
|
@@ -6,4 +6,4 @@ class AddCronToDelayedJobs < ActiveRecord::Migration
|
|
6
6
|
def self.down
|
7
7
|
remove_column :delayed_jobs, :cron
|
8
8
|
end
|
9
|
-
end
|
9
|
+
end
|
data/spec/active_job_spec.rb
CHANGED
@@ -21,7 +21,7 @@ describe ActiveJob do
|
|
21
21
|
let(:now) { Delayed::Job.db_time_now }
|
22
22
|
let(:next_run) do
|
23
23
|
run = now.hour * 60 + now.min >= 65 ? now + 1.day : now
|
24
|
-
Time.
|
24
|
+
Time.zone.local(run.year, run.month, run.day, 1, 5)
|
25
25
|
end
|
26
26
|
|
27
27
|
context 'with cron' do
|
@@ -66,4 +66,4 @@ describe ActiveJob do
|
|
66
66
|
expect(delayed_job.cron).to eq(cron)
|
67
67
|
end
|
68
68
|
end
|
69
|
-
end
|
69
|
+
end
|
@@ -6,6 +6,16 @@ describe DelayedCronJob do
|
|
6
6
|
def perform; end
|
7
7
|
end
|
8
8
|
|
9
|
+
class DatabaseDisconnectPlugin < Delayed::Plugin
|
10
|
+
|
11
|
+
callbacks do |lifecycle|
|
12
|
+
lifecycle.after(:perform) do
|
13
|
+
ActiveRecord::Base.connection.disconnect!
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
9
19
|
before { Delayed::Job.delete_all }
|
10
20
|
|
11
21
|
let(:cron) { '5 1 * * *' }
|
@@ -15,7 +25,7 @@ describe DelayedCronJob do
|
|
15
25
|
let(:now) { Delayed::Job.db_time_now }
|
16
26
|
let(:next_run) do
|
17
27
|
run = now.hour * 60 + now.min >= 65 ? now + 1.day : now
|
18
|
-
Time.
|
28
|
+
Time.zone.local(run.year, run.month, run.day, 1, 5)
|
19
29
|
end
|
20
30
|
|
21
31
|
context 'with cron' do
|
@@ -25,8 +35,8 @@ describe DelayedCronJob do
|
|
25
35
|
end
|
26
36
|
|
27
37
|
it 'enqueue fails with invalid cron' do
|
28
|
-
expect { Delayed::Job.enqueue(handler, cron: 'no valid cron') }
|
29
|
-
to raise_error(ArgumentError)
|
38
|
+
expect { Delayed::Job.enqueue(handler, cron: 'no valid cron') }
|
39
|
+
.to raise_error(ArgumentError)
|
30
40
|
end
|
31
41
|
|
32
42
|
it 'schedules a new job after success' do
|
@@ -74,19 +84,16 @@ describe DelayedCronJob do
|
|
74
84
|
expect(j.cron).to eq(job.cron)
|
75
85
|
expect(j.run_at).to eq(next_run)
|
76
86
|
expect(j.attempts).to eq(1)
|
77
|
-
expect(j.last_error).to match(
|
87
|
+
expect(j.last_error).to match('execution expired')
|
78
88
|
end
|
79
89
|
|
80
|
-
it '
|
81
|
-
Delayed::Worker.max_run_time = 1.second
|
90
|
+
it 'does not schedule new job after deserialization error' do
|
82
91
|
job.update_column(:run_at, now)
|
83
92
|
allow_any_instance_of(TestJob).to receive(:perform).and_raise(Delayed::DeserializationError)
|
84
93
|
|
85
94
|
worker.work_off
|
86
95
|
|
87
|
-
expect(Delayed::Job.count).to eq(
|
88
|
-
j = Delayed::Job.first
|
89
|
-
expect(j.last_error).to match("Delayed::DeserializationError")
|
96
|
+
expect(Delayed::Job.count).to eq(0)
|
90
97
|
end
|
91
98
|
|
92
99
|
it 'has empty last_error after success' do
|
@@ -109,14 +116,20 @@ describe DelayedCronJob do
|
|
109
116
|
end
|
110
117
|
|
111
118
|
it 'uses correct db time for next run' do
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
119
|
+
local_zone = Time.zone
|
120
|
+
begin
|
121
|
+
Time.zone = nil
|
122
|
+
if Time.now != now
|
123
|
+
job = Delayed::Job.enqueue(handler, cron: '* * * * *')
|
124
|
+
run = now.hour == 23 && now.min == 59 ? now + 1.day : now
|
125
|
+
hour = now.min == 59 ? (now.hour + 1) % 24 : now.hour
|
126
|
+
run_at = Time.utc(run.year, run.month, run.day, hour, (now.min + 1) % 60)
|
127
|
+
expect(job.run_at).to eq(run_at)
|
128
|
+
else
|
129
|
+
pending 'This test only makes sense in non-UTC time zone'
|
130
|
+
end
|
131
|
+
ensure
|
132
|
+
Time.zone = local_zone
|
120
133
|
end
|
121
134
|
end
|
122
135
|
|
@@ -168,6 +181,35 @@ describe DelayedCronJob do
|
|
168
181
|
expect { worker.work_off }.to change { Delayed::Job.count }.by(-1)
|
169
182
|
end
|
170
183
|
|
184
|
+
context 'when database connection is lost' do
|
185
|
+
around(:each) do |example|
|
186
|
+
Delayed::Worker.plugins.unshift DatabaseDisconnectPlugin
|
187
|
+
# hold onto a connection so the in-memory database isn't lost when disconnected
|
188
|
+
temp_connection = ActiveRecord::Base.connection_pool.checkout
|
189
|
+
example.run
|
190
|
+
ActiveRecord::Base.connection_pool.checkin temp_connection
|
191
|
+
Delayed::Worker.plugins.delete DatabaseDisconnectPlugin
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'does not lose the job if database connection is lost' do
|
195
|
+
job.update_column(:run_at, now)
|
196
|
+
job.reload # adjusts granularity of run_at datetime
|
197
|
+
|
198
|
+
begin
|
199
|
+
worker.work_off
|
200
|
+
rescue StandardError
|
201
|
+
# Attempting to save the clone delayed_job will raise an exception due to the database connection being closed
|
202
|
+
end
|
203
|
+
|
204
|
+
ActiveRecord::Base.connection.reconnect!
|
205
|
+
|
206
|
+
expect(Delayed::Job.count).to eq(1)
|
207
|
+
j = Delayed::Job.first
|
208
|
+
expect(j.id).to eq(job.id)
|
209
|
+
expect(j.cron).to eq(job.cron)
|
210
|
+
expect(j.attempts).to eq(0)
|
211
|
+
end
|
212
|
+
end
|
171
213
|
end
|
172
214
|
|
173
215
|
context 'without cron' do
|
data/spec/spec_helper.rb
CHANGED
@@ -21,9 +21,11 @@ require 'delayed_cron_job'
|
|
21
21
|
Delayed::Worker.logger = Logger.new('/tmp/dj.log')
|
22
22
|
ENV['RAILS_ENV'] = 'test'
|
23
23
|
|
24
|
+
Time.zone = EtOrbi.get_tzone(:local)
|
25
|
+
|
24
26
|
ActiveJob::Base.queue_adapter = :delayed_job
|
25
27
|
|
26
|
-
ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => '
|
28
|
+
ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => 'file::memory:?cache=shared'
|
27
29
|
ActiveRecord::Base.logger = Delayed::Worker.logger
|
28
30
|
ActiveRecord::Migration.verbose = false
|
29
31
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: delayed_cron_job
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pascal Zumkehr
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-09-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: delayed_job
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '4.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: fugit
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.5'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.5'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: bundler
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,15 +136,15 @@ files:
|
|
122
136
|
- ".rspec"
|
123
137
|
- ".travis.yml"
|
124
138
|
- Gemfile
|
125
|
-
- LICENSE
|
139
|
+
- LICENSE
|
126
140
|
- README.md
|
127
141
|
- Rakefile
|
128
142
|
- delayed_cron_job.gemspec
|
129
143
|
- lib/delayed_cron_job.rb
|
130
144
|
- lib/delayed_cron_job/active_job/enqueuing.rb
|
131
145
|
- lib/delayed_cron_job/active_job/queue_adapter.rb
|
146
|
+
- lib/delayed_cron_job/backend/active_record/railtie.rb
|
132
147
|
- lib/delayed_cron_job/backend/updatable_cron.rb
|
133
|
-
- lib/delayed_cron_job/cronline.rb
|
134
148
|
- lib/delayed_cron_job/plugin.rb
|
135
149
|
- lib/delayed_cron_job/version.rb
|
136
150
|
- lib/generators/delayed_job/cron_generator.rb
|
@@ -142,7 +156,7 @@ homepage: https://github.com/codez/delayed_cron_job
|
|
142
156
|
licenses:
|
143
157
|
- MIT
|
144
158
|
metadata: {}
|
145
|
-
post_install_message:
|
159
|
+
post_install_message:
|
146
160
|
rdoc_options: []
|
147
161
|
require_paths:
|
148
162
|
- lib
|
@@ -157,9 +171,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
157
171
|
- !ruby/object:Gem::Version
|
158
172
|
version: '0'
|
159
173
|
requirements: []
|
160
|
-
|
161
|
-
|
162
|
-
signing_key:
|
174
|
+
rubygems_version: 3.1.4
|
175
|
+
signing_key:
|
163
176
|
specification_version: 4
|
164
177
|
summary: An extension to Delayed::Job that allows you to set cron expressions for
|
165
178
|
your jobs to run regularly.
|
@@ -1,465 +0,0 @@
|
|
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
|