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.
@@ -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: []