unicron 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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: []