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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: ef66952c36796b2dfa66c343a31631d061d09bd1
4
- data.tar.gz: fba9ad110c34d1e475bc8057d818b9d0c0fa02d4
2
+ SHA256:
3
+ metadata.gz: 06776f6ef3c8e55b7e737ecd6e2eb0739c8185c72f108bd6bde3ec59aa44780e
4
+ data.tar.gz: 50af71b765e892796d0b04866e2a99558664f0c236ebaca532b3d15355843eee
5
5
  SHA512:
6
- metadata.gz: da230b6a7fef72f96129ff81f32f8e1345b025a6f0b54cc6bcc482d44a11e823480a2f3a0715c46687a98226d8f9c7e210ccf3f6892b08723ce6f78fd884e498
7
- data.tar.gz: 150cd81fe161e5cd030a99924345902e47788877c861db22975ad6db80aa9eeb593e09abbd11a67c35054eb13603eddcb8313b85f4698368e78e879e0e1229ec
6
+ metadata.gz: efffba2bfd9c55a156e649fa70adb9150717f1ed41831b8031791155054d6740c42119f82f57224323c47908d24a50621698bf872157ebb71a3bc1c4d3d2c61d
7
+ data.tar.gz: 4869d5b81626d2b29e8037cc371ce02deb846981f6bdf26a463fa893c60dc7e6577bc24312dc96056f38d3aac522280a0f56672b5642f9aea80ce4f26f22358b
data/.gitignore CHANGED
@@ -20,4 +20,5 @@ tmp
20
20
  *.o
21
21
  *.a
22
22
  mkmf.log
23
- .project
23
+ .project
24
+ file::memory:*
data/.travis.yml CHANGED
@@ -1,7 +1,9 @@
1
1
  language: ruby
2
2
 
3
3
  rvm:
4
- - "2.3.0"
4
+ - "2.5.8"
5
+ - "2.7.2"
6
+ - "3.0.0"
5
7
 
6
8
  sudo: false
7
9
 
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014 Pascal Zumkehr
1
+ Copyright (c) 2014-2017 Pascal Zumkehr
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # Delayed::Cron::Job
2
2
 
3
+ [![Build Status](https://app.travis-ci.com/codez/delayed_cron_job.svg?branch=master)](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 this line to your application's Gemfile:
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
- Delayed::Job.enqueue(MyRepeatedJob.new, cron: '15 */6 * * 1-5')
31
+ ```ruby
32
+ Delayed::Job.enqueue(MyRepeatedJob.new, cron: '15 */6 * * 1-5')
33
+ ```
30
34
 
31
- Or, when using Active Job:
35
+ Or, when using ActiveJob:
32
36
 
33
- MyJob.set(cron: '*/5 * * * *').perform_later
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
- The credits for the `Cronline` class used go to
37
- [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler).
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
- ## Details
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.
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.require_paths = ["lib"]
23
23
 
24
24
  spec.add_dependency "delayed_job", ">= 4.1"
25
+ spec.add_dependency "fugit", ">= 1.5"
25
26
 
26
27
  spec.add_development_dependency "bundler"
27
28
  spec.add_development_dependency "rake"
@@ -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
- self.run_at = Cronline.new(cron).next_time(Delayed::Job.db_time_now)
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
- # Update the cron expression from the database in case it was updated.
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
- next_job = job.dup
45
- next_job.id = job.id
46
- next_job.created_at = job.created_at
47
- next_job.locked_at = nil
48
- next_job.locked_by = nil
49
- next_job.attempts += 1
50
- next_job.save!
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
@@ -1,3 +1,3 @@
1
1
  module DelayedCronJob
2
- VERSION = '0.7.1'
2
+ VERSION = '0.8.0'
3
3
  end
@@ -1,10 +1,18 @@
1
1
  require 'delayed_job'
2
2
  require 'English'
3
- require 'delayed_cron_job/cronline'
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
- Delayed::Backend::ActiveRecord::Job.send(:include, DelayedCronJob::Backend::UpdatableCron)
20
- if Delayed::Backend::ActiveRecord::Job.respond_to?(:attr_accessible)
21
- Delayed::Backend::ActiveRecord::Job.attr_accessible(:cron)
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', 'db/migrate/add_cron_to_delayed_jobs.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
@@ -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.utc(run.year, run.month, run.day, 1, 5)
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.utc(run.year, run.month, run.day, 1, 5)
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("execution expired")
87
+ expect(j.last_error).to match('execution expired')
78
88
  end
79
89
 
80
- it 'schedules new job after deserialization error' do
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(1)
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
- if Time.now != now
113
- job = Delayed::Job.enqueue(handler, cron: '* * * * *')
114
- run = now.hour == 23 && now.min == 59 ? now + 1.day : now
115
- hour = now.min == 59 ? (now.hour + 1) % 24 : now.hour
116
- run_at = Time.utc(run.year, run.month, run.day, hour, (now.min + 1) % 60)
117
- expect(job.run_at).to eq(run_at)
118
- else
119
- pending "This test only makes sense in non-UTC time zone"
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 => ':memory:'
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.7.1
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: 2016-05-26 00:00:00.000000000 Z
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.txt
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
- rubyforge_project:
161
- rubygems_version: 2.4.8
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