rufus-scheduler 2.0.24 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.txt +6 -0
- data/CREDITS.txt +4 -0
- data/README.md +1064 -0
- data/Rakefile +1 -4
- data/TODO.txt +145 -55
- data/lib/rufus/scheduler.rb +502 -26
- data/lib/rufus/{sc → scheduler}/cronline.rb +46 -17
- data/lib/rufus/{sc/version.rb → scheduler/job_array.rb} +56 -4
- data/lib/rufus/scheduler/jobs.rb +548 -0
- data/lib/rufus/scheduler/util.rb +318 -0
- data/rufus-scheduler.gemspec +30 -4
- data/spec/cronline_spec.rb +29 -8
- data/spec/error_spec.rb +116 -0
- data/spec/job_array_spec.rb +39 -0
- data/spec/job_at_spec.rb +58 -0
- data/spec/job_cron_spec.rb +67 -0
- data/spec/job_every_spec.rb +71 -0
- data/spec/job_in_spec.rb +20 -0
- data/spec/job_interval_spec.rb +68 -0
- data/spec/job_repeat_spec.rb +308 -0
- data/spec/job_spec.rb +387 -115
- data/spec/lockfile_spec.rb +61 -0
- data/spec/parse_spec.rb +203 -0
- data/spec/schedule_at_spec.rb +129 -0
- data/spec/schedule_cron_spec.rb +66 -0
- data/spec/schedule_every_spec.rb +109 -0
- data/spec/schedule_in_spec.rb +80 -0
- data/spec/schedule_interval_spec.rb +128 -0
- data/spec/scheduler_spec.rb +831 -124
- data/spec/spec_helper.rb +65 -0
- data/spec/threads_spec.rb +75 -0
- metadata +64 -59
- data/README.rdoc +0 -661
- data/lib/rufus/otime.rb +0 -3
- data/lib/rufus/sc/jobqueues.rb +0 -160
- data/lib/rufus/sc/jobs.rb +0 -471
- data/lib/rufus/sc/rtime.rb +0 -363
- data/lib/rufus/sc/scheduler.rb +0 -636
- data/spec/at_in_spec.rb +0 -47
- data/spec/at_spec.rb +0 -125
- data/spec/blocking_spec.rb +0 -64
- data/spec/cron_spec.rb +0 -134
- data/spec/every_spec.rb +0 -304
- data/spec/exception_spec.rb +0 -113
- data/spec/in_spec.rb +0 -150
- data/spec/mutex_spec.rb +0 -159
- data/spec/rtime_spec.rb +0 -137
- data/spec/schedulable_spec.rb +0 -97
- data/spec/spec_base.rb +0 -87
- data/spec/stress_schedule_unschedule_spec.rb +0 -159
- data/spec/timeout_spec.rb +0 -148
- data/test/kjw.rb +0 -113
- data/test/t.rb +0 -20
data/Rakefile
CHANGED
@@ -1,8 +1,5 @@
|
|
1
1
|
|
2
|
-
$:.unshift('.') # 1.9.2
|
3
|
-
|
4
2
|
require 'rubygems'
|
5
|
-
require 'rubygems/user_interaction' if Gem::RubyGemsVersion == '1.5.0'
|
6
3
|
|
7
4
|
require 'rake'
|
8
5
|
require 'rake/clean'
|
@@ -42,7 +39,7 @@ desc %{
|
|
42
39
|
task :build do
|
43
40
|
|
44
41
|
sh "gem build #{GEMSPEC_FILE}"
|
45
|
-
sh "mkdir pkg"
|
42
|
+
sh "mkdir -p pkg"
|
46
43
|
sh "mv #{GEMSPEC.name}-#{GEMSPEC.version}.gem pkg/"
|
47
44
|
end
|
48
45
|
|
data/TODO.txt
CHANGED
@@ -1,57 +1,147 @@
|
|
1
1
|
|
2
|
-
[o]
|
3
|
-
[
|
4
|
-
|
5
|
-
|
6
|
-
[o]
|
7
|
-
|
8
|
-
[o]
|
9
|
-
|
10
|
-
[o]
|
11
|
-
[o]
|
12
|
-
|
13
|
-
[o]
|
14
|
-
|
15
|
-
[
|
16
|
-
|
17
|
-
[o]
|
18
|
-
[o]
|
19
|
-
|
20
|
-
[
|
21
|
-
|
22
|
-
[o]
|
23
|
-
[o]
|
24
|
-
|
25
|
-
[o]
|
26
|
-
|
27
|
-
|
28
|
-
[o]
|
29
|
-
|
30
|
-
[o]
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
[
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
[
|
40
|
-
|
41
|
-
[o]
|
42
|
-
[o]
|
43
|
-
|
44
|
-
[o]
|
45
|
-
|
46
|
-
[o]
|
47
|
-
|
48
|
-
|
49
|
-
[o]
|
50
|
-
[o]
|
51
|
-
|
52
|
-
[
|
53
|
-
[
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
57
147
|
|
data/lib/rufus/scheduler.rb
CHANGED
@@ -22,41 +22,517 @@
|
|
22
22
|
# Made in Japan.
|
23
23
|
#++
|
24
24
|
|
25
|
+
require 'date' if RUBY_VERSION < '1.9.0'
|
26
|
+
require 'time'
|
27
|
+
require 'thread'
|
28
|
+
require 'tzinfo'
|
29
|
+
require 'fileutils'
|
25
30
|
|
26
|
-
require 'rufus/sc/scheduler'
|
27
31
|
|
32
|
+
module Rufus
|
28
33
|
|
29
|
-
|
34
|
+
class Scheduler
|
30
35
|
|
31
|
-
|
32
|
-
|
33
|
-
|
36
|
+
require 'rufus/scheduler/util'
|
37
|
+
require 'rufus/scheduler/jobs'
|
38
|
+
require 'rufus/scheduler/cronline'
|
39
|
+
require 'rufus/scheduler/job_array'
|
34
40
|
|
35
|
-
|
36
|
-
|
41
|
+
VERSION = '3.0.0'
|
42
|
+
|
43
|
+
#
|
44
|
+
# This error is thrown when the :timeout attribute triggers
|
45
|
+
#
|
46
|
+
class TimeoutError < StandardError; end
|
47
|
+
|
48
|
+
#MIN_WORK_THREADS = 7
|
49
|
+
MAX_WORK_THREADS = 35
|
50
|
+
|
51
|
+
attr_accessor :frequency
|
52
|
+
attr_reader :started_at
|
53
|
+
attr_reader :thread
|
54
|
+
attr_reader :thread_key
|
55
|
+
attr_reader :mutexes
|
56
|
+
|
57
|
+
#attr_accessor :min_work_threads
|
58
|
+
attr_accessor :max_work_threads
|
59
|
+
|
60
|
+
attr_accessor :stderr
|
61
|
+
|
62
|
+
attr_reader :work_queue
|
63
|
+
|
64
|
+
def initialize(opts={})
|
65
|
+
|
66
|
+
@opts = opts
|
67
|
+
|
68
|
+
@started_at = nil
|
69
|
+
@paused = false
|
70
|
+
|
71
|
+
@jobs = JobArray.new
|
72
|
+
|
73
|
+
@frequency = Rufus::Scheduler.parse(opts[:frequency] || 0.300)
|
74
|
+
@mutexes = {}
|
75
|
+
|
76
|
+
@work_queue = Queue.new
|
37
77
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
78
|
+
#@min_work_threads = opts[:min_work_threads] || MIN_WORK_THREADS
|
79
|
+
@max_work_threads = opts[:max_work_threads] || MAX_WORK_THREADS
|
80
|
+
|
81
|
+
@stderr = $stderr
|
82
|
+
|
83
|
+
@thread_key = "rufus_scheduler_#{self.object_id}"
|
84
|
+
|
85
|
+
consider_lockfile || return
|
86
|
+
|
87
|
+
start
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns a singleton Rufus::Scheduler instance
|
91
|
+
#
|
92
|
+
def self.singleton(opts={})
|
93
|
+
|
94
|
+
@singleton ||= Rufus::Scheduler.new(opts)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Alias for Rufus::Scheduler.singleton
|
98
|
+
#
|
99
|
+
def self.s(opts={}); singleton(opts); end
|
100
|
+
|
101
|
+
# Releasing the gem would probably require redirecting .start_new to
|
102
|
+
# .new and emit a simple deprecation message.
|
103
|
+
#
|
104
|
+
# For now, let's assume the people pointing at rufus-scheduler/master
|
105
|
+
# on GitHub know what they do...
|
106
|
+
#
|
107
|
+
def self.start_new
|
108
|
+
|
109
|
+
fail "this is rufus-scheduler 3.0, use .new instead of .start_new"
|
110
|
+
end
|
111
|
+
|
112
|
+
def shutdown(opt=nil)
|
113
|
+
|
114
|
+
@started_at = nil
|
115
|
+
|
116
|
+
jobs.each { |j| j.unschedule }
|
117
|
+
|
118
|
+
@work_queue.clear
|
119
|
+
|
120
|
+
if opt == :wait
|
121
|
+
join_all_work_threads
|
122
|
+
elsif opt == :kill
|
123
|
+
kill_all_work_threads
|
124
|
+
end
|
125
|
+
|
126
|
+
@lockfile.flock(File::LOCK_UN) if @lockfile
|
127
|
+
end
|
128
|
+
|
129
|
+
alias stop shutdown
|
130
|
+
|
131
|
+
def uptime
|
132
|
+
|
133
|
+
@started_at ? Time.now - @started_at : nil
|
134
|
+
end
|
135
|
+
|
136
|
+
def uptime_s
|
137
|
+
|
138
|
+
self.class.to_duration(uptime)
|
139
|
+
end
|
140
|
+
|
141
|
+
def join
|
142
|
+
|
143
|
+
@thread.join
|
144
|
+
end
|
145
|
+
|
146
|
+
def paused?
|
147
|
+
|
148
|
+
@paused
|
149
|
+
end
|
150
|
+
|
151
|
+
def pause
|
152
|
+
|
153
|
+
@paused = true
|
154
|
+
end
|
155
|
+
|
156
|
+
def resume
|
157
|
+
|
158
|
+
@paused = false
|
52
159
|
end
|
53
|
-
end
|
54
160
|
|
55
|
-
|
56
|
-
|
57
|
-
|
161
|
+
#--
|
162
|
+
# scheduling methods
|
163
|
+
#++
|
164
|
+
|
165
|
+
def at(time, callable=nil, opts={}, &block)
|
166
|
+
|
167
|
+
do_schedule(:once, time, callable, opts, opts[:job], block)
|
168
|
+
end
|
169
|
+
|
170
|
+
def schedule_at(time, callable=nil, opts={}, &block)
|
171
|
+
|
172
|
+
do_schedule(:once, time, callable, opts, true, block)
|
173
|
+
end
|
174
|
+
|
175
|
+
def in(duration, callable=nil, opts={}, &block)
|
176
|
+
|
177
|
+
do_schedule(:once, duration, callable, opts, opts[:job], block)
|
178
|
+
end
|
179
|
+
|
180
|
+
def schedule_in(duration, callable=nil, opts={}, &block)
|
181
|
+
|
182
|
+
do_schedule(:once, duration, callable, opts, true, block)
|
183
|
+
end
|
184
|
+
|
185
|
+
def every(duration, callable=nil, opts={}, &block)
|
186
|
+
|
187
|
+
do_schedule(:every, duration, callable, opts, opts[:job], block)
|
188
|
+
end
|
189
|
+
|
190
|
+
def schedule_every(duration, callable=nil, opts={}, &block)
|
191
|
+
|
192
|
+
do_schedule(:every, duration, callable, opts, true, block)
|
193
|
+
end
|
194
|
+
|
195
|
+
def interval(duration, callable=nil, opts={}, &block)
|
196
|
+
|
197
|
+
do_schedule(:interval, duration, callable, opts, opts[:job], block)
|
198
|
+
end
|
199
|
+
|
200
|
+
def schedule_interval(duration, callable=nil, opts={}, &block)
|
201
|
+
|
202
|
+
do_schedule(:interval, duration, callable, opts, true, block)
|
203
|
+
end
|
204
|
+
|
205
|
+
def cron(cronline, callable=nil, opts={}, &block)
|
206
|
+
|
207
|
+
do_schedule(:cron, cronline, callable, opts, opts[:job], block)
|
208
|
+
end
|
209
|
+
|
210
|
+
def schedule_cron(cronline, callable=nil, opts={}, &block)
|
211
|
+
|
212
|
+
do_schedule(:cron, cronline, callable, opts, true, block)
|
213
|
+
end
|
214
|
+
|
215
|
+
def schedule(arg, callable=nil, opts={}, &block)
|
216
|
+
|
217
|
+
# TODO: eventually, spare one parse call
|
218
|
+
|
219
|
+
case Scheduler.parse(arg)
|
220
|
+
when CronLine then schedule_cron(arg, callable, opts, &block)
|
221
|
+
when Time then schedule_at(arg, callable, opts, &block)
|
222
|
+
else schedule_in(arg, callable, opts, &block)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def repeat(arg, callable=nil, opts={}, &block)
|
227
|
+
|
228
|
+
# TODO: eventually, spare one parse call
|
229
|
+
|
230
|
+
case Scheduler.parse(arg)
|
231
|
+
when CronLine then schedule_cron(arg, callable, opts, &block)
|
232
|
+
else schedule_every(arg, callable, opts, &block)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def unschedule(job_or_job_id)
|
237
|
+
|
238
|
+
job, job_id = fetch(job_or_job_id)
|
239
|
+
|
240
|
+
fail ArgumentError.new("no job found with id '#{job_id}'") unless job
|
241
|
+
|
242
|
+
job.unschedule if job
|
243
|
+
end
|
244
|
+
|
245
|
+
#--
|
246
|
+
# jobs methods
|
247
|
+
#++
|
248
|
+
|
249
|
+
# Returns all the scheduled jobs
|
250
|
+
# (even those right before re-schedule).
|
251
|
+
#
|
252
|
+
def jobs(opts={})
|
253
|
+
|
254
|
+
opts = { opts => true } if opts.is_a?(Symbol)
|
255
|
+
|
256
|
+
jobs = @jobs.to_a
|
257
|
+
|
258
|
+
if opts[:running]
|
259
|
+
jobs = jobs.select { |j| j.running? }
|
260
|
+
elsif ! opts[:all]
|
261
|
+
jobs = jobs.reject { |j| j.next_time.nil? || j.unscheduled_at }
|
262
|
+
end
|
263
|
+
|
264
|
+
tags = Array(opts[:tag] || opts[:tags]).collect { |t| t.to_s }
|
265
|
+
jobs = jobs.reject { |j| tags.find { |t| ! j.tags.include?(t) } }
|
266
|
+
|
267
|
+
jobs
|
268
|
+
end
|
58
269
|
|
59
|
-
|
270
|
+
def at_jobs(opts={})
|
271
|
+
|
272
|
+
jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::AtJob) }
|
273
|
+
end
|
274
|
+
|
275
|
+
def in_jobs(opts={})
|
276
|
+
|
277
|
+
jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::InJob) }
|
278
|
+
end
|
279
|
+
|
280
|
+
def every_jobs(opts={})
|
281
|
+
|
282
|
+
jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::EveryJob) }
|
283
|
+
end
|
284
|
+
|
285
|
+
def interval_jobs(opts={})
|
286
|
+
|
287
|
+
jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::IntervalJob) }
|
288
|
+
end
|
289
|
+
|
290
|
+
def cron_jobs(opts={})
|
291
|
+
|
292
|
+
jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::CronJob) }
|
293
|
+
end
|
294
|
+
|
295
|
+
def job(job_id)
|
296
|
+
|
297
|
+
@jobs[job_id]
|
298
|
+
end
|
299
|
+
|
300
|
+
# Returns true if this job is currently scheduled.
|
301
|
+
#
|
302
|
+
# Takes extra care to answer true if the job is a repeat job
|
303
|
+
# currently firing.
|
304
|
+
#
|
305
|
+
def scheduled?(job_or_job_id)
|
306
|
+
|
307
|
+
job, job_id = fetch(job_or_job_id)
|
308
|
+
|
309
|
+
!! (job && job.next_time != nil)
|
310
|
+
end
|
311
|
+
|
312
|
+
# Lists all the threads associated with this scheduler.
|
313
|
+
#
|
314
|
+
def threads
|
315
|
+
|
316
|
+
Thread.list.select { |t| t[thread_key] }
|
317
|
+
end
|
318
|
+
|
319
|
+
# Lists all the work threads (the ones actually running the scheduled
|
320
|
+
# block code)
|
321
|
+
#
|
322
|
+
# Accepts a query option, which can be set to:
|
323
|
+
# * :all (default), returns all the threads that are work threads
|
324
|
+
# or are currently running a job
|
325
|
+
# * :active, returns all threads that are currenly running a job
|
326
|
+
# * :vacant, returns the threads that are not running a job
|
327
|
+
#
|
328
|
+
# If, thanks to :blocking => true, a job is scheduled to monopolize the
|
329
|
+
# main scheduler thread, that thread will get returned when :active or
|
330
|
+
# :all.
|
331
|
+
#
|
332
|
+
def work_threads(query=:all)
|
333
|
+
|
334
|
+
ts =
|
335
|
+
threads.select { |t|
|
336
|
+
t[:rufus_scheduler_job] || t[:rufus_scheduler_work_thread]
|
337
|
+
}
|
338
|
+
|
339
|
+
case query
|
340
|
+
when :active then ts.select { |t| t[:rufus_scheduler_job] }
|
341
|
+
when :vacant then ts.reject { |t| t[:rufus_scheduler_job] }
|
342
|
+
else ts
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
def running_jobs(opts={})
|
347
|
+
|
348
|
+
jobs(opts.merge(:running => true))
|
349
|
+
end
|
350
|
+
|
351
|
+
def on_error(job, err)
|
352
|
+
|
353
|
+
pre = err.object_id.to_s
|
354
|
+
|
355
|
+
stderr.puts("{ #{pre} rufus-scheduler intercepted an error:")
|
356
|
+
stderr.puts(" #{pre} job:")
|
357
|
+
stderr.puts(" #{pre} #{job.class} #{job.original.inspect} #{job.opts.inspect}")
|
358
|
+
stderr.puts(" #{pre} error:")
|
359
|
+
stderr.puts(" #{pre} #{err.object_id}")
|
360
|
+
stderr.puts(" #{pre} #{err.class}")
|
361
|
+
stderr.puts(" #{pre} #{err}")
|
362
|
+
err.backtrace.each do |l|
|
363
|
+
stderr.puts(" #{pre} #{l}")
|
364
|
+
end
|
365
|
+
stderr.puts("} #{pre} .")
|
366
|
+
|
367
|
+
rescue => e
|
368
|
+
|
369
|
+
stderr.puts("failure in #on_error itself:")
|
370
|
+
stderr.puts(e.inspect)
|
371
|
+
stderr.puts(e.backtrace)
|
372
|
+
|
373
|
+
ensure
|
374
|
+
|
375
|
+
stderr.flush
|
376
|
+
end
|
377
|
+
|
378
|
+
protected
|
379
|
+
|
380
|
+
# Returns [ job, job_id ]
|
381
|
+
#
|
382
|
+
def fetch(job_or_job_id)
|
383
|
+
|
384
|
+
if job_or_job_id.respond_to?(:job_id)
|
385
|
+
[ job_or_job_id, job_or_job_id.job_id ]
|
386
|
+
else
|
387
|
+
[ job(job_or_job_id), job_or_job_id ]
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
def consider_lockfile
|
392
|
+
|
393
|
+
@lockfile = nil
|
394
|
+
|
395
|
+
return true unless f = @opts[:lockfile]
|
396
|
+
|
397
|
+
raise ArgumentError.new(
|
398
|
+
":lockfile argument must be a string, not a #{f.class}"
|
399
|
+
) unless f.is_a?(String)
|
400
|
+
|
401
|
+
FileUtils.mkdir_p(File.dirname(f))
|
402
|
+
|
403
|
+
f = File.new(f, File::RDWR | File::CREAT)
|
404
|
+
locked = f.flock(File::LOCK_NB | File::LOCK_EX)
|
405
|
+
|
406
|
+
return false unless locked
|
407
|
+
|
408
|
+
now = Time.now
|
409
|
+
|
410
|
+
f.print("pid: #{$$}, ")
|
411
|
+
f.print("scheduler.object_id: #{self.object_id}, ")
|
412
|
+
f.print("time: #{now}, ")
|
413
|
+
f.print("timestamp: #{now.to_f}")
|
414
|
+
f.flush
|
415
|
+
|
416
|
+
@lockfile = f
|
417
|
+
|
418
|
+
true
|
419
|
+
end
|
420
|
+
|
421
|
+
def terminate_all_jobs
|
422
|
+
|
423
|
+
jobs.each { |j| j.unschedule }
|
424
|
+
|
425
|
+
sleep 0.01 while running_jobs.size > 0
|
426
|
+
end
|
427
|
+
|
428
|
+
def join_all_work_threads
|
429
|
+
|
430
|
+
work_threads.size.times { @work_queue << :sayonara }
|
431
|
+
|
432
|
+
work_threads.each { |t| t.join }
|
433
|
+
|
434
|
+
@work_queue.clear
|
435
|
+
end
|
436
|
+
|
437
|
+
def kill_all_work_threads
|
438
|
+
|
439
|
+
work_threads.each { |t| t.kill }
|
440
|
+
end
|
441
|
+
|
442
|
+
#def free_all_work_threads
|
443
|
+
#
|
444
|
+
# work_threads.each { |t| t.raise(KillSignal) }
|
445
|
+
#end
|
446
|
+
|
447
|
+
def start
|
448
|
+
|
449
|
+
@started_at = Time.now
|
450
|
+
|
451
|
+
@thread =
|
452
|
+
Thread.new do
|
453
|
+
|
454
|
+
while @started_at do
|
455
|
+
|
456
|
+
unschedule_jobs
|
457
|
+
trigger_jobs unless @paused
|
458
|
+
timeout_jobs
|
459
|
+
|
460
|
+
sleep(@frequency)
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
@thread[@thread_key] = true
|
465
|
+
@thread[:rufus_scheduler] = self
|
466
|
+
@thread[:name] = @opts[:thread_name] || "#{@thread_key}_scheduler"
|
467
|
+
end
|
468
|
+
|
469
|
+
def unschedule_jobs
|
470
|
+
|
471
|
+
@jobs.delete_unscheduled
|
472
|
+
end
|
473
|
+
|
474
|
+
def trigger_jobs
|
475
|
+
|
476
|
+
now = Time.now
|
477
|
+
|
478
|
+
@jobs.each(now) do |job|
|
479
|
+
|
480
|
+
job.trigger(now)
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
def timeout_jobs
|
485
|
+
|
486
|
+
work_threads(:active).each do |t|
|
487
|
+
|
488
|
+
job = t[:rufus_scheduler_job]
|
489
|
+
to = t[:rufus_scheduler_timeout]
|
490
|
+
|
491
|
+
next unless job && to
|
492
|
+
# thread might just have become inactive (job -> nil)
|
493
|
+
|
494
|
+
ts = t[:rufus_scheduler_time]
|
495
|
+
to = to.is_a?(Time) ? to : ts + to
|
496
|
+
|
497
|
+
next if to > Time.now
|
498
|
+
|
499
|
+
t.raise(Rufus::Scheduler::TimeoutError)
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
def do_schedule(job_type, t, callable, opts, return_job_instance, block)
|
504
|
+
|
505
|
+
raise RuntimeError.new(
|
506
|
+
'cannot schedule, scheduler is down or shutting down'
|
507
|
+
) if @started_at == nil
|
508
|
+
|
509
|
+
callable, opts = nil, callable if callable.is_a?(Hash)
|
510
|
+
return_job_instance ||= opts[:job]
|
511
|
+
|
512
|
+
job_class =
|
513
|
+
case job_type
|
514
|
+
when :once
|
515
|
+
tt = Rufus::Scheduler.parse(t)
|
516
|
+
tt.is_a?(Time) ? AtJob : InJob
|
517
|
+
when :every
|
518
|
+
EveryJob
|
519
|
+
when :interval
|
520
|
+
IntervalJob
|
521
|
+
when :cron
|
522
|
+
CronJob
|
523
|
+
end
|
524
|
+
|
525
|
+
job = job_class.new(self, t, opts, block || callable)
|
526
|
+
|
527
|
+
raise ArgumentError.new(
|
528
|
+
"job frequency (#{job.frequency}) is higher than " +
|
529
|
+
"scheduler frequency (#{@frequency})"
|
530
|
+
) if job.respond_to?(:frequency) && job.frequency < @frequency
|
531
|
+
|
532
|
+
@jobs.push(job)
|
533
|
+
|
534
|
+
return_job_instance ? job : job.job_id
|
535
|
+
end
|
60
536
|
end
|
61
537
|
end
|
62
538
|
|