unicron 0.1.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/lib/unicron.rb +30 -0
- data/lib/unicron/job.rb +148 -0
- data/lib/unicron/job_queue.rb +69 -0
- data/lib/unicron/schedule.rb +59 -0
- metadata +61 -0
data/lib/unicron.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
##
|
2
|
+
# Unicron provides a simple mechanism for scheduling one-off and repeating tasks
|
3
|
+
# to execute at specific times in the background of a long-running Ruby process.
|
4
|
+
#
|
5
|
+
# Tasks are represented as Job instances added to a JobQueue. A Schedule
|
6
|
+
# manages a single background thread which pops items off the JobQueue and runs
|
7
|
+
# them one at a time. Unicron manages a default Schedule for simplicity, but it
|
8
|
+
# is safe to create more - each will run its own thread with its own JobQueue.
|
9
|
+
# Since a Schedule only runs one Job at a time, a Job that is blocked or
|
10
|
+
# processing could potentially delay other Jobs on the same Schedule. For that
|
11
|
+
# reason, you may want to use a separate Schedule for long-running tasks.
|
12
|
+
#
|
13
|
+
module Unicron
|
14
|
+
autoload :Job, 'unicron/job'
|
15
|
+
autoload :JobQueue, 'unicron/job_queue'
|
16
|
+
autoload :Schedule, 'unicron/schedule'
|
17
|
+
|
18
|
+
## Provide access to the default Unicron Schedule.
|
19
|
+
def self.default_schedule
|
20
|
+
@@default_schedule ||= Schedule.new
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Create a new Job and add it to the default Schedule. See Job::new for
|
25
|
+
# available options.
|
26
|
+
#
|
27
|
+
def self.schedule options={}, &block
|
28
|
+
default_schedule.schedule options, &block
|
29
|
+
end
|
30
|
+
end
|
data/lib/unicron/job.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
##
|
2
|
+
# Job contains all of the information pertinent to a particular task that can be
|
3
|
+
# managed by Unicron.
|
4
|
+
#
|
5
|
+
class Unicron::Job
|
6
|
+
|
7
|
+
## Time of the first run of this Job.
|
8
|
+
attr_reader :at
|
9
|
+
|
10
|
+
## Set the time of the first run of this Job.
|
11
|
+
def at= value
|
12
|
+
self.options = {at: value}
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# True if a cancellation is pending. Only meaningful while the Job is running.
|
17
|
+
#
|
18
|
+
attr_reader :cancelled
|
19
|
+
|
20
|
+
## Time before the first run, and between repeats of this Job.
|
21
|
+
attr_reader :delay
|
22
|
+
|
23
|
+
## Set the time before the first run, and between repeats of this Job.
|
24
|
+
def delay= value
|
25
|
+
self.options = {delay: value}
|
26
|
+
end
|
27
|
+
|
28
|
+
## Time of the next Job run.
|
29
|
+
attr_reader :next_run
|
30
|
+
|
31
|
+
## Time of the previous Job run.
|
32
|
+
attr_reader :previous_run
|
33
|
+
|
34
|
+
## The number of times to repeat, or true for an infinitely repeating Job.
|
35
|
+
attr_reader :repeat
|
36
|
+
|
37
|
+
## Set the number of times to repeat, or true for an infinitely repeating Job.
|
38
|
+
def repeat= value
|
39
|
+
self.options = {repeat: value}
|
40
|
+
end
|
41
|
+
|
42
|
+
## Proc that gets called when the Job is run.
|
43
|
+
attr_reader :task
|
44
|
+
|
45
|
+
## Set the Proc that gets called when the Job is run.
|
46
|
+
def task= value
|
47
|
+
self.options = {task: value}
|
48
|
+
end
|
49
|
+
|
50
|
+
## Compare two Job instances chronologically by time of next run.
|
51
|
+
def <=> other
|
52
|
+
next_run <=> other.next_run
|
53
|
+
end
|
54
|
+
|
55
|
+
## Deschedules the Job.
|
56
|
+
def cancel
|
57
|
+
if queue
|
58
|
+
queue.delete self
|
59
|
+
else
|
60
|
+
@cancelled = true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Create a new Job. The code to run can be specified either as a block to the
|
66
|
+
# constructor or as an option named +task+. If both are specified, the option
|
67
|
+
# takes precedence over the block.
|
68
|
+
#
|
69
|
+
# For non-repeating Jobs, you must use either +at+ or +delay+ to specify when
|
70
|
+
# the Job should be run. If +at+ is specified, the job will run at that time
|
71
|
+
# (or immediately, if +at+ is in the past). If +delay+ is specified, it will
|
72
|
+
# run that number of seconds after it is scheduled.
|
73
|
+
#
|
74
|
+
# For repeating Jobs, set +repeat+ to the number of times to repeat the Job,
|
75
|
+
# or +true+ to repeat infinitely. The +delay+ option becomes mandatory and
|
76
|
+
# specifies the number of seconds between invocations. If the +at+ option is
|
77
|
+
# specified, it gives the time of the first invocation; otherwise, it first
|
78
|
+
# fires +delay+ seconds in the future.
|
79
|
+
#
|
80
|
+
# For both repeating and non-repeating jobs, if both +at+ and +delay+ are
|
81
|
+
# specified, and +at+ is in the past, the first invocation will occur at the
|
82
|
+
# first even multiple of +delay+ seconds after +at+. This provides an easy way
|
83
|
+
# of ensuring that the jobs are run at the same time every hour or day.
|
84
|
+
#
|
85
|
+
def initialize options={}, &task
|
86
|
+
self.options = options.merge task: task
|
87
|
+
end
|
88
|
+
|
89
|
+
## Return a Hash of this Job's options.
|
90
|
+
def options
|
91
|
+
{at: at, delay: delay, repeat: repeat, task: task}
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Set many Job options at once.
|
96
|
+
#
|
97
|
+
def options= value
|
98
|
+
notify_queue = !queue.nil? && [:at, :delay].any? {|k| value.has_key? k}
|
99
|
+
value = {at: at, delay: delay, repeat: repeat, task: task}.merge! value
|
100
|
+
if value[:task].nil?
|
101
|
+
raise ArgumentError, 'No task specified.'
|
102
|
+
end
|
103
|
+
if value[:at].nil? && value[:delay].nil?
|
104
|
+
raise ArgumentError, 'Either at or delay must be specified.'
|
105
|
+
end
|
106
|
+
if value[:repeat] && value[:delay].nil?
|
107
|
+
raise ArgumentError, 'Repeating jobs must specify delay.'
|
108
|
+
end
|
109
|
+
@at = value[:at]
|
110
|
+
@delay = value[:delay]
|
111
|
+
@repeat = value[:repeat]
|
112
|
+
@task = value[:task]
|
113
|
+
queue.send :jobs_changed if notify_queue
|
114
|
+
value
|
115
|
+
end
|
116
|
+
|
117
|
+
##
|
118
|
+
# Runs the Job, yielding self.
|
119
|
+
#
|
120
|
+
def run
|
121
|
+
self.repeat -= 1 if repeat.is_a? Numeric
|
122
|
+
task.call self
|
123
|
+
@previous_run = next_run
|
124
|
+
end
|
125
|
+
|
126
|
+
protected
|
127
|
+
## JobQueue that this Job is scheduled on. (Protected)
|
128
|
+
attr_reader :queue
|
129
|
+
|
130
|
+
##
|
131
|
+
# Set the JobQueue that this Job is scheduled on.
|
132
|
+
#
|
133
|
+
def queue= value
|
134
|
+
@cancelled = false
|
135
|
+
@queue = value
|
136
|
+
if queue
|
137
|
+
now = Time.now
|
138
|
+
base = at || previous_run
|
139
|
+
@next_run = if base.nil?
|
140
|
+
now + delay
|
141
|
+
elsif delay.nil? || base >= now
|
142
|
+
base
|
143
|
+
else
|
144
|
+
base + ((now - base) / delay).ceil * delay
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'monitor'
|
2
|
+
|
3
|
+
##
|
4
|
+
# JobQueue manages a set of Job instances for a Schedule. Users of Unicron
|
5
|
+
# should not have to deal directly with JobQueue instances.
|
6
|
+
#
|
7
|
+
class Unicron::JobQueue < Monitor
|
8
|
+
|
9
|
+
## Add a Job to the JobQueue.
|
10
|
+
def << job
|
11
|
+
synchronize do
|
12
|
+
unless @jobs.include? job
|
13
|
+
@jobs << job
|
14
|
+
job.send :queue=, self
|
15
|
+
jobs_changed
|
16
|
+
end
|
17
|
+
self
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
## Delete a Job from the JobQueue.
|
22
|
+
def delete job
|
23
|
+
synchronize do
|
24
|
+
if @jobs.include? job
|
25
|
+
job.send :queue=, nil
|
26
|
+
@jobs.delete job
|
27
|
+
jobs_changed
|
28
|
+
end
|
29
|
+
job
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
## Create an empty JobQueue.
|
34
|
+
def initialize
|
35
|
+
super
|
36
|
+
@jobs = []
|
37
|
+
@jobs_changed = new_cond
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Return the next Job in the JobQueue, sorted chronologically, no earlier than
|
42
|
+
# its appointed time. This call will block until the next Job is ready. If the
|
43
|
+
# JobQueue is empty, it will block until a Job is added and that Job becomes
|
44
|
+
# ready.
|
45
|
+
#
|
46
|
+
def pop
|
47
|
+
synchronize do
|
48
|
+
while true
|
49
|
+
@jobs_changed.wait_while {@jobs.empty?}
|
50
|
+
job = @jobs.first
|
51
|
+
delay = job.next_run - Time.now
|
52
|
+
break if delay <= 0
|
53
|
+
@jobs_changed.wait delay
|
54
|
+
end
|
55
|
+
job.send :queue=, nil
|
56
|
+
@jobs.delete job
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
## Notify a blocked thread that it should check the queue contents again.
|
63
|
+
def jobs_changed
|
64
|
+
synchronize do
|
65
|
+
@jobs.sort!
|
66
|
+
@jobs_changed.signal
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
##
|
2
|
+
# Schedule manages a JobQueue which contains Job instances, and a thread which
|
3
|
+
# pops and executes them. For simplicity's sake, the Unicron module manages a
|
4
|
+
# default Schedule, but you can create and manage your own if necessary. Each
|
5
|
+
# uses its own JobQueue and thread, so they will not interfere with each other.
|
6
|
+
#
|
7
|
+
class Unicron::Schedule
|
8
|
+
|
9
|
+
##
|
10
|
+
# Add a Job to the Schedule's JobQueue.
|
11
|
+
#
|
12
|
+
def << job
|
13
|
+
@queue << job
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
# Create a new, empty Schedule. The Schedule's thread is started and blocks
|
19
|
+
# immediately, waiting for a Job to be added.
|
20
|
+
#
|
21
|
+
def initialize
|
22
|
+
@queue = Unicron::JobQueue.new
|
23
|
+
@thread = Thread.new &method(:run)
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Create a new Job and add it to the Schedule. See Job::new for available
|
28
|
+
# options.
|
29
|
+
#
|
30
|
+
def schedule options={}, &block
|
31
|
+
job = Unicron::Job.new options, &block
|
32
|
+
self << job
|
33
|
+
job
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
##
|
39
|
+
# Loops infinitely, popping jobs off the JobQueue and running it. All
|
40
|
+
# exceptions are caught and logged to STDERR so that they do not interfere
|
41
|
+
# with other jobs. If the Job is repeating, it is added back to the JobQueue
|
42
|
+
# after it completes, regardless of whether it raised an exception.
|
43
|
+
#
|
44
|
+
def run
|
45
|
+
while job = @queue.pop
|
46
|
+
begin
|
47
|
+
job.run
|
48
|
+
rescue => err
|
49
|
+
puts STDERR, (["#{err.class}: #{err.message}"] +
|
50
|
+
err.backtrace.collect {|i| "from #{i}"}).join("\n\t")
|
51
|
+
end
|
52
|
+
if !job.cancelled &&
|
53
|
+
((job.repeat.is_a?(Numeric) && job.repeat > 0) ||
|
54
|
+
(!job.repeat.is_a?(Numeric) && job.repeat))
|
55
|
+
self << job
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: unicron
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- David P Kleinschmidt
|
9
|
+
- Ian C. Anderson
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2011-10-18 00:00:00.000000000Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
requirement: &83620100 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ~>
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 2.7.0
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *83620100
|
26
|
+
description: Schedules one-off and repeating background tasks within long-running
|
27
|
+
ruby processes.
|
28
|
+
email: david@kleinschmidt.name
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- lib/unicron.rb
|
34
|
+
- lib/unicron/schedule.rb
|
35
|
+
- lib/unicron/job_queue.rb
|
36
|
+
- lib/unicron/job.rb
|
37
|
+
homepage: http://github.com/zobar/unicron
|
38
|
+
licenses: []
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options: []
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements: []
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 1.8.10
|
58
|
+
signing_key:
|
59
|
+
specification_version: 3
|
60
|
+
summary: Schedule background tasks.
|
61
|
+
test_files: []
|