rufus-scheduler 3.4.2 → 3.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 +4 -4
- data/CHANGELOG.txt +7 -0
- data/CREDITS.txt +1 -0
- data/Makefile +4 -0
- data/README.md +3 -2
- data/lib/rufus/scheduler.rb +36 -12
- data/lib/rufus/scheduler/jobs.rb +1 -3
- data/lib/rufus/scheduler/util.rb +151 -216
- data/rufus-scheduler.gemspec +6 -4
- metadata +30 -32
- data/TODO.txt +0 -151
- data/fail.txt +0 -2
- data/fail18.txt +0 -12
- data/lib/rufus/scheduler/cronline.rb +0 -498
- data/log.txt +0 -285
- data/n.txt +0 -38
- data/pics.txt +0 -15
- data/sofia.md +0 -89
data/rufus-scheduler.gemspec
CHANGED
@@ -21,17 +21,19 @@ Job scheduler for Ruby (at, cron, in and every jobs). Not a replacement for cron
|
|
21
21
|
|
22
22
|
#s.files = `git ls-files`.split("\n")
|
23
23
|
s.files = Dir[
|
24
|
+
'README.{md,txt}',
|
25
|
+
'CHANGELOG.{md,txt}', 'CREDITS.{md,txt}', 'LICENSE.{md,txt}',
|
24
26
|
'Makefile',
|
25
27
|
'lib/**/*.rb', #'spec/**/*.rb', 'test/**/*.rb',
|
26
|
-
|
28
|
+
"#{s.name}.gemspec",
|
27
29
|
]
|
28
30
|
|
29
31
|
s.required_ruby_version = '>= 1.9'
|
30
32
|
|
31
|
-
s.add_runtime_dependency '
|
33
|
+
s.add_runtime_dependency 'fugit', '~> 1.1', '>= 1.1.1'
|
32
34
|
|
33
|
-
s.add_development_dependency 'rspec', '~> 3.
|
34
|
-
s.add_development_dependency 'chronic'
|
35
|
+
s.add_development_dependency 'rspec', '~> 3.7'
|
36
|
+
s.add_development_dependency 'chronic', '~> 0.10'
|
35
37
|
|
36
38
|
s.require_path = 'lib'
|
37
39
|
end
|
metadata
CHANGED
@@ -1,57 +1,63 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rufus-scheduler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Mettraux
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-05-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: fugit
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - ~>
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1.
|
19
|
+
version: '1.1'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.1.1
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
|
-
- - ~>
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.1'
|
30
|
+
- - ">="
|
25
31
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
32
|
+
version: 1.1.1
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: rspec
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
30
36
|
requirements:
|
31
|
-
- - ~>
|
37
|
+
- - "~>"
|
32
38
|
- !ruby/object:Gem::Version
|
33
|
-
version: '3.
|
39
|
+
version: '3.7'
|
34
40
|
type: :development
|
35
41
|
prerelease: false
|
36
42
|
version_requirements: !ruby/object:Gem::Requirement
|
37
43
|
requirements:
|
38
|
-
- - ~>
|
44
|
+
- - "~>"
|
39
45
|
- !ruby/object:Gem::Version
|
40
|
-
version: '3.
|
46
|
+
version: '3.7'
|
41
47
|
- !ruby/object:Gem::Dependency
|
42
48
|
name: chronic
|
43
49
|
requirement: !ruby/object:Gem::Requirement
|
44
50
|
requirements:
|
45
|
-
- -
|
51
|
+
- - "~>"
|
46
52
|
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
53
|
+
version: '0.10'
|
48
54
|
type: :development
|
49
55
|
prerelease: false
|
50
56
|
version_requirements: !ruby/object:Gem::Requirement
|
51
57
|
requirements:
|
52
|
-
- -
|
58
|
+
- - "~>"
|
53
59
|
- !ruby/object:Gem::Version
|
54
|
-
version: '0'
|
60
|
+
version: '0.10'
|
55
61
|
description: Job scheduler for Ruby (at, cron, in and every jobs). Not a replacement
|
56
62
|
for crond.
|
57
63
|
email:
|
@@ -60,26 +66,18 @@ executables: []
|
|
60
66
|
extensions: []
|
61
67
|
extra_rdoc_files: []
|
62
68
|
files:
|
69
|
+
- CHANGELOG.txt
|
70
|
+
- CREDITS.txt
|
71
|
+
- LICENSE.txt
|
63
72
|
- Makefile
|
64
|
-
-
|
73
|
+
- README.md
|
74
|
+
- lib/rufus-scheduler.rb
|
75
|
+
- lib/rufus/scheduler.rb
|
65
76
|
- lib/rufus/scheduler/job_array.rb
|
66
77
|
- lib/rufus/scheduler/jobs.rb
|
67
78
|
- lib/rufus/scheduler/locks.rb
|
68
79
|
- lib/rufus/scheduler/util.rb
|
69
|
-
- lib/rufus/scheduler.rb
|
70
|
-
- lib/rufus-scheduler.rb
|
71
80
|
- rufus-scheduler.gemspec
|
72
|
-
- CHANGELOG.txt
|
73
|
-
- CREDITS.txt
|
74
|
-
- fail.txt
|
75
|
-
- fail18.txt
|
76
|
-
- LICENSE.txt
|
77
|
-
- log.txt
|
78
|
-
- n.txt
|
79
|
-
- pics.txt
|
80
|
-
- TODO.txt
|
81
|
-
- README.md
|
82
|
-
- sofia.md
|
83
81
|
homepage: http://github.com/jmettraux/rufus-scheduler
|
84
82
|
licenses:
|
85
83
|
- MIT
|
@@ -90,17 +88,17 @@ require_paths:
|
|
90
88
|
- lib
|
91
89
|
required_ruby_version: !ruby/object:Gem::Requirement
|
92
90
|
requirements:
|
93
|
-
- -
|
91
|
+
- - ">="
|
94
92
|
- !ruby/object:Gem::Version
|
95
93
|
version: '1.9'
|
96
94
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
95
|
requirements:
|
98
|
-
- -
|
96
|
+
- - ">="
|
99
97
|
- !ruby/object:Gem::Version
|
100
98
|
version: '0'
|
101
99
|
requirements: []
|
102
100
|
rubyforge_project: rufus
|
103
|
-
rubygems_version: 2.
|
101
|
+
rubygems_version: 2.6.13
|
104
102
|
signing_key:
|
105
103
|
specification_version: 4
|
106
104
|
summary: job scheduler for Ruby (at, cron, in and every jobs)
|
data/TODO.txt
DELETED
@@ -1,151 +0,0 @@
|
|
1
|
-
|
2
|
-
[o] merge schedule_queue and unschedule_queue (and merge [un]schedule steps)
|
3
|
-
[x] OR stop using queue, since we've got the thread-safe JobArray
|
4
|
-
[x] if possible, drop the mutex in JobArray
|
5
|
-
NO, that mutex is necessary for Scheduler#jobs (on JRuby an co)...
|
6
|
-
[o] named mutexes
|
7
|
-
[o] drop the schedule queue, rely on the mutex in JobArray
|
8
|
-
[o] def jobs; (@jobs.to_a + running_jobs).uniq; end
|
9
|
-
[o] replace @unscheduled by @unscheduled_at
|
10
|
-
[o] make sure #jobs doesn't return unscheduled jobs
|
11
|
-
[o] job tags and find_by_tag(t) (as in rs 2.x)
|
12
|
-
[o] require tzinfo anyway (runtime dep)
|
13
|
-
[o] document frequency
|
14
|
-
[o] accept :frequency => '5s'
|
15
|
-
[o] timeout (as in rufus-scheduler 2.x)
|
16
|
-
[o] Rufus::Scheduler#running_jobs (as in rufus-scheduler 2.x)
|
17
|
-
[o] Rufus::Scheduler#terminate_all_jobs
|
18
|
-
[o] Rufus::Scheduler::Job#kill
|
19
|
-
[x] Rufus::Scheduler#kill_all_jobs
|
20
|
-
[o] Rufus::Scheduler#shutdown(:terminate or :kill (or nothing))
|
21
|
-
[o] RepeatJob #pause / #resume (think about discard past)
|
22
|
-
[o] Rufus::Scheduler.start_new (backward comp) (with deprec note?)
|
23
|
-
[o] pass job to scheduled block? What does rs 2.x do?
|
24
|
-
[o] :first[_in|_at] for RepeatJob
|
25
|
-
[o] :last[_in|_at] for RepeatJob
|
26
|
-
[o] :times for RepeatJob (how many recurrences)
|
27
|
-
[o] fix issue #39 (first_at parses as UTC)
|
28
|
-
[o] about issue #43, raise if cron/every job frequency < scheduler frequency
|
29
|
-
[o] unlock spec/parse_spec.rb:30 "parse datimes with timezones"
|
30
|
-
[o] some kind of Schedulable (Xyz#call(job, time))
|
31
|
-
[o] add Jruby and Rubinius to Travis
|
32
|
-
[o] make Job #first_at= / #last_at= automatically parse strings?
|
33
|
-
[o] bring in Kratob's spec about mutex vs timeout and adapt 3.0 to it,
|
34
|
-
https://github.com/jmettraux/rufus-scheduler/pull/67
|
35
|
-
[x] :unschedule_if => lambda { |job| ... }
|
36
|
-
[o] OR look at how it was done in rs 2.0.x, some return value?
|
37
|
-
no, pass the job as arg to the block, then let the block do job.unschedule
|
38
|
-
so, document schedule.every('10d') { |j| j.unschedule if x?() }
|
39
|
-
[x] remove the time in job.trigger(time)
|
40
|
-
[o] add spec for job queued then unscheduled
|
41
|
-
[o] add spec for Scheduler#shutdown and work threads
|
42
|
-
[o] at some point, bring back rbx19 to Travis
|
43
|
-
[o] move the parse/util part of scheduler.rb to util.rb
|
44
|
-
[o] rescue KillSignal in job thread loop to kill just the job
|
45
|
-
[o] add spec for raise if scheduling a job while scheduler is shutting down
|
46
|
-
[o] schedule_in(2.days.from_now) {}
|
47
|
-
at and in could understand each others time parameter, ftw...
|
48
|
-
use the new #parse_to_time? no
|
49
|
-
[o] do repeat jobs reschedule after timing out? yes
|
50
|
-
[o] schedule_interval('20s')?
|
51
|
-
[x] Scheduler#reschedule(job) (new copy of the job)
|
52
|
-
[x] #free_all_work_threads is missing an implementation
|
53
|
-
[x] rescue StandardError
|
54
|
-
:on_error => :crash[_scheduler]
|
55
|
-
:on_error => :ignore
|
56
|
-
:on_error => ...
|
57
|
-
[o] on_error: what about TimeoutError in that scheme?
|
58
|
-
TimeoutError goes to $stderr, like a normal error
|
59
|
-
[o] link to SO for support
|
60
|
-
- sublink to "how to report bugs effectively"
|
61
|
-
[o] link to #ruote for support
|
62
|
-
[x] lockblock? pass a block to teach the scheduler how to lock?
|
63
|
-
is not necessary, @scheduler = Scheduler.new if should_start?
|
64
|
-
the surrounding Ruby code checks
|
65
|
-
[o] introduce job "vars", as in
|
66
|
-
http://stackoverflow.com/questions/18202848/how-to-have-a-variable-that-will-available-to-particular-scheduled-task-whenever
|
67
|
-
or job['key'] Job #[] and #[]=, as with Thread #[] #[]=
|
68
|
-
job-local variables #keys #key?
|
69
|
-
[o] thread-safety for job-local variables?
|
70
|
-
[x] discard past? discard_past => true or => "1d"
|
71
|
-
default would be discard_past => "1m" or scheduler freq * 2 ?
|
72
|
-
jobs would adjust their next_time until it fits the window...
|
73
|
-
~~ discard past by default
|
74
|
-
[o] expanded block/schedulable (it's "callable")
|
75
|
-
```
|
76
|
-
scheduler.every '10m' do
|
77
|
-
def pre
|
78
|
-
return false if Backend.down?
|
79
|
-
# ...
|
80
|
-
end
|
81
|
-
def post
|
82
|
-
# ...
|
83
|
-
end
|
84
|
-
def trigger
|
85
|
-
puts "oh hai!"
|
86
|
-
end
|
87
|
-
end
|
88
|
-
```
|
89
|
-
or something like that...
|
90
|
-
...
|
91
|
-
OR accept a class (and instantiate it the first time)
|
92
|
-
```
|
93
|
-
scheduler.every '10m', Class.new do
|
94
|
-
def call(job, time)
|
95
|
-
# ...
|
96
|
-
end
|
97
|
-
end
|
98
|
-
```
|
99
|
-
the job contains the instance in its @callable
|
100
|
-
[x] add spec case for corner case in Job#trigger (overlap vs reschedule) !!!
|
101
|
-
[o] rethink job array vs job set for #scheduled?
|
102
|
-
[x] introduce common parent class for EveryJob and IntervalJob
|
103
|
-
[o] create spec/ at_job_spec.rb, repeat_job_spec.rb, cron_job_spec.rb, ...
|
104
|
-
[x] ensure EveryJob do not schedule in the past (it's already like that)
|
105
|
-
[o] CronLine#next_time should return a time with subseconds chopped off
|
106
|
-
[o] drop min work threads setting?
|
107
|
-
[o] thread pool something? Thread upper limit?
|
108
|
-
[o] Rufus::Scheduler.singleton, Rufus::Scheduler.s
|
109
|
-
[o] EveryJob#first_at= and IntervalJob#first_at= should alter @next_time
|
110
|
-
[o] scheduler.schedule duration/time/cron ... for at/in/cron
|
111
|
-
(not every, nor interval)
|
112
|
-
scheduler.repeat time/cron ... for every/cron
|
113
|
-
|
114
|
-
[o] :lockfile => x, timestamp, process_id, thread_id...
|
115
|
-
warning: have to clean up that file on exit... or does the scheduler
|
116
|
-
timestamps it?
|
117
|
-
[ ] develop lockfile timestamp thinggy
|
118
|
-
~ if the timestamp is too old (twice the default frequency?) then
|
119
|
-
lock [file] take over...
|
120
|
-
Is that really what we want all the time?
|
121
|
-
|
122
|
-
[ ] idea: :mutex => x and :skip_on_mutex => true ?
|
123
|
-
would prevent blocking/waiting for the mutex to get available
|
124
|
-
:mutex => [ "mutex_name", true ]
|
125
|
-
:mutex => [ [ "mutex_name", true ], [ "other_mutex_name", false ] ]
|
126
|
-
|
127
|
-
[ ] bring back EM (but only EM.defer ?) :defer => true (Job or Scheduler
|
128
|
-
or both option?)
|
129
|
-
|
130
|
-
[ ] prepare a daemon, trust daemon-kit for that
|
131
|
-
|
132
|
-
[ ] :if => lambda { |job, time| ... } why not?
|
133
|
-
:unless => lambda { ...
|
134
|
-
:block => lambda { ...
|
135
|
-
can help get the block themselves leaner
|
136
|
-
#
|
137
|
-
investigate guards for schedulables... def if_guard; ...; end
|
138
|
-
|
139
|
-
[ ] scheduler.every '10', Class.new do
|
140
|
-
def call(job, time)
|
141
|
-
# might fail...
|
142
|
-
end
|
143
|
-
def on_error(err, job)
|
144
|
-
# catches...
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
~~~
|
149
|
-
|
150
|
-
[ ] scheduler.at('chronic string', chronic_options...)
|
151
|
-
|
data/fail.txt
DELETED
data/fail18.txt
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
|
2
|
-
rspec ./spec/job_spec.rb:640 # Rufus::Scheduler::Job work time #mean_work_time gathers work times and computes the mean
|
3
|
-
rspec ./spec/schedule_at_spec.rb:60 # Rufus::Scheduler#at triggers a job
|
4
|
-
rspec ./spec/schedule_in_spec.rb:44 # Rufus::Scheduler#in removes the job after execution
|
5
|
-
rspec ./spec/scheduler_spec.rb:83 # Rufus::Scheduler a schedule method passes the job to its block when it triggers
|
6
|
-
rspec ./spec/scheduler_spec.rb:534 # Rufus::Scheduler#running_jobs(:tag/:tags => x) returns a list of running jobs filtered by tag
|
7
|
-
rspec ./spec/scheduler_spec.rb:601 # Rufus::Scheduler#occurrences(time0, time1) respects :times for repeat jobs
|
8
|
-
rspec ./spec/scheduler_spec.rb:1019 # Rufus::Scheduler#on_post_trigger is called right after a job triggers
|
9
|
-
|
10
|
-
|
11
|
-
determine_id specs are slower... much slower...
|
12
|
-
|
@@ -1,498 +0,0 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
class Rufus::Scheduler
|
4
|
-
|
5
|
-
#
|
6
|
-
# A 'cron line' is a line in the sense of a crontab
|
7
|
-
# (man 5 crontab) file line.
|
8
|
-
#
|
9
|
-
class CronLine
|
10
|
-
|
11
|
-
# The max number of years in the future or the past before giving up
|
12
|
-
# searching for #next_time or #previous_time respectively
|
13
|
-
#
|
14
|
-
NEXT_TIME_MAX_YEARS = 14
|
15
|
-
|
16
|
-
# The string used for creating this cronline instance.
|
17
|
-
#
|
18
|
-
attr_reader :original
|
19
|
-
attr_reader :original_timezone
|
20
|
-
|
21
|
-
attr_reader :seconds
|
22
|
-
attr_reader :minutes
|
23
|
-
attr_reader :hours
|
24
|
-
attr_reader :days
|
25
|
-
attr_reader :months
|
26
|
-
#attr_reader :monthdays # reader defined below
|
27
|
-
attr_reader :weekdays
|
28
|
-
attr_reader :timezone
|
29
|
-
|
30
|
-
def initialize(line)
|
31
|
-
|
32
|
-
fail ArgumentError.new(
|
33
|
-
"not a string: #{line.inspect}"
|
34
|
-
) unless line.is_a?(String)
|
35
|
-
|
36
|
-
@original = line
|
37
|
-
@original_timezone = nil
|
38
|
-
|
39
|
-
items = line.split
|
40
|
-
|
41
|
-
if @timezone = EoTime.get_tzone(items.last)
|
42
|
-
@original_timezone = items.pop
|
43
|
-
else
|
44
|
-
@timezone = EoTime.local_tzone
|
45
|
-
end
|
46
|
-
|
47
|
-
fail ArgumentError.new(
|
48
|
-
"not a valid cronline : '#{line}'"
|
49
|
-
) unless items.length == 5 or items.length == 6
|
50
|
-
|
51
|
-
offset = items.length - 5
|
52
|
-
|
53
|
-
@seconds = offset == 1 ? parse_item(items[0], 0, 59) : [ 0 ]
|
54
|
-
@minutes = parse_item(items[0 + offset], 0, 59)
|
55
|
-
@hours = parse_item(items[1 + offset], 0, 24)
|
56
|
-
@days = parse_item(items[2 + offset], -30, 31)
|
57
|
-
@months = parse_item(items[3 + offset], 1, 12)
|
58
|
-
@weekdays, @monthdays = parse_weekdays(items[4 + offset])
|
59
|
-
|
60
|
-
[ @seconds, @minutes, @hours, @months ].each do |es|
|
61
|
-
|
62
|
-
fail ArgumentError.new(
|
63
|
-
"invalid cronline: '#{line}'"
|
64
|
-
) if es && es.find { |e| ! e.is_a?(Integer) }
|
65
|
-
end
|
66
|
-
|
67
|
-
if @days && @days.include?(0) # gh-221
|
68
|
-
|
69
|
-
fail ArgumentError.new('invalid day 0 in cronline')
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
# Returns true if the given time matches this cron line.
|
74
|
-
#
|
75
|
-
def matches?(time)
|
76
|
-
|
77
|
-
# FIXME Don't create a new EoTime if time is already a EoTime in same
|
78
|
-
# zone ...
|
79
|
-
# Wait, this seems only used in specs...
|
80
|
-
t = EoTime.new(time.to_f, @timezone)
|
81
|
-
|
82
|
-
return false unless sub_match?(t, :sec, @seconds)
|
83
|
-
return false unless sub_match?(t, :min, @minutes)
|
84
|
-
return false unless sub_match?(t, :hour, @hours)
|
85
|
-
return false unless date_match?(t)
|
86
|
-
true
|
87
|
-
end
|
88
|
-
|
89
|
-
# Returns the next time that this cron line is supposed to 'fire'
|
90
|
-
#
|
91
|
-
# This is raw, 3 secs to iterate over 1 year on my macbook :( brutal.
|
92
|
-
# (Well, I was wrong, takes 0.001 sec on 1.8.7 and 1.9.1)
|
93
|
-
#
|
94
|
-
# This method accepts an optional Time parameter. It's the starting point
|
95
|
-
# for the 'search'. By default, it's Time.now
|
96
|
-
#
|
97
|
-
# Note that the time instance returned will be in the same time zone that
|
98
|
-
# the given start point Time (thus a result in the local time zone will
|
99
|
-
# be passed if no start time is specified (search start time set to
|
100
|
-
# Time.now))
|
101
|
-
#
|
102
|
-
# Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
|
103
|
-
# Time.mktime(2008, 10, 24, 7, 29))
|
104
|
-
# #=> Fri Oct 24 07:30:00 -0500 2008
|
105
|
-
#
|
106
|
-
# Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
|
107
|
-
# Time.utc(2008, 10, 24, 7, 29))
|
108
|
-
# #=> Fri Oct 24 07:30:00 UTC 2008
|
109
|
-
#
|
110
|
-
# Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
|
111
|
-
# Time.utc(2008, 10, 24, 7, 29)).localtime
|
112
|
-
# #=> Fri Oct 24 02:30:00 -0500 2008
|
113
|
-
#
|
114
|
-
# (Thanks to K Liu for the note and the examples)
|
115
|
-
#
|
116
|
-
def next_time(from=EoTime.now)
|
117
|
-
|
118
|
-
nt = nil
|
119
|
-
zt = EoTime.new(from.to_i + 1, @timezone)
|
120
|
-
maxy = from.year + NEXT_TIME_MAX_YEARS
|
121
|
-
|
122
|
-
loop do
|
123
|
-
|
124
|
-
nt = zt.dup
|
125
|
-
|
126
|
-
fail RangeError.new(
|
127
|
-
"failed to reach occurrence within " +
|
128
|
-
"#{NEXT_TIME_MAX_YEARS} years for '#{original}'"
|
129
|
-
) if nt.year > maxy
|
130
|
-
|
131
|
-
unless date_match?(nt)
|
132
|
-
zt.add((24 - nt.hour) * 3600 - nt.min * 60 - nt.sec)
|
133
|
-
next
|
134
|
-
end
|
135
|
-
unless sub_match?(nt, :hour, @hours)
|
136
|
-
zt.add((60 - nt.min) * 60 - nt.sec)
|
137
|
-
next
|
138
|
-
end
|
139
|
-
unless sub_match?(nt, :min, @minutes)
|
140
|
-
zt.add(60 - nt.sec)
|
141
|
-
next
|
142
|
-
end
|
143
|
-
unless sub_match?(nt, :sec, @seconds)
|
144
|
-
zt.add(next_second(nt))
|
145
|
-
next
|
146
|
-
end
|
147
|
-
|
148
|
-
break
|
149
|
-
end
|
150
|
-
|
151
|
-
nt
|
152
|
-
end
|
153
|
-
|
154
|
-
# Returns the previous time the cronline matched. It's like next_time, but
|
155
|
-
# for the past.
|
156
|
-
#
|
157
|
-
def previous_time(from=EoTime.now)
|
158
|
-
|
159
|
-
pt = nil
|
160
|
-
zt = EoTime.new(from.to_i - 1, @timezone)
|
161
|
-
miny = from.year - NEXT_TIME_MAX_YEARS
|
162
|
-
|
163
|
-
loop do
|
164
|
-
|
165
|
-
pt = zt.dup
|
166
|
-
|
167
|
-
fail RangeError.new(
|
168
|
-
"failed to reach occurrence within " +
|
169
|
-
"#{NEXT_TIME_MAX_YEARS} years for '#{original}'"
|
170
|
-
) if pt.year < miny
|
171
|
-
|
172
|
-
unless date_match?(pt)
|
173
|
-
zt.subtract(pt.hour * 3600 + pt.min * 60 + pt.sec + 1)
|
174
|
-
next
|
175
|
-
end
|
176
|
-
unless sub_match?(pt, :hour, @hours)
|
177
|
-
zt.subtract(pt.min * 60 + pt.sec + 1)
|
178
|
-
next
|
179
|
-
end
|
180
|
-
unless sub_match?(pt, :min, @minutes)
|
181
|
-
zt.subtract(pt.sec + 1)
|
182
|
-
next
|
183
|
-
end
|
184
|
-
unless sub_match?(pt, :sec, @seconds)
|
185
|
-
zt.subtract(prev_second(pt))
|
186
|
-
next
|
187
|
-
end
|
188
|
-
|
189
|
-
break
|
190
|
-
end
|
191
|
-
|
192
|
-
pt
|
193
|
-
end
|
194
|
-
|
195
|
-
# Returns an array of 6 arrays (seconds, minutes, hours, days,
|
196
|
-
# months, weekdays).
|
197
|
-
# This method is mostly used by the cronline specs.
|
198
|
-
#
|
199
|
-
def to_a
|
200
|
-
|
201
|
-
[
|
202
|
-
toa(@seconds),
|
203
|
-
toa(@minutes),
|
204
|
-
toa(@hours),
|
205
|
-
toa(@days),
|
206
|
-
toa(@months),
|
207
|
-
toa(@weekdays),
|
208
|
-
toa(@monthdays),
|
209
|
-
@timezone.name
|
210
|
-
]
|
211
|
-
end
|
212
|
-
alias to_array to_a
|
213
|
-
|
214
|
-
# Returns a quickly computed approximation of the frequency for this
|
215
|
-
# cron line.
|
216
|
-
#
|
217
|
-
# #brute_frequency, on the other hand, will compute the frequency by
|
218
|
-
# examining a whole year, that can take more than seconds for a seconds
|
219
|
-
# level cron...
|
220
|
-
#
|
221
|
-
def frequency
|
222
|
-
|
223
|
-
return brute_frequency unless @seconds && @seconds.length > 1
|
224
|
-
|
225
|
-
secs = toa(@seconds)
|
226
|
-
|
227
|
-
secs[1..-1].inject([ secs[0], 60 ]) { |(prev, delta), sec|
|
228
|
-
d = sec - prev
|
229
|
-
[ sec, d < delta ? d : delta ]
|
230
|
-
}[1]
|
231
|
-
end
|
232
|
-
|
233
|
-
# Caching facility. Currently only used for brute frequencies.
|
234
|
-
#
|
235
|
-
@cache = {}; class << self; attr_reader :cache; end
|
236
|
-
|
237
|
-
# Returns the shortest delta between two potential occurrences of the
|
238
|
-
# schedule described by this cronline.
|
239
|
-
#
|
240
|
-
# .
|
241
|
-
#
|
242
|
-
# For a simple cronline like "*/5 * * * *", obviously the frequency is
|
243
|
-
# five minutes. Why does this method look at a whole year of #next_time ?
|
244
|
-
#
|
245
|
-
# Consider "* * * * sun#2,sun#3", the computed frequency is 1 week
|
246
|
-
# (the shortest delta is the one between the second sunday and the third
|
247
|
-
# sunday). This method takes no chance and runs next_time for the span
|
248
|
-
# of a whole year and keeps the shortest.
|
249
|
-
#
|
250
|
-
# Of course, this method can get VERY slow if you call on it a second-
|
251
|
-
# based cronline...
|
252
|
-
#
|
253
|
-
def brute_frequency
|
254
|
-
|
255
|
-
key = "brute_frequency:#{@original}"
|
256
|
-
|
257
|
-
delta = self.class.cache[key]
|
258
|
-
return delta if delta
|
259
|
-
|
260
|
-
delta = 366 * DAY_S
|
261
|
-
|
262
|
-
t0 = previous_time(Time.local(2000, 1, 1))
|
263
|
-
|
264
|
-
loop do
|
265
|
-
|
266
|
-
break if delta <= 1
|
267
|
-
break if delta <= 60 && @seconds && @seconds.size == 1
|
268
|
-
|
269
|
-
#st = Time.now
|
270
|
-
t1 = next_time(t0)
|
271
|
-
#p Time.now - st
|
272
|
-
d = t1 - t0
|
273
|
-
delta = d if d < delta
|
274
|
-
break if @months.nil? && t1.month == 2
|
275
|
-
break if @months.nil? && @days.nil? && t1.day == 2
|
276
|
-
break if @months.nil? && @days.nil? && @hours.nil? && t1.hour == 1
|
277
|
-
break if @months.nil? && @days.nil? && @hours.nil? && @minutes.nil? && t1.min == 1
|
278
|
-
break if t1.year >= 2001
|
279
|
-
|
280
|
-
t0 = t1
|
281
|
-
end
|
282
|
-
|
283
|
-
self.class.cache[key] = delta
|
284
|
-
end
|
285
|
-
|
286
|
-
def next_second(time)
|
287
|
-
|
288
|
-
secs = toa(@seconds)
|
289
|
-
|
290
|
-
return secs.first + 60 - time.sec if time.sec > secs.last
|
291
|
-
|
292
|
-
secs.shift while secs.first < time.sec
|
293
|
-
|
294
|
-
secs.first - time.sec
|
295
|
-
end
|
296
|
-
|
297
|
-
def prev_second(time)
|
298
|
-
|
299
|
-
secs = toa(@seconds)
|
300
|
-
|
301
|
-
return time.sec + 60 - secs.last if time.sec < secs.first
|
302
|
-
|
303
|
-
secs.pop while time.sec < secs.last
|
304
|
-
|
305
|
-
time.sec - secs.last
|
306
|
-
end
|
307
|
-
|
308
|
-
protected
|
309
|
-
|
310
|
-
def sc_sort(a)
|
311
|
-
|
312
|
-
a.sort_by { |e| e.is_a?(String) ? 61 : e.to_i }
|
313
|
-
end
|
314
|
-
|
315
|
-
if RUBY_VERSION >= '1.9'
|
316
|
-
def toa(item)
|
317
|
-
item == nil ? nil : item.to_a
|
318
|
-
end
|
319
|
-
else
|
320
|
-
def toa(item)
|
321
|
-
item.is_a?(Set) ? sc_sort(item.to_a) : item
|
322
|
-
end
|
323
|
-
end
|
324
|
-
|
325
|
-
WEEKDAYS = %w[ sun mon tue wed thu fri sat ]
|
326
|
-
DAY_S = 24 * 3600
|
327
|
-
|
328
|
-
def parse_weekdays(item)
|
329
|
-
|
330
|
-
return nil if item == '*'
|
331
|
-
|
332
|
-
weekdays = nil
|
333
|
-
monthdays = nil
|
334
|
-
|
335
|
-
item.downcase.split(',').each do |it|
|
336
|
-
|
337
|
-
WEEKDAYS.each_with_index { |a, i| it.gsub!(/#{a}/, i.to_s) }
|
338
|
-
|
339
|
-
it = it.gsub(/([^#])l/, '\1#-1')
|
340
|
-
# "5L" == "5#-1" == the last Friday
|
341
|
-
|
342
|
-
if m = it.match(/\A(.+)#(l|-?[12345])\z/)
|
343
|
-
|
344
|
-
fail ArgumentError.new(
|
345
|
-
"ranges are not supported for monthdays (#{it})"
|
346
|
-
) if m[1].index('-')
|
347
|
-
|
348
|
-
it = it.gsub(/#l/, '#-1')
|
349
|
-
|
350
|
-
(monthdays ||= []) << it
|
351
|
-
|
352
|
-
else
|
353
|
-
|
354
|
-
fail ArgumentError.new(
|
355
|
-
"invalid weekday expression (#{item})"
|
356
|
-
) if it !~ /\A0*[0-7](-0*[0-7])?\z/
|
357
|
-
|
358
|
-
its = it.index('-') ? parse_range(it, 0, 7) : [ Integer(it) ]
|
359
|
-
its = its.collect { |i| i == 7 ? 0 : i }
|
360
|
-
|
361
|
-
(weekdays ||= []).concat(its)
|
362
|
-
end
|
363
|
-
end
|
364
|
-
|
365
|
-
weekdays = weekdays.uniq.sort if weekdays
|
366
|
-
|
367
|
-
[ weekdays, monthdays ]
|
368
|
-
end
|
369
|
-
|
370
|
-
def parse_item(item, min, max)
|
371
|
-
|
372
|
-
return nil if item == '*'
|
373
|
-
|
374
|
-
r = item.split(',').map { |i| parse_range(i.strip, min, max) }.flatten
|
375
|
-
|
376
|
-
fail ArgumentError.new(
|
377
|
-
"found duplicates in #{item.inspect}"
|
378
|
-
) if r.uniq.size < r.size
|
379
|
-
|
380
|
-
r = sc_sort(r)
|
381
|
-
|
382
|
-
Set.new(r)
|
383
|
-
end
|
384
|
-
|
385
|
-
RANGE_REGEX = /\A(\*|-?\d{1,2})(?:-(-?\d{1,2}))?(?:\/(\d{1,2}))?\z/
|
386
|
-
|
387
|
-
def parse_range(item, min, max)
|
388
|
-
|
389
|
-
return %w[ L ] if item == 'L'
|
390
|
-
|
391
|
-
item = '*' + item if item[0, 1] == '/'
|
392
|
-
|
393
|
-
m = item.match(RANGE_REGEX)
|
394
|
-
|
395
|
-
fail ArgumentError.new(
|
396
|
-
"cannot parse #{item.inspect}"
|
397
|
-
) unless m
|
398
|
-
|
399
|
-
mmin = min == -30 ? 1 : min # days
|
400
|
-
|
401
|
-
sta = m[1]
|
402
|
-
sta = sta == '*' ? mmin : sta.to_i
|
403
|
-
|
404
|
-
edn = m[2]
|
405
|
-
edn = edn ? edn.to_i : sta
|
406
|
-
edn = max if m[1] == '*'
|
407
|
-
|
408
|
-
inc = m[3]
|
409
|
-
inc = inc ? inc.to_i : 1
|
410
|
-
|
411
|
-
fail ArgumentError.new(
|
412
|
-
"#{item.inspect} positive/negative ranges not allowed"
|
413
|
-
) if (sta < 0 && edn > 0) || (sta > 0 && edn < 0)
|
414
|
-
|
415
|
-
fail ArgumentError.new(
|
416
|
-
"#{item.inspect} descending day ranges not allowed"
|
417
|
-
) if min == -30 && sta > edn
|
418
|
-
|
419
|
-
fail ArgumentError.new(
|
420
|
-
"#{item.inspect} is not in range #{min}..#{max}"
|
421
|
-
) if sta < min || edn > max
|
422
|
-
|
423
|
-
fail ArgumentError.new(
|
424
|
-
"#{item.inspect} increment must be greater than zero"
|
425
|
-
) if inc == 0
|
426
|
-
|
427
|
-
r = []
|
428
|
-
val = sta
|
429
|
-
|
430
|
-
loop do
|
431
|
-
v = val
|
432
|
-
v = 0 if max == 24 && v == 24 # hours
|
433
|
-
r << v
|
434
|
-
break if inc == 1 && val == edn
|
435
|
-
val += inc
|
436
|
-
break if inc > 1 && val > edn
|
437
|
-
val = min if val > max
|
438
|
-
end
|
439
|
-
|
440
|
-
r.uniq
|
441
|
-
end
|
442
|
-
|
443
|
-
# FIXME: Eventually split into day_match?, hour_match? and monthdays_match?o
|
444
|
-
#
|
445
|
-
def sub_match?(time, accessor, values)
|
446
|
-
|
447
|
-
return true if values.nil?
|
448
|
-
|
449
|
-
value = time.send(accessor)
|
450
|
-
|
451
|
-
if accessor == :day
|
452
|
-
|
453
|
-
values.each do |v|
|
454
|
-
return true if v == 'L' && (time + DAY_S).day == 1
|
455
|
-
return true if v.to_i < 0 && (time + (1 - v) * DAY_S).day == 1
|
456
|
-
end
|
457
|
-
end
|
458
|
-
|
459
|
-
if accessor == :hour
|
460
|
-
|
461
|
-
return true if value == 0 && values.include?(24)
|
462
|
-
end
|
463
|
-
|
464
|
-
if accessor == :monthdays
|
465
|
-
|
466
|
-
return true if (values & value).any?
|
467
|
-
end
|
468
|
-
|
469
|
-
values.include?(value)
|
470
|
-
end
|
471
|
-
|
472
|
-
# def monthday_match?(zt, values)
|
473
|
-
#
|
474
|
-
# return true if values.nil?
|
475
|
-
#
|
476
|
-
# today_values = monthdays(zt)
|
477
|
-
#
|
478
|
-
# (today_values & values).any?
|
479
|
-
# end
|
480
|
-
|
481
|
-
def date_match?(zt)
|
482
|
-
|
483
|
-
return false unless sub_match?(zt, :day, @days)
|
484
|
-
return false unless sub_match?(zt, :month, @months)
|
485
|
-
|
486
|
-
return true if (
|
487
|
-
(@weekdays && @monthdays) &&
|
488
|
-
(sub_match?(zt, :wday, @weekdays) ||
|
489
|
-
sub_match?(zt, :monthdays, @monthdays)))
|
490
|
-
|
491
|
-
return false unless sub_match?(zt, :wday, @weekdays)
|
492
|
-
return false unless sub_match?(zt, :monthdays, @monthdays)
|
493
|
-
|
494
|
-
true
|
495
|
-
end
|
496
|
-
end
|
497
|
-
end
|
498
|
-
|