rcron 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/CHANGELOG.rdoc +3 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +26 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +75 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/lib/rcron.rb +5 -0
- data/lib/rcron/alarm.rb +6 -0
- data/lib/rcron/parser.rb +86 -0
- data/lib/rcron/scheduler.rb +102 -0
- data/lib/rcron/task.rb +172 -0
- data/lib/rcron/timeout.rb +14 -0
- data/test/helper.rb +39 -0
- data/test/test_rcron.rb +176 -0
- data/test/test_rcron_parser.rb +88 -0
- data/test/test_rcron_task.rb +175 -0
- metadata +123 -0
data/.document
ADDED
data/CHANGELOG.rdoc
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem "yard", "~> 0.6.0"
|
10
|
+
gem "bundler", "~> 1.0.0"
|
11
|
+
gem "jeweler", "~> 1.6.4"
|
12
|
+
gem "rcov", ">= 0"
|
13
|
+
gem "simplecov"
|
14
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
git (1.2.5)
|
5
|
+
jeweler (1.6.4)
|
6
|
+
bundler (~> 1.0)
|
7
|
+
git (>= 1.2.5)
|
8
|
+
rake
|
9
|
+
rake (0.9.2)
|
10
|
+
rcov (0.9.10)
|
11
|
+
rcov (0.9.10-java)
|
12
|
+
simplecov (0.4.2)
|
13
|
+
simplecov-html (~> 0.4.4)
|
14
|
+
simplecov-html (0.4.5)
|
15
|
+
yard (0.6.8)
|
16
|
+
|
17
|
+
PLATFORMS
|
18
|
+
java
|
19
|
+
ruby
|
20
|
+
|
21
|
+
DEPENDENCIES
|
22
|
+
bundler (~> 1.0.0)
|
23
|
+
jeweler (~> 1.6.4)
|
24
|
+
rcov
|
25
|
+
simplecov
|
26
|
+
yard (~> 0.6.0)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Junegunn Choi
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
= rcron
|
2
|
+
|
3
|
+
A simple cron-like scheduler for Ruby.
|
4
|
+
|
5
|
+
== Installation
|
6
|
+
gem install rcron
|
7
|
+
|
8
|
+
== Cron format
|
9
|
+
As of now, most of the expressions except for ? and W are supported.
|
10
|
+
|
11
|
+
http://en.wikipedia.org/wiki/Cron#Format
|
12
|
+
|
13
|
+
== Examples
|
14
|
+
|
15
|
+
=== Basic
|
16
|
+
require 'rcron'
|
17
|
+
rcron = RCron.new
|
18
|
+
|
19
|
+
# Enqueue a task
|
20
|
+
rcron.q('runs every two minutes', '*/2 * * * *') do |task|
|
21
|
+
# Task logic
|
22
|
+
# ...
|
23
|
+
end
|
24
|
+
|
25
|
+
# You can `q' any number of tasks before starting rcron
|
26
|
+
|
27
|
+
rcron.start
|
28
|
+
|
29
|
+
=== One-time only task
|
30
|
+
rcron = RCron.new
|
31
|
+
rcron.q('will run once at 8pm next second friday', '0 8 * * fri#2') do |task|
|
32
|
+
# Removes the task from the queue
|
33
|
+
task.dq
|
34
|
+
|
35
|
+
# Task logic
|
36
|
+
# ...
|
37
|
+
end
|
38
|
+
rcron.start
|
39
|
+
|
40
|
+
=== Options
|
41
|
+
rcron = RCron.new
|
42
|
+
|
43
|
+
# :exclusive - Only one instance of this task will run simultaneously.
|
44
|
+
# :timeout - Task will be terminated if it takes longer than the specified seconds.
|
45
|
+
rcron.q('Every ten-minutes during summer',
|
46
|
+
'*/10 * * jun-aug *',
|
47
|
+
:exclusive => true,
|
48
|
+
:timeout => 1200) do |task|
|
49
|
+
# Task logic
|
50
|
+
# ...
|
51
|
+
end
|
52
|
+
|
53
|
+
# log to $stderr instead of default $stdout
|
54
|
+
rcron.start $stderr
|
55
|
+
|
56
|
+
== Notes
|
57
|
+
- Minimum interval for each task is one-minute just like cron. So rcron usually sleeps most of the time and wakes up only once a minute. (Except when short timeouts for the tasks are specified. In that case, rcron wakes up more frequently to check whether the task should be terminated)
|
58
|
+
- rcron checks to start tasks at the very first second of every minute. e.g. When you first start it at 12:00:45, it will sleep 15 seconds before doing anything.
|
59
|
+
- Tested on Ruby 1.8.7, Ruby 1.9.2 and JRuby 1.6.4
|
60
|
+
- 99.67% test coverage. (a false sense of security, though)
|
61
|
+
|
62
|
+
== Contributing to rcron
|
63
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
64
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
65
|
+
* Fork the project
|
66
|
+
* Start a feature/bugfix branch
|
67
|
+
* Commit and push until you are happy with your contribution
|
68
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
69
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
70
|
+
|
71
|
+
== Copyright
|
72
|
+
|
73
|
+
Copyright (c) 2011 Junegunn Choi. See LICENSE.txt for
|
74
|
+
further details.
|
75
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "rcron"
|
18
|
+
gem.homepage = "http://github.com/junegunn/rcron"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{A simple cron-like scheduler}
|
21
|
+
gem.description = %Q{A simple cron-like scheduler for Ruby}
|
22
|
+
gem.email = "junegunn.c@gmail.com"
|
23
|
+
gem.authors = ["Junegunn Choi"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rake/testtask'
|
29
|
+
Rake::TestTask.new(:test) do |test|
|
30
|
+
test.libs << 'lib' << 'test'
|
31
|
+
test.pattern = 'test/**/test_*.rb'
|
32
|
+
test.verbose = true
|
33
|
+
end
|
34
|
+
|
35
|
+
require 'rcov/rcovtask'
|
36
|
+
Rcov::RcovTask.new do |test|
|
37
|
+
test.libs << 'test'
|
38
|
+
test.pattern = 'test/**/test_*.rb'
|
39
|
+
test.verbose = true
|
40
|
+
test.rcov_opts << '--exclude "gems/*"'
|
41
|
+
end
|
42
|
+
|
43
|
+
task :default => :test
|
44
|
+
|
45
|
+
require 'yard'
|
46
|
+
YARD::Rake::YardocTask.new
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/rcron.rb
ADDED
data/lib/rcron/alarm.rb
ADDED
data/lib/rcron/parser.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
class RCron
|
2
|
+
module Parser
|
3
|
+
# @param [String] cron Cron-format schedule string
|
4
|
+
# @return [Hash] Parsed schedule
|
5
|
+
def self.parse(cron)
|
6
|
+
elements = cron.strip.split(/\s+/)
|
7
|
+
raise ArgumentError.new("Invalid format: '#{cron}'") unless [5, 6].include? elements.length
|
8
|
+
|
9
|
+
parser = lambda { |type, min, len, element, subs, extra|
|
10
|
+
return nil if element.nil?
|
11
|
+
|
12
|
+
max = min + len
|
13
|
+
|
14
|
+
(subs || {}).each do |k, v|
|
15
|
+
element = element.gsub(/\b#{k}\b/i, v.to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
ret = element.split(',').map { |e|
|
19
|
+
err = ArgumentError.new("Invalid #{type} specification: '#{cron}'")
|
20
|
+
case e
|
21
|
+
when '*'
|
22
|
+
nil
|
23
|
+
when %r|^[0-9]+$|
|
24
|
+
ei = e.to_i
|
25
|
+
raise err if ei < min || ei > max
|
26
|
+
ei
|
27
|
+
when %r|^\*/([1-9][0-9]*)$|
|
28
|
+
(min...max).select { |m| m % $1.to_i == 0 }
|
29
|
+
when %r|^([0-9]+)-([0-9]+)$|
|
30
|
+
f, t = $1.to_i, $2.to_i
|
31
|
+
|
32
|
+
raise err if f < min || f > max
|
33
|
+
raise err if t < min || t > max
|
34
|
+
|
35
|
+
if f < t
|
36
|
+
(f..t).to_a
|
37
|
+
else
|
38
|
+
(f...max).to_a + (min..t).to_a
|
39
|
+
end
|
40
|
+
else
|
41
|
+
extra && extra.call(e) || raise(err)
|
42
|
+
end
|
43
|
+
}.flatten.compact.uniq.inject({}) { |h, k|
|
44
|
+
if k.is_a? Hash
|
45
|
+
h[k.first.first] = k.first.last
|
46
|
+
else
|
47
|
+
h[k] = true
|
48
|
+
end
|
49
|
+
h
|
50
|
+
}
|
51
|
+
ret.empty? ? nil : ret
|
52
|
+
}
|
53
|
+
|
54
|
+
schedule = {
|
55
|
+
:minutes => parser.call('minute', 0, 60, elements[0], nil, nil),
|
56
|
+
:hours => parser.call('hour', 0, 24, elements[1], nil, nil),
|
57
|
+
:days => parser.call('day/month', 1, 31, elements[2], nil,
|
58
|
+
lambda { |e|
|
59
|
+
case e.upcase
|
60
|
+
when 'L'
|
61
|
+
-1
|
62
|
+
when /W/
|
63
|
+
raise NotImplementedError.new("Nearest weekday not implemeneted: '#{cron}'")
|
64
|
+
end
|
65
|
+
}),
|
66
|
+
:months => parser.call('month', 1, 12, elements[3],
|
67
|
+
{ 'jan' => 1, 'feb' => 2, 'mar' => 3,
|
68
|
+
'apr' => 4, 'may' => 5, 'jun' => 6,
|
69
|
+
'jul' => 7, 'aug' => 8, 'sep' => 9,
|
70
|
+
'oct' => 10, 'nov' => 11, 'dec' => 12 }, nil),
|
71
|
+
:weekdays => parser.call('day/week', 0, 7, elements[4],
|
72
|
+
{ 'sun' => 0, 'mon' => 1, 'tue' => 2,
|
73
|
+
'wed' => 3, 'thu' => 4, 'fri' => 5, 'sat' => 6 },
|
74
|
+
lambda { |e|
|
75
|
+
if e =~ /^([0-6]+)#([1-5])$/
|
76
|
+
{$1.to_i => $2.to_i}
|
77
|
+
end
|
78
|
+
}),
|
79
|
+
:years => parser.call('year', 1970, 130, elements[5], nil, nil)
|
80
|
+
}
|
81
|
+
schedule
|
82
|
+
end
|
83
|
+
end#Parser
|
84
|
+
end#RCron
|
85
|
+
|
86
|
+
#puts RCron::Parser.parse('*/8 * */2,L * sun,wed#2')
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
class RCron
|
4
|
+
def initialize
|
5
|
+
@tasks = []
|
6
|
+
@mutex = Mutex.new
|
7
|
+
@sleeping = false
|
8
|
+
@log_mutex = Mutex.new
|
9
|
+
@log_ostream = $stdout
|
10
|
+
end
|
11
|
+
|
12
|
+
# Enqueues a task to be run
|
13
|
+
# @param [String] name Name of the task
|
14
|
+
# @param [String] schedule Cron-format schedule string
|
15
|
+
# @param [Hash] options Additional options for the task. :exclusive and :timeout.
|
16
|
+
# @return [RCron::Task]
|
17
|
+
def q name, schedule, options = {}, &block
|
18
|
+
raise ArgumentError.new("Block not given") unless block_given?
|
19
|
+
|
20
|
+
new_task = nil
|
21
|
+
@mutex.synchronize do
|
22
|
+
@tasks << new_task = Task.send(:new,
|
23
|
+
self, name, schedule,
|
24
|
+
options[:exclusive], options[:timeout],
|
25
|
+
&block)
|
26
|
+
end
|
27
|
+
return new_task
|
28
|
+
end
|
29
|
+
|
30
|
+
# Starts the scheduler
|
31
|
+
# @param log_output_stream Stream to output scheduler log. Should implement puts method.
|
32
|
+
def start log_output_stream = $stdout
|
33
|
+
raise ArgumentError.new(
|
34
|
+
"Log output stream should implement puts method") unless
|
35
|
+
log_output_stream.respond_to? :puts
|
36
|
+
|
37
|
+
@log_ostream = log_output_stream
|
38
|
+
@thread = Thread.current
|
39
|
+
|
40
|
+
log "rcron started"
|
41
|
+
|
42
|
+
now = Time.now
|
43
|
+
while @tasks.length > 0
|
44
|
+
# At every minute
|
45
|
+
next_tick = Time.at( (now + 60 - now.sec).to_i )
|
46
|
+
interval = @tasks.select(&:running?).map(&:timeout).compact.min
|
47
|
+
begin
|
48
|
+
@mutex.synchronize { @sleeping = true }
|
49
|
+
#puts [ next_tick - now, interval ].compact.min
|
50
|
+
sleep [ next_tick - now, interval ].compact.min
|
51
|
+
@mutex.synchronize { @sleeping = false }
|
52
|
+
rescue RCron::Alarm => e
|
53
|
+
# puts 'woke up'
|
54
|
+
end
|
55
|
+
|
56
|
+
# Join completed threads
|
57
|
+
@tasks.select(&:running?).each do |t|
|
58
|
+
t.send :join
|
59
|
+
end
|
60
|
+
|
61
|
+
# Removed dequeued tasks
|
62
|
+
@tasks.reject { |e| e.running? || e.queued? }.each do |t|
|
63
|
+
@mutex.synchronize { @tasks.delete t }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Start new task threads if it's time
|
67
|
+
now = Time.now
|
68
|
+
@tasks.select { |e| e.queued? && e.scheduled?(now) }.each do |t|
|
69
|
+
if t.running? && t.exclusive?
|
70
|
+
log "[#{t.name}] already running exclusively"
|
71
|
+
next
|
72
|
+
end
|
73
|
+
|
74
|
+
log "[#{t.name}] started"
|
75
|
+
t.send :start, now
|
76
|
+
end if now >= next_tick
|
77
|
+
end#while
|
78
|
+
log "rcron completed"
|
79
|
+
end#start
|
80
|
+
|
81
|
+
# Crontab-like tasklist
|
82
|
+
# @return [String]
|
83
|
+
def tab
|
84
|
+
@tasks.map { |t| "#{t.schedule_expression} #{t.name}" }.join($/)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
def wake_up
|
89
|
+
@mutex.synchronize {
|
90
|
+
if @sleeping
|
91
|
+
@sleeping = false
|
92
|
+
@thread.raise(RCron::Alarm.new)
|
93
|
+
end
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
def log msg
|
98
|
+
@log_mutex.synchronize do
|
99
|
+
@log_ostream.puts "[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] #{msg}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end#RCron
|
data/lib/rcron/task.rb
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
class RCron
|
5
|
+
class Task
|
6
|
+
# RCron scheduler for this task
|
7
|
+
attr_reader :rcron
|
8
|
+
|
9
|
+
# Name of the task
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
# Cron schedule expression
|
13
|
+
attr_reader :schedule_expression
|
14
|
+
|
15
|
+
# Parsed cron schedule
|
16
|
+
attr_reader :schedule
|
17
|
+
|
18
|
+
# Timeout for the task
|
19
|
+
attr_reader :timeout
|
20
|
+
|
21
|
+
# Threads running this task
|
22
|
+
def threads
|
23
|
+
@mutex.synchronize { return @threads.dup }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Executes the task manually
|
27
|
+
def run
|
28
|
+
if @block.arity >= 1
|
29
|
+
@block.call self
|
30
|
+
else
|
31
|
+
@block.call
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns if the task is being executed by one or more threads
|
36
|
+
# @return [Boolean]
|
37
|
+
def running?
|
38
|
+
@mutex.synchronize {
|
39
|
+
return @threads.empty? == false
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns whether if the same task should not run simultaneously
|
44
|
+
# @return [Boolean]
|
45
|
+
def exclusive?
|
46
|
+
@exclusive
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns if the task is queued to the scheduler
|
50
|
+
# @return [Boolean]
|
51
|
+
def queued?
|
52
|
+
@queued
|
53
|
+
end
|
54
|
+
|
55
|
+
# Removes the task from the scheduler
|
56
|
+
def dq
|
57
|
+
@queued = false
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns if the task is supposed to be triggered at the given moment.
|
62
|
+
# @param [Time] at
|
63
|
+
# @return [Boolean]
|
64
|
+
def scheduled? at
|
65
|
+
if @previous_start.nil? || (at - at.sec).to_i > (@previous_start - @previous_start.sec).to_i
|
66
|
+
s, m, h, day, mon, year, wd = at.to_a
|
67
|
+
|
68
|
+
td = Date.new(year, mon, day) # at.to_date # Doesn't work with current JRuby
|
69
|
+
wom = ((td - td.day + 1).wday + td.day - 1) / 7 + 1
|
70
|
+
last_day = (td + 1).month > td.month
|
71
|
+
|
72
|
+
(@schedule[:years].nil? || @schedule[:years].has_key?(year)) &&
|
73
|
+
(@schedule[:months].nil? || @schedule[:months].has_key?(mon)) &&
|
74
|
+
(@schedule[:weekdays].nil? || [true, wom].include?(@schedule[:weekdays][wd])) &&
|
75
|
+
(@schedule[:days].nil? || @schedule[:days].has_key?(day) || (last_day && @schedule[:days].has_key?(-1)) ) &&
|
76
|
+
(@schedule[:hours].nil? || @schedule[:hours].has_key?(h)) &&
|
77
|
+
(@schedule[:minutes].nil? || @schedule[:minutes].has_key?(m))
|
78
|
+
else
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
def start now
|
85
|
+
@previous_start = now
|
86
|
+
@mutex.synchronize do
|
87
|
+
@threads << TaskThread.new(self, now)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def join
|
92
|
+
@threads.dup.each do |thr|
|
93
|
+
if thr.alive?
|
94
|
+
# Timeout!
|
95
|
+
if @timeout && thr.elapsed > @timeout
|
96
|
+
thr.kill!
|
97
|
+
@mutex.synchronize { @threads.delete thr }
|
98
|
+
end
|
99
|
+
else
|
100
|
+
# Finished already
|
101
|
+
thr.cleanup
|
102
|
+
@mutex.synchronize { @threads.delete thr }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
private_class_method :new
|
109
|
+
|
110
|
+
def initialize scheduler, name, schedule, exclusive, timeout, &block
|
111
|
+
if timeout && (timeout.is_a?(Numeric) == false || timeout < 1)
|
112
|
+
raise ArgumentError.new("Invalid timeout: #{timeout} (sec)")
|
113
|
+
end
|
114
|
+
unless [true, false, nil].include? exclusive
|
115
|
+
raise ArgumentError.new("exclusive option must be true or false")
|
116
|
+
end
|
117
|
+
|
118
|
+
@rcron = scheduler
|
119
|
+
@name = name
|
120
|
+
@schedule = Parser.parse schedule
|
121
|
+
@schedule_expression = schedule
|
122
|
+
@exclusive = exclusive == true
|
123
|
+
@timeout = timeout
|
124
|
+
@block = block
|
125
|
+
@queued = true
|
126
|
+
@threads = []
|
127
|
+
@mutex = Mutex.new
|
128
|
+
|
129
|
+
@previous_start = nil
|
130
|
+
end
|
131
|
+
|
132
|
+
class TaskThread
|
133
|
+
attr_reader :started_at, :ended_at
|
134
|
+
attr_reader :exception
|
135
|
+
|
136
|
+
def initialize task, started_at
|
137
|
+
@started_at = started_at
|
138
|
+
@thread = Thread.new {
|
139
|
+
begin
|
140
|
+
task.run
|
141
|
+
@ended_at = Time.now
|
142
|
+
|
143
|
+
task.rcron.send :log, "[#{task.name}] completed (#{'%.2f' % elapsed}s)"
|
144
|
+
rescue Exception => e
|
145
|
+
@ended_at = Time.now
|
146
|
+
|
147
|
+
task.rcron.send :log, "[#{task.name}] terminated: #{[e, e.backtrace].join($/)}"
|
148
|
+
# Ignore exception?
|
149
|
+
end
|
150
|
+
task.rcron.send :wake_up
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
def cleanup
|
155
|
+
@thread.join
|
156
|
+
end
|
157
|
+
|
158
|
+
def elapsed
|
159
|
+
(@ended_at || Time.now) - @started_at
|
160
|
+
end
|
161
|
+
|
162
|
+
def alive?
|
163
|
+
@thread.alive?
|
164
|
+
end
|
165
|
+
|
166
|
+
def kill!
|
167
|
+
@thread.raise RCron::Timeout.new("Timeout: task terminated by rcron", @started_at, Time.now)
|
168
|
+
cleanup
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end#Task
|
172
|
+
end#RCron
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class RCron
|
2
|
+
# Timeout exception
|
3
|
+
class Timeout < Exception
|
4
|
+
attr_reader :started_at
|
5
|
+
attr_reader :terminated_at
|
6
|
+
|
7
|
+
def initialize(msg, started_at, terminated_at)
|
8
|
+
super(msg)
|
9
|
+
@started_at = started_at
|
10
|
+
@terminated_at = terminated_at
|
11
|
+
end
|
12
|
+
end#Timeout
|
13
|
+
end#RCron
|
14
|
+
|
data/test/helper.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
require 'simplecov'
|
4
|
+
SimpleCov.start
|
5
|
+
|
6
|
+
begin
|
7
|
+
Bundler.setup(:default, :development)
|
8
|
+
rescue Bundler::BundlerError => e
|
9
|
+
$stderr.puts e.message
|
10
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
11
|
+
exit e.status_code
|
12
|
+
end
|
13
|
+
require 'test/unit'
|
14
|
+
|
15
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
16
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
17
|
+
require 'rcron'
|
18
|
+
|
19
|
+
class Test::Unit::TestCase
|
20
|
+
end
|
21
|
+
|
22
|
+
# Overrides timings constructs to boost up the test speed
|
23
|
+
# - This might break some test results
|
24
|
+
class Time
|
25
|
+
class << self
|
26
|
+
alias org_now now
|
27
|
+
end
|
28
|
+
def self.now
|
29
|
+
@@first_call ||= Time.org_now
|
30
|
+
return @@first_call + (Time.org_now - @@first_call) * 20
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def sleep n
|
35
|
+
Kernel.sleep n * 0.05
|
36
|
+
end
|
37
|
+
|
38
|
+
Thread.abort_on_exception = false # FIXME: Without this, test fails.
|
39
|
+
|
data/test/test_rcron.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
$LOAD_PATH << "."
|
2
|
+
require 'helper'
|
3
|
+
|
4
|
+
class TestRcron < Test::Unit::TestCase
|
5
|
+
class LogStream
|
6
|
+
def initialize
|
7
|
+
@lines = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def puts str
|
11
|
+
$stdout.puts str
|
12
|
+
@lines << str
|
13
|
+
end
|
14
|
+
|
15
|
+
def count pat
|
16
|
+
@lines.select { |e| e =~ pat }.count
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_empty_q
|
21
|
+
rcron = RCron.new
|
22
|
+
log = LogStream.new
|
23
|
+
rcron.start(log)
|
24
|
+
|
25
|
+
assert_equal 1, log.count(/completed/)
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_blockless
|
29
|
+
rcron = RCron.new
|
30
|
+
assert_raise(ArgumentError) { rcron.q('test task 1', "* * * * *") }
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_invalid_schedule
|
34
|
+
rcron = RCron.new
|
35
|
+
assert_raise(ArgumentError) { rcron.q('test task 1', "* *") { |task| } }
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_basic_task_dq
|
39
|
+
puts 'basic task eq'
|
40
|
+
counter = 0
|
41
|
+
rcron = RCron.new
|
42
|
+
rcron.q('basic task 1 - auto dq', "* * * * *") do |task|
|
43
|
+
counter += 1
|
44
|
+
sleep 1
|
45
|
+
task.dq
|
46
|
+
end
|
47
|
+
|
48
|
+
@task = rcron.q('basic task 2 - auto dq', "* * * * *") do
|
49
|
+
counter += 2
|
50
|
+
sleep 3
|
51
|
+
@task.dq
|
52
|
+
end
|
53
|
+
|
54
|
+
st = Time.now
|
55
|
+
rcron.start
|
56
|
+
assert_equal 3, counter
|
57
|
+
assert Time.now - st > 3
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_basic_task
|
61
|
+
puts 'basic task'
|
62
|
+
counter = 0
|
63
|
+
rcron = RCron.new
|
64
|
+
rcron.q('basic task', "* * * * *") do |task|
|
65
|
+
task.dq if counter >= 2
|
66
|
+
|
67
|
+
counter += 1
|
68
|
+
sleep 60 + 10
|
69
|
+
end
|
70
|
+
|
71
|
+
st = Time.now
|
72
|
+
rcron.start
|
73
|
+
assert_equal 3, counter
|
74
|
+
assert Time.now - st > 3 * 60
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_dq
|
78
|
+
counter = 0
|
79
|
+
rcron = RCron.new
|
80
|
+
task = rcron.q('never', "* * * * *") do |task|
|
81
|
+
counter += 1
|
82
|
+
end
|
83
|
+
|
84
|
+
task.dq
|
85
|
+
rcron.start
|
86
|
+
assert_equal 0, counter
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_timeout
|
90
|
+
puts 'timeout'
|
91
|
+
counter = 0
|
92
|
+
rcron = RCron.new
|
93
|
+
rcron.q('timeout', '*/5 * * * *', :timeout => 10) do |task|
|
94
|
+
task.dq # no more
|
95
|
+
loop do
|
96
|
+
counter += 1
|
97
|
+
sleep 1
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
assert_raise(ArgumentError) { rcron.q('inv timeout', '* * * * *', :timeout => -3) { } }
|
102
|
+
assert_raise(ArgumentError) { rcron.q('inv timeout', '* * * * *', :timeout => 'not too long') { } }
|
103
|
+
|
104
|
+
rcron.start
|
105
|
+
assert_equal 10, counter
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_non_exclusive
|
109
|
+
puts 'non exclusive'
|
110
|
+
counter = 0
|
111
|
+
log = LogStream.new
|
112
|
+
rcron = RCron.new
|
113
|
+
truth = true
|
114
|
+
rcron.q('non-exclusive', '* * * * *') do |task|
|
115
|
+
counter += 1
|
116
|
+
truth &&= counter == task.threads.length
|
117
|
+
puts task.threads.length
|
118
|
+
sleep 60 * 2 + 30
|
119
|
+
task.dq
|
120
|
+
end
|
121
|
+
rcron.start(log)
|
122
|
+
|
123
|
+
assert truth
|
124
|
+
assert_equal 3, counter
|
125
|
+
end
|
126
|
+
|
127
|
+
def test_exclusive
|
128
|
+
puts 'exclusive'
|
129
|
+
counter = 0
|
130
|
+
log = LogStream.new
|
131
|
+
rcron = RCron.new
|
132
|
+
rcron.q('exclusive', '* * * * *', :exclusive => true) do |task|
|
133
|
+
counter += 1
|
134
|
+
sleep 60 * 2 + 10
|
135
|
+
task.dq
|
136
|
+
end
|
137
|
+
|
138
|
+
assert_raise(ArgumentError) {
|
139
|
+
rcron.q('inv exclusive', '* * * * *', :exclusive => 'i guess') { }
|
140
|
+
}
|
141
|
+
rcron.start(log)
|
142
|
+
|
143
|
+
assert_equal 1, counter
|
144
|
+
assert log.count(/exclusively/i) > 0
|
145
|
+
end
|
146
|
+
|
147
|
+
def test_invalid_output_stream
|
148
|
+
rcron = RCron.new
|
149
|
+
assert_raise(ArgumentError) { rcron.start("invalid") }
|
150
|
+
end
|
151
|
+
|
152
|
+
def test_tab
|
153
|
+
rcron = RCron.new
|
154
|
+
rcron.q('task number 1', '* * * * *') {}
|
155
|
+
rcron.q('task number 2', '*/2 * * * *') {}
|
156
|
+
rcron.q('task number 3', '*/3 * * * *') {}
|
157
|
+
|
158
|
+
assert_equal ["* * * * * task number 1",
|
159
|
+
"*/2 * * * * task number 2",
|
160
|
+
"*/3 * * * * task number 3"].join($/), rcron.tab
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_exception
|
164
|
+
log = LogStream.new
|
165
|
+
rcron = RCron.new
|
166
|
+
rcron.q('Exceptional', '* * * * *') do |task|
|
167
|
+
task.dq
|
168
|
+
sleep 80
|
169
|
+
raise Exception.new("this-error")
|
170
|
+
end
|
171
|
+
rcron.start(log)
|
172
|
+
|
173
|
+
assert_equal 1, log.count(/this-error/)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
@@ -0,0 +1,88 @@
|
|
1
|
+
$LOAD_PATH << "."
|
2
|
+
require 'helper'
|
3
|
+
|
4
|
+
class TestRcronParser < Test::Unit::TestCase
|
5
|
+
def test_invalid_format
|
6
|
+
assert_raise(ArgumentError) { RCron::Parser.parse('* * * *') }
|
7
|
+
assert_raise(ArgumentError) { RCron::Parser.parse('* * * * * * *') }
|
8
|
+
assert_raise(ArgumentError) { RCron::Parser.parse('a b c d e') }
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_asterisk
|
12
|
+
RCron::Parser.parse('* * * * *').each do |k, v|
|
13
|
+
assert_nil v
|
14
|
+
end
|
15
|
+
|
16
|
+
RCron::Parser.parse('* * * * * *').each do |k, v|
|
17
|
+
assert_nil v
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_single_number
|
22
|
+
RCron::Parser.parse('1 1 1 1 1 2000').each do |k, v|
|
23
|
+
assert_equal(
|
24
|
+
case k
|
25
|
+
when :minutes
|
26
|
+
{1 => true}
|
27
|
+
when :hours
|
28
|
+
{1 => true}
|
29
|
+
when :days
|
30
|
+
{1 => true}
|
31
|
+
when :months
|
32
|
+
{1 => true}
|
33
|
+
when :weekdays
|
34
|
+
{1 => true}
|
35
|
+
when :years
|
36
|
+
{2000 => true}
|
37
|
+
end, v)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Invalid range
|
41
|
+
assert_raise(ArgumentError) { RCron::Parser.parse('61 * * * *') }
|
42
|
+
assert_raise(ArgumentError) { RCron::Parser.parse('* 25 * * *') }
|
43
|
+
assert_raise(ArgumentError) { RCron::Parser.parse('* * 50 * *') }
|
44
|
+
assert_raise(ArgumentError) { RCron::Parser.parse('* * * 14 *') }
|
45
|
+
assert_raise(ArgumentError) { RCron::Parser.parse('* * * * 10000') }
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_range
|
49
|
+
assert_equal({10 => true, 11 => true, 12 => true, 59 => true, 0 => true, 1 => true, 2 => true},
|
50
|
+
RCron::Parser.parse('10-12,59-2 * * * *')[:minutes])
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_dividend
|
54
|
+
assert_equal({3 => true, 6 => true, 9 => true, 12 =>true},
|
55
|
+
RCron::Parser.parse('* * * */3 *')[:months])
|
56
|
+
assert_equal({0 => true, 23 => true, 24 => true, 46 => true, 48 => true},
|
57
|
+
RCron::Parser.parse('*/23,*/24 * * * *')[:minutes])
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_months
|
61
|
+
assert_equal({1 => true, 2 => true, 3 => true, 6 => true, 7 => true, 8 => true},
|
62
|
+
RCron::Parser.parse('* * * jan-MAR,Jun-Aug *')[:months])
|
63
|
+
assert_equal((1..12).to_a.sort,
|
64
|
+
RCron::Parser.parse('* * * jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec *')[:months].keys.sort)
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_weekdays
|
68
|
+
assert_equal([0,1,2,3,4,5,6],
|
69
|
+
RCron::Parser.parse('* * * * sun,mon,tue,wed,thu,fri,sat,sun')[:weekdays].keys.sort)
|
70
|
+
assert_equal([0,1,2,3,4,5,6],
|
71
|
+
RCron::Parser.parse('* * * * SUN,MON,TUE,WED,THU,FRI,SAT,SUN')[:weekdays].keys.sort)
|
72
|
+
assert_equal([0,1,2,3,4,6],
|
73
|
+
RCron::Parser.parse('* * * * WED-THU,SAT-TUE')[:weekdays].keys.sort)
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_nearest_weekday
|
77
|
+
# TODO FIXME TODO
|
78
|
+
assert_raise(NotImplementedError) { RCron::Parser.parse('* * W * *') }
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_last_day
|
82
|
+
assert_equal([-1, 31], RCron::Parser.parse('* * L,31 * *')[:days].keys.sort)
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_year
|
86
|
+
assert_equal([2010,2011,2012,2020], RCron::Parser.parse('* * L,31 * * 2010-2012,2020')[:years].keys.sort)
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
$LOAD_PATH << "."
|
2
|
+
require 'helper'
|
3
|
+
|
4
|
+
class TestRcronTask < Test::Unit::TestCase
|
5
|
+
def test_name
|
6
|
+
rcron = RCron.new
|
7
|
+
name = nil
|
8
|
+
|
9
|
+
task = rcron.q('my name', '* * * * *') { |t|
|
10
|
+
name = t.name
|
11
|
+
t.dq
|
12
|
+
}
|
13
|
+
assert_equal 'my name', task.name
|
14
|
+
rcron.start
|
15
|
+
assert_equal 'my name', name
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_schedule
|
19
|
+
rcron = RCron.new
|
20
|
+
schedule = '* * * * *'
|
21
|
+
task = rcron.q('test_schedule', schedule) { }
|
22
|
+
assert_equal RCron::Parser.parse(schedule), task.schedule
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_rcron
|
26
|
+
rcron = RCron.new
|
27
|
+
schedule = '* * * * *'
|
28
|
+
task = rcron.q('test_rcron', schedule) { }
|
29
|
+
assert_equal rcron, task.rcron
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_timeout
|
33
|
+
rcron = RCron.new
|
34
|
+
task = rcron.q('test_timeout', '* * * * *', :timeout => 100) { }
|
35
|
+
assert_equal 100, task.timeout
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_exclusive
|
39
|
+
rcron = RCron.new
|
40
|
+
task = rcron.q('test_exclusive', '* * * * *', :exclusive => true) { }
|
41
|
+
assert_equal true, task.exclusive?
|
42
|
+
task = rcron.q('test_exclusive', '* * * * *', :exclusive => false) { }
|
43
|
+
assert_equal false, task.exclusive?
|
44
|
+
task = rcron.q('test_exclusive', '* * * * *') { }
|
45
|
+
assert_equal false, task.exclusive?
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_running
|
49
|
+
rcron = RCron.new
|
50
|
+
running = nil
|
51
|
+
task = rcron.q('test_running', '* * * * *') { |t|
|
52
|
+
running = t.running?
|
53
|
+
t.dq
|
54
|
+
}
|
55
|
+
rcron.start
|
56
|
+
assert_equal false, task.running?
|
57
|
+
assert_equal true, running
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_queued
|
61
|
+
rcron = RCron.new
|
62
|
+
task = rcron.q('test_queued', '* * * * *') { }
|
63
|
+
assert_equal true, task.queued?
|
64
|
+
task.dq
|
65
|
+
assert_equal false, task.queued?
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_threads
|
69
|
+
rcron = RCron.new
|
70
|
+
num_threads = nil
|
71
|
+
task = rcron.q('test_threads', '* * * * *') { |t|
|
72
|
+
p t.threads
|
73
|
+
num_threads = t.threads.length
|
74
|
+
sleep 60 + 10
|
75
|
+
t.dq
|
76
|
+
}
|
77
|
+
assert_equal 0, task.threads.length
|
78
|
+
rcron.start
|
79
|
+
assert_equal 2, num_threads
|
80
|
+
assert_equal 0, task.threads.length
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_now
|
84
|
+
now = Time.parse('2011-08-27 16:16:16')
|
85
|
+
rcron = RCron.new
|
86
|
+
{
|
87
|
+
'* * * * *' => true,
|
88
|
+
'0 * * * *' => false,
|
89
|
+
'16 * * * *' => true,
|
90
|
+
'*/2 * * * *' => true,
|
91
|
+
'10-17 * * * *' => true,
|
92
|
+
'*/3 * * * *' => false,
|
93
|
+
'17-15 * * * *' => false,
|
94
|
+
|
95
|
+
'* 16 * * *' => true,
|
96
|
+
'* */2 * * *' => true,
|
97
|
+
'* */4 * * *' => true,
|
98
|
+
'* 10-17 * * *' => true,
|
99
|
+
'* */3 * * *' => false,
|
100
|
+
'* 17-15 * * *' => false,
|
101
|
+
|
102
|
+
'* * 27 * *' => true,
|
103
|
+
'* * */3 * *' => true,
|
104
|
+
'* * */9 * *' => true,
|
105
|
+
'* * 20-29 * *' => true,
|
106
|
+
'* * 20-22,27,*/4 * *' => true,
|
107
|
+
'* * L * *' => false,
|
108
|
+
'* * 28 * *' => false,
|
109
|
+
|
110
|
+
'* * * 8 *' => true,
|
111
|
+
'* * * */1 *' => true,
|
112
|
+
'* * * */2 *' => true,
|
113
|
+
'* * * */4 *' => true,
|
114
|
+
'* * * */8 *' => true,
|
115
|
+
'* * * Aug *' => true,
|
116
|
+
'* * * AuG *' => true,
|
117
|
+
'* * * 5,7,8 *' => true,
|
118
|
+
'* * * 5-9 *' => true,
|
119
|
+
'* * * 12-9 *' => true,
|
120
|
+
'* * * may-sep *' => true,
|
121
|
+
'* * * nov-sep *' => true,
|
122
|
+
'* * * jan,dec,aug *' => true,
|
123
|
+
'* * * sep *' => false,
|
124
|
+
'* * * 9-10 *' => false,
|
125
|
+
'* * * */7 *' => false,
|
126
|
+
|
127
|
+
'* * * * sat' => true,
|
128
|
+
'* * * * fri-sun' => true,
|
129
|
+
'* * * * mon,tue,sat' => true,
|
130
|
+
'* * * * 6' => true,
|
131
|
+
'* * * * 5-6' => true,
|
132
|
+
'* * * * 5-0' => true,
|
133
|
+
'* * * * 1,6' => true,
|
134
|
+
'* * * * */1' => true,
|
135
|
+
'* * * * */2' => true,
|
136
|
+
'* * * * */3' => true,
|
137
|
+
'* * * * */6' => true,
|
138
|
+
'* * * * tue' => false,
|
139
|
+
'* * * * fri' => false,
|
140
|
+
'* * * * sun-fri' => false,
|
141
|
+
'* * * * 0-5' => false,
|
142
|
+
|
143
|
+
'* * * * sat#1' => false,
|
144
|
+
'* * * * sat#2' => false,
|
145
|
+
'* * * * sat#3' => false,
|
146
|
+
'* * * * sat#4' => true,
|
147
|
+
'* * * * sat#5' => false,
|
148
|
+
'* * * * 6#1' => false,
|
149
|
+
'* * * * 6#2' => false,
|
150
|
+
'* * * * 6#3' => false,
|
151
|
+
'* * * * 6#4' => true,
|
152
|
+
'* * * * 6#5' => false,
|
153
|
+
|
154
|
+
'* * * * * 2011' => true,
|
155
|
+
'* * * * * */2011' => true,
|
156
|
+
'* * * * * 2010-2012' => true,
|
157
|
+
'* * * * * 2010' => false,
|
158
|
+
|
159
|
+
'16 16 27 8 6 2010' => false,
|
160
|
+
'16 16 27 8 6 2011' => true,
|
161
|
+
'16 16 27 8 sat 2011' => true,
|
162
|
+
'16 16 27 aug sat 2010-2012' => true,
|
163
|
+
'16 16 27 aug sun 2010-2012' => false,
|
164
|
+
'*/8 16 27 aug 5-6 2010-2012' => true,
|
165
|
+
'17 16 27 aug sat 2010-2012' => false,
|
166
|
+
'16 17 27 aug * 2010-2012' => false,
|
167
|
+
'16 16 L aug * 2010-2012' => false
|
168
|
+
}.each do |sch, ass|
|
169
|
+
task = rcron.q('test_now', sch) { }
|
170
|
+
puts sch
|
171
|
+
assert_equal ass, task.scheduled?(now)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rcron
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Junegunn Choi
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-08-31 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: yard
|
16
|
+
requirement: &2156282420 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.6.0
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2156282420
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: bundler
|
27
|
+
requirement: &2156281700 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.0.0
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2156281700
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: jeweler
|
38
|
+
requirement: &2156281080 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 1.6.4
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2156281080
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rcov
|
49
|
+
requirement: &2156280220 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *2156280220
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: simplecov
|
60
|
+
requirement: &2156279280 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *2156279280
|
69
|
+
description: A simple cron-like scheduler for Ruby
|
70
|
+
email: junegunn.c@gmail.com
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files:
|
74
|
+
- LICENSE.txt
|
75
|
+
- README.rdoc
|
76
|
+
files:
|
77
|
+
- .document
|
78
|
+
- CHANGELOG.rdoc
|
79
|
+
- Gemfile
|
80
|
+
- Gemfile.lock
|
81
|
+
- LICENSE.txt
|
82
|
+
- README.rdoc
|
83
|
+
- Rakefile
|
84
|
+
- VERSION
|
85
|
+
- lib/rcron.rb
|
86
|
+
- lib/rcron/alarm.rb
|
87
|
+
- lib/rcron/parser.rb
|
88
|
+
- lib/rcron/scheduler.rb
|
89
|
+
- lib/rcron/task.rb
|
90
|
+
- lib/rcron/timeout.rb
|
91
|
+
- test/helper.rb
|
92
|
+
- test/test_rcron.rb
|
93
|
+
- test/test_rcron_parser.rb
|
94
|
+
- test/test_rcron_task.rb
|
95
|
+
homepage: http://github.com/junegunn/rcron
|
96
|
+
licenses:
|
97
|
+
- MIT
|
98
|
+
post_install_message:
|
99
|
+
rdoc_options: []
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ! '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
segments:
|
109
|
+
- 0
|
110
|
+
hash: -1983967147733122663
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
none: false
|
113
|
+
requirements:
|
114
|
+
- - ! '>='
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
requirements: []
|
118
|
+
rubyforge_project:
|
119
|
+
rubygems_version: 1.8.6
|
120
|
+
signing_key:
|
121
|
+
specification_version: 3
|
122
|
+
summary: A simple cron-like scheduler
|
123
|
+
test_files: []
|