rufus-scheduler 2.0.24 → 3.0.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.
- 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
|
|