recurrent 0.0.1
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/.autotest +13 -0
- data/.gitignore +7 -0
- data/.rspec +2 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.markdown +0 -0
- data/Rakefile +5 -0
- data/autotest/discover.rb +1 -0
- data/bin/recurrent +14 -0
- data/lib/recurrent.rb +14 -0
- data/lib/recurrent/configuration.rb +26 -0
- data/lib/recurrent/ice_cube_extensions.rb +22 -0
- data/lib/recurrent/logger.rb +26 -0
- data/lib/recurrent/scheduler.rb +149 -0
- data/lib/recurrent/task.rb +57 -0
- data/lib/recurrent/version.rb +3 -0
- data/lib/recurrent/worker.rb +101 -0
- data/recurrent.gemspec +28 -0
- data/spec/logger_spec.rb +47 -0
- data/spec/scheduler_spec.rb +310 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/task_spec.rb +191 -0
- data/spec/worker_spec.rb +17 -0
- metadata +221 -0
data/.autotest
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Autotest.add_hook :initialize do |at|
|
2
|
+
at.clear_mappings
|
3
|
+
|
4
|
+
# Run any test that changes
|
5
|
+
at.add_mapping(%r{^spec/.*_spec\.rb$}) do |f, _|
|
6
|
+
[f]
|
7
|
+
end
|
8
|
+
|
9
|
+
# Run tests for any file that changes in lib
|
10
|
+
at.add_mapping(%r{^lib/recurrent((/[^/]+)+)\.rb$}) do |_, m|
|
11
|
+
["spec#{m[1]}_spec.rb"]
|
12
|
+
end
|
13
|
+
end
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Zencoder
|
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.markdown
ADDED
File without changes
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Autotest.add_discovery { "rspec2" }
|
data/bin/recurrent
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
$: << File.expand_path("#{File.dirname(__FILE__)}/../lib")
|
2
|
+
require 'rubygems'
|
3
|
+
require 'trollop'
|
4
|
+
begin
|
5
|
+
require 'config/environment'
|
6
|
+
rescue LoadError
|
7
|
+
require 'recurrent'
|
8
|
+
end
|
9
|
+
|
10
|
+
opts = Trollop::options do
|
11
|
+
opt :tasks_file, "File containing task definitions and configuration", :default => 'config/recurrences.rb'
|
12
|
+
end
|
13
|
+
|
14
|
+
Recurrent::Worker.new(opts[:tasks_file]).start
|
data/lib/recurrent.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'ice_cube'
|
2
|
+
begin
|
3
|
+
require 'active_support/time'
|
4
|
+
rescue LoadError
|
5
|
+
require 'active_support'
|
6
|
+
end
|
7
|
+
require 'recurrent/ice_cube_extensions'
|
8
|
+
|
9
|
+
require 'recurrent/configuration'
|
10
|
+
require 'recurrent/logger'
|
11
|
+
require 'recurrent/scheduler'
|
12
|
+
require 'recurrent/task'
|
13
|
+
require 'recurrent/version'
|
14
|
+
require 'recurrent/worker'
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Recurrent
|
2
|
+
class Configuration
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
attr_accessor :logging, :wait_for_running_tasks_on_exit_for
|
7
|
+
|
8
|
+
def self.block_accessor(*fields)
|
9
|
+
fields.each do |field|
|
10
|
+
attr_writer field
|
11
|
+
eval("
|
12
|
+
def #{field}
|
13
|
+
if block_given?
|
14
|
+
@#{field} = Proc.new
|
15
|
+
else
|
16
|
+
@#{field}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
block_accessor :logger, :save_task_schedule, :load_task_schedule, :save_task_return_value, :process_locking, :handle_slow_task
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module IceCube
|
2
|
+
class Rule
|
3
|
+
def frequency_in_seconds
|
4
|
+
rule_type = self.class
|
5
|
+
if rule_type == IceCube::YearlyRule
|
6
|
+
@interval.years
|
7
|
+
elsif rule_type == IceCube::MonthlyRule
|
8
|
+
@interval.months
|
9
|
+
elsif rule_type == IceCube::WeeklyRule
|
10
|
+
@interval.weeks
|
11
|
+
elsif rule_type == IceCube::DailyRule
|
12
|
+
@interval.days
|
13
|
+
elsif rule_type == IceCube::HourlyRule
|
14
|
+
@interval.hours
|
15
|
+
elsif rule_type == IceCube::MinutelyRule
|
16
|
+
@interval.minutes
|
17
|
+
elsif rule_type == IceCube::SecondlyRule
|
18
|
+
@interval.seconds
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Recurrent
|
2
|
+
class Logger
|
3
|
+
|
4
|
+
attr_reader :identifier
|
5
|
+
|
6
|
+
def initialize(identifier)
|
7
|
+
@identifier = identifier
|
8
|
+
end
|
9
|
+
|
10
|
+
def log_message(message)
|
11
|
+
"[Recurrent - Process:#{@identifier} - Timestamp:#{Time.now.to_s(:seconds)}] - #{message}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.define_log_levels(*log_levels)
|
15
|
+
log_levels.each do |log_level|
|
16
|
+
define_method(log_level) do |message|
|
17
|
+
message = log_message(message)
|
18
|
+
puts message unless Configuration.logging == "quiet"
|
19
|
+
Configuration.logger.call(message, log_level) if Configuration.logger
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
define_log_levels :info, :debug, :warn
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module Recurrent
|
2
|
+
class Scheduler
|
3
|
+
|
4
|
+
attr_accessor :tasks, :logger
|
5
|
+
|
6
|
+
def initialize(task_file=nil)
|
7
|
+
@tasks = []
|
8
|
+
identifier = "host:#{Socket.gethostname} pid:#{Process.pid}" rescue "pid:#{Process.pid}"
|
9
|
+
@logger = Logger.new(identifier)
|
10
|
+
eval(File.read(task_file)) if task_file
|
11
|
+
end
|
12
|
+
|
13
|
+
def configure
|
14
|
+
Configuration
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_rule_from_frequency(frequency)
|
18
|
+
logger.info "| Creating an IceCube Rule"
|
19
|
+
if yearly?(frequency)
|
20
|
+
logger.info "| Creating a yearly rule"
|
21
|
+
IceCube::Rule.yearly(frequency / 1.year)
|
22
|
+
elsif monthly?(frequency)
|
23
|
+
logger.info "| Creating a monthly rule"
|
24
|
+
IceCube::Rule.monthly(frequency / 1.month)
|
25
|
+
elsif weekly?(frequency)
|
26
|
+
logger.info "| Creating a weekly rule"
|
27
|
+
IceCube::Rule.weekly(frequency / 1.week)
|
28
|
+
elsif daily?(frequency)
|
29
|
+
logger.info "| Creating a daily rule"
|
30
|
+
IceCube::Rule.daily(frequency / 1.day)
|
31
|
+
elsif hourly?(frequency)
|
32
|
+
logger.info "| Creating an hourly rule"
|
33
|
+
IceCube::Rule.hourly(frequency / 1.hour)
|
34
|
+
elsif minutely?(frequency)
|
35
|
+
logger.info "| Creating a minutely rule"
|
36
|
+
IceCube::Rule.minutely(frequency / 1.minute)
|
37
|
+
else
|
38
|
+
logger.info "| Creating a secondly rule"
|
39
|
+
IceCube::Rule.secondly(frequency)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_schedule(name, frequency, start_time=nil)
|
44
|
+
logger.info "| Creating schedule"
|
45
|
+
if frequency.is_a? IceCube::Rule
|
46
|
+
logger.info "| Frequency is an IceCube Rule: #{frequency.to_s}"
|
47
|
+
rule = frequency
|
48
|
+
frequency_in_seconds = rule.frequency_in_seconds
|
49
|
+
else
|
50
|
+
logger.info "| Frequency is an integer: #{frequency}"
|
51
|
+
rule = create_rule_from_frequency(frequency)
|
52
|
+
logger.info "| IceCube Rule created: #{rule.to_s}"
|
53
|
+
frequency_in_seconds = frequency
|
54
|
+
end
|
55
|
+
start_time ||= derive_start_time(name, frequency_in_seconds)
|
56
|
+
schedule = IceCube::Schedule.new(start_time)
|
57
|
+
schedule.add_recurrence_rule rule
|
58
|
+
logger.info "| schedule created"
|
59
|
+
schedule
|
60
|
+
end
|
61
|
+
|
62
|
+
def derive_start_time(name, frequency)
|
63
|
+
logger.info "| No start time provided, deriving one."
|
64
|
+
if Configuration.load_task_schedule
|
65
|
+
logger.info "| Attempting to derive from saved schedule"
|
66
|
+
derive_start_time_from_saved_schedule(name, frequency)
|
67
|
+
else
|
68
|
+
derive_start_time_from_frequency(frequency)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def derive_start_time_from_saved_schedule(name, frequency)
|
73
|
+
saved_schedule = Configuration.load_task_schedule.call(name)
|
74
|
+
if saved_schedule
|
75
|
+
logger.info "| Saved schedule found"
|
76
|
+
if saved_schedule.rrules.first.frequency_in_seconds == frequency
|
77
|
+
logger.info "| Saved schedule frequency matches, setting start time to saved schedules next occurrence: #{saved_schedule.next_occurrence.to_s(:seconds)}"
|
78
|
+
saved_schedule.next_occurrence
|
79
|
+
else
|
80
|
+
logger.info "| Schedule frequency does not match saved schedule frequency"
|
81
|
+
derive_start_time_from_frequency(frequency)
|
82
|
+
end
|
83
|
+
else
|
84
|
+
derive_start_time_from_frequency(frequency)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def derive_start_time_from_frequency(frequency)
|
89
|
+
logger.info "| Deriving start time from frequency"
|
90
|
+
current_time = Time.now
|
91
|
+
if frequency < 1.minute
|
92
|
+
logger.info "| Setting start time to beginning of current minute"
|
93
|
+
current_time.change(:sec => 0, :usec => 0)
|
94
|
+
elsif frequency < 1.hour
|
95
|
+
logger.info "| Setting start time to beginning of current hour"
|
96
|
+
current_time.change(:min => 0, :sec => 0, :usec => 0)
|
97
|
+
elsif frequency < 1.day
|
98
|
+
logger.info "| Setting start time to beginning of current day"
|
99
|
+
current_time.beginning_of_day
|
100
|
+
elsif frequency < 1.week
|
101
|
+
logger.info "| Setting start time to beginning of current week"
|
102
|
+
current_time.beginning_of_week
|
103
|
+
elsif frequency < 1.month
|
104
|
+
logger.info "| Setting start time to beginning of current month"
|
105
|
+
current_time.beginning_of_month
|
106
|
+
elsif frequency < 1.year
|
107
|
+
logger.info "| Setting start time to beginning of current year"
|
108
|
+
current_time.beginning_of_year
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def every(frequency, key, options={}, &block)
|
113
|
+
logger.info "Adding Task: #{key}"
|
114
|
+
@tasks << Task.new(:name => key,
|
115
|
+
:schedule => create_schedule(key, frequency, options[:start_time]),
|
116
|
+
:action => block,
|
117
|
+
:save => options[:save],
|
118
|
+
:logger => logger)
|
119
|
+
logger.info "| #{key} added to Scheduler"
|
120
|
+
end
|
121
|
+
|
122
|
+
def next_task_time
|
123
|
+
tasks.map { |task| task.next_occurrence }.sort.first
|
124
|
+
end
|
125
|
+
|
126
|
+
def running_tasks
|
127
|
+
tasks.select do |task|
|
128
|
+
task.running?
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def tasks_at_time(time)
|
133
|
+
tasks.select do |task|
|
134
|
+
task.next_occurrence == time
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.define_frequencies(*frequencies)
|
139
|
+
frequencies.each do |frequency|
|
140
|
+
method_name = frequency == :day ? :daily? : :"#{frequency}ly?"
|
141
|
+
define_method(method_name) do |number|
|
142
|
+
(number % 1.send(frequency)) == 0
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
define_frequencies :year, :month, :week, :day, :hour, :minute, :second
|
147
|
+
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Recurrent
|
2
|
+
class Task
|
3
|
+
attr_accessor :action, :name, :logger, :save, :schedule, :thread
|
4
|
+
|
5
|
+
def initialize(options={})
|
6
|
+
@name = options[:name]
|
7
|
+
@schedule = options[:schedule]
|
8
|
+
@action = options[:action]
|
9
|
+
@save = options[:save]
|
10
|
+
@logger = options[:logger]
|
11
|
+
Configuration.save_task_schedule.call(name, schedule) if Configuration.save_task_schedule
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(execution_time)
|
15
|
+
return handle_still_running(execution_time) if running?
|
16
|
+
@thread = Thread.new do
|
17
|
+
Thread.current["execution_time"] = execution_time
|
18
|
+
return_value = action.call
|
19
|
+
save_results(return_value) if save?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def handle_still_running(current_time)
|
24
|
+
logger.info "#{name}: Execution from #{thread['execution_time'].to_s(:seconds)} still running, aborting this execution."
|
25
|
+
if Configuration.handle_slow_task
|
26
|
+
Configuration.handle_slow_task.call(name, current_time)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def next_occurrence
|
31
|
+
return @next_occurrence if @next_occurrence && @next_occurrence.future?
|
32
|
+
@next_occurrence = schedule.next_occurrence
|
33
|
+
end
|
34
|
+
|
35
|
+
def save?
|
36
|
+
!!save
|
37
|
+
end
|
38
|
+
|
39
|
+
def save_results(return_value)
|
40
|
+
logger.info "#{name}: Wants to save its return value."
|
41
|
+
if Configuration.save_task_return_value
|
42
|
+
Configuration.save_task_return_value.call(:name => name,
|
43
|
+
:return_value => return_value,
|
44
|
+
:executed_at => thread['execution_time'],
|
45
|
+
:executed_by => logger.identifier)
|
46
|
+
logger.info "#{name}: Return value saved."
|
47
|
+
else
|
48
|
+
logger.info "#{name}: No method to save return values is configured."
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def running?
|
53
|
+
thread.try(:alive?)
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Recurrent
|
2
|
+
class Worker
|
3
|
+
|
4
|
+
attr_accessor :scheduler, :logger
|
5
|
+
|
6
|
+
def initialize(task_file=nil)
|
7
|
+
@scheduler = Scheduler.new(task_file)
|
8
|
+
@logger = scheduler.logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def start
|
12
|
+
logger.info "Starting Recurrent"
|
13
|
+
|
14
|
+
trap('TERM') { logger.info 'Waiting for running tasks and exiting...'; $exit = true }
|
15
|
+
trap('INT') { logger.info 'Waiting for running tasks and exiting...'; $exit = true }
|
16
|
+
trap('QUIT') { logger.info 'Waiting for running tasks and exiting...'; $exit = true }
|
17
|
+
|
18
|
+
if Configuration.process_locking
|
19
|
+
execute_with_locking
|
20
|
+
else
|
21
|
+
execute
|
22
|
+
end
|
23
|
+
|
24
|
+
logger.info("Goodbye.")
|
25
|
+
end
|
26
|
+
|
27
|
+
def execute
|
28
|
+
loop do
|
29
|
+
execution_time = scheduler.next_task_time
|
30
|
+
tasks_to_execute = scheduler.tasks_at_time(execution_time)
|
31
|
+
|
32
|
+
wait_for_running_tasks && break if $exit
|
33
|
+
|
34
|
+
wait_until(execution_time)
|
35
|
+
|
36
|
+
wait_for_running_tasks && break if $exit
|
37
|
+
|
38
|
+
tasks_to_execute.each do |task|
|
39
|
+
logger.info "#{task.name}: Executing at #{execution_time.to_s(:seconds)}"
|
40
|
+
task.execute(execution_time)
|
41
|
+
end
|
42
|
+
|
43
|
+
wait_for_running_tasks && break if $exit
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def execute_with_locking
|
48
|
+
lock_established = nil
|
49
|
+
until lock_established
|
50
|
+
break if $exit
|
51
|
+
lock_established = Configuration.process_locking.call('recurrent') do
|
52
|
+
execute
|
53
|
+
end
|
54
|
+
break if $exit
|
55
|
+
logger.info 'Tasks are being monitored by another process. Standing by.'
|
56
|
+
sleep(5)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def wait_for_running_tasks
|
61
|
+
if Configuration.wait_for_running_tasks_on_exit_for
|
62
|
+
wait_for_running_tasks_for(Configuration.wait_for_running_tasks_on_exit_for)
|
63
|
+
else
|
64
|
+
wait_for_running_tasks_indefinitely
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def wait_for_running_tasks_for(seconds)
|
69
|
+
while scheduler.running_tasks.any? do
|
70
|
+
logger.info "Killing running tasks in #{seconds.inspect}."
|
71
|
+
seconds -= 1
|
72
|
+
sleep(1)
|
73
|
+
if seconds == 0
|
74
|
+
scheduler.running_tasks.each do |task|
|
75
|
+
logger.info "Killing #{task.name}."
|
76
|
+
task.thread = nil unless task.thread.try(:kill).try(:alive?)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
true
|
81
|
+
end
|
82
|
+
|
83
|
+
def wait_for_running_tasks_indefinitely
|
84
|
+
if task = scheduler.running_tasks.first
|
85
|
+
logger.info "Waiting for #{task.name} to finish."
|
86
|
+
task.thread.try(:join)
|
87
|
+
wait_for_running_tasks_indefinitely
|
88
|
+
else
|
89
|
+
logger.info "All tasks finished, exiting..."
|
90
|
+
true
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def wait_until(time)
|
95
|
+
until time.past?
|
96
|
+
sleep(0.5) unless $exit
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
data/recurrent.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require 'recurrent/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "recurrent"
|
7
|
+
s.version = Recurrent::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Adam Kittelson"]
|
10
|
+
s.email = ["adam@zencoder.com"]
|
11
|
+
s.homepage = "http://github.com/zencoder/recurrent"
|
12
|
+
s.summary = "Task scheduler that doesn't need to bootstrap your Rails environment every time it executes a task the way running a rake task via cron does."
|
13
|
+
s.description = "Task scheduler that doesn't need to bootstrap your Rails environment every time it executes a task the way running a rake task via cron does."
|
14
|
+
|
15
|
+
s.add_dependency "ice_cube", "0.6.8"
|
16
|
+
s.add_dependency "activesupport"
|
17
|
+
s.add_dependency "i18n"
|
18
|
+
s.add_dependency "trollop"
|
19
|
+
s.add_development_dependency "rspec"
|
20
|
+
s.add_development_dependency "autotest"
|
21
|
+
s.add_development_dependency "timecop"
|
22
|
+
s.add_development_dependency "pry"
|
23
|
+
s.add_development_dependency "pry-doc"
|
24
|
+
s.executables << "recurrent"
|
25
|
+
s.files = `git ls-files`.split("\n")
|
26
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
27
|
+
s.require_path = ["lib"]
|
28
|
+
end
|
data/spec/logger_spec.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Recurrent
|
4
|
+
describe Logger do
|
5
|
+
before(:each) do
|
6
|
+
@logger = Logger.new('logtastic')
|
7
|
+
@users_logger = stub('logger')
|
8
|
+
Configuration.logger do |message, log_level|
|
9
|
+
@users_logger.info(message) if log_level == :info
|
10
|
+
@users_logger.debug(message) if log_level == :debug
|
11
|
+
@users_logger.warn(message) if log_level == :warn
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
after(:all) do
|
16
|
+
Configuration.logger = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#info" do
|
20
|
+
it "should send a message to the logger with the info logging level" do
|
21
|
+
@users_logger.should_receive(:info).with(@logger.log_message("testing logger"))
|
22
|
+
@logger.info("testing logger")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#debug" do
|
27
|
+
it "should send a message to the logger with the debug logging level" do
|
28
|
+
@users_logger.should_receive(:debug).with(@logger.log_message("testing logger"))
|
29
|
+
@logger.debug("testing logger")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#warn" do
|
34
|
+
it "should send a message to the logger with the info logging level" do
|
35
|
+
@users_logger.should_receive(:warn).with(@logger.log_message("testing logger"))
|
36
|
+
@logger.warn("testing logger")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#log_message" do
|
41
|
+
it "adds the scheduler's identifier to the message" do
|
42
|
+
@logger.log_message("testing").should == "[Recurrent - Process:logtastic - Timestamp:#{Time.now.to_s(:seconds)}] - testing"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,310 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Recurrent
|
4
|
+
describe Scheduler do
|
5
|
+
before(:all) do
|
6
|
+
Configuration.logging = "quiet"
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "schedule creation methods" do
|
10
|
+
before(:all) do
|
11
|
+
@scheduler = Scheduler.new
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#create_rule_from_frequency" do
|
15
|
+
context "when the frequency is in years" do
|
16
|
+
it "should create a yearly rule" do
|
17
|
+
@scheduler.create_rule_from_frequency(2.years).class.should == IceCube::YearlyRule
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context "when the frequency is in months" do
|
22
|
+
it "should create a yearly rule" do
|
23
|
+
@scheduler.create_rule_from_frequency(3.months).class.should == IceCube::MonthlyRule
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context "when the frequency is in weeks" do
|
28
|
+
it "should create a weekly rule" do
|
29
|
+
@scheduler.create_rule_from_frequency(2.weeks).class.should == IceCube::WeeklyRule
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "when the frequency is in days" do
|
34
|
+
it "should create a daily rule" do
|
35
|
+
@scheduler.create_rule_from_frequency(3.days).class.should == IceCube::DailyRule
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "when the frequency is in hours" do
|
40
|
+
it "should create an hourly rule" do
|
41
|
+
@scheduler.create_rule_from_frequency(6.hours).class.should == IceCube::HourlyRule
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "when the frequency is in minutes" do
|
46
|
+
it "should create a minutely rule" do
|
47
|
+
@scheduler.create_rule_from_frequency(10.minutes).class.should == IceCube::MinutelyRule
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "when the frequency is in seconds" do
|
52
|
+
it "should create a secondly rule" do
|
53
|
+
@scheduler.create_rule_from_frequency(30.seconds).class.should == IceCube::SecondlyRule
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "create_schedule" do
|
59
|
+
context "when frequency is an IceCube Rule" do
|
60
|
+
subject do
|
61
|
+
rule = IceCube::Rule.daily(1)
|
62
|
+
@scheduler.create_schedule(:test, rule)
|
63
|
+
end
|
64
|
+
it "should be a schedule" do
|
65
|
+
subject.class.should == IceCube::Schedule
|
66
|
+
end
|
67
|
+
it "should have the correct rule" do
|
68
|
+
subject.rrules.first.is_a? IceCube::DailyRule
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "when frequency is a number" do
|
73
|
+
subject do
|
74
|
+
@scheduler.create_schedule(:test, 1.day)
|
75
|
+
end
|
76
|
+
it "should be a schedule" do
|
77
|
+
subject.class.should == IceCube::Schedule
|
78
|
+
end
|
79
|
+
it "should have the correct rule" do
|
80
|
+
subject.rrules.first.is_a? IceCube::DailyRule
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context "when start time is not provided" do
|
85
|
+
it "should derive its own start time" do
|
86
|
+
@scheduler.should_receive(:derive_start_time)
|
87
|
+
@scheduler.create_schedule(:test, 1.day)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context "when start time is provided" do
|
92
|
+
it "should not derive its own start time" do
|
93
|
+
@scheduler.should_not_receive(:derive_start_time)
|
94
|
+
@scheduler.create_schedule(:test, 1.day, Time.now)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "derive_start_time_from_frequency" do
|
100
|
+
context "when the current time is 11:35:12 am on July 26th, 2011" do
|
101
|
+
before(:all) do
|
102
|
+
Timecop.freeze(Time.local(2011, 7, 26, 11, 35, 12))
|
103
|
+
end
|
104
|
+
|
105
|
+
context "and the frequency is less than a minute" do
|
106
|
+
it "should be 11:35:00, the beginning of the current minute" do
|
107
|
+
start_time = @scheduler.derive_start_time_from_frequency(30.seconds)
|
108
|
+
start_time.should == Time.local(2011, 7, 26, 11, 35, 00)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context "and the frequency is less than an hour" do
|
113
|
+
it "should be 11:00:00, the beginning of the current hour" do
|
114
|
+
start_time = @scheduler.derive_start_time_from_frequency(15.minutes)
|
115
|
+
start_time.should == Time.local(2011, 7, 26, 11, 00, 00)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context "and the frequency is less than a day" do
|
120
|
+
it "should be 00:00:00 on July 26th, 2011, the beginning of the current day" do
|
121
|
+
start_time = @scheduler.derive_start_time_from_frequency(3.hours)
|
122
|
+
start_time.should == Time.local(2011, 7, 26, 00, 00, 00)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
context "and the frequency is less than a week" do
|
127
|
+
it "should be 00:00:00 on July 25th, 2011, the beginning of the current week" do
|
128
|
+
start_time = @scheduler.derive_start_time_from_frequency(3.days)
|
129
|
+
start_time.should == Time.local(2011, 7, 25, 00, 00, 00)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context "and the frequency is less than a month" do
|
134
|
+
it "should be 00:00:00 on July 1st, 2011, the beginning of the current month" do
|
135
|
+
start_time = @scheduler.derive_start_time_from_frequency(10.days)
|
136
|
+
start_time.should == Time.local(2011, 7, 01, 00, 00, 00)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context "and the frequency is less than a year" do
|
141
|
+
it "should be 00:00:00 on January 1st, 2011, the beginning of the current year" do
|
142
|
+
start_time = @scheduler.derive_start_time_from_frequency(2.months)
|
143
|
+
start_time.should == Time.local(2011, 1, 01, 00, 00, 00)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
after(:all) do
|
148
|
+
Timecop.return
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe "derive_start_time_from_saved_schedule" do
|
154
|
+
before(:all) do
|
155
|
+
@scheduler = Scheduler.new
|
156
|
+
Configuration.load_task_schedule do |name|
|
157
|
+
current_time = Time.new
|
158
|
+
current_time.change(:sec => 0, :usec => 0)
|
159
|
+
@scheduler.create_schedule(:test, 10.seconds, current_time) if name == :test
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
describe "a schedule being created with a saved schedule with the same name and frequency" do
|
164
|
+
it "derives its start time from the saved schedule" do
|
165
|
+
@scheduler.should_not_receive(:derive_start_time_from_frequency)
|
166
|
+
@scheduler.create_schedule(:test, 10.seconds)
|
167
|
+
end
|
168
|
+
|
169
|
+
describe "the created schedule's start time" do
|
170
|
+
it "should be the next occurrence of the saved schedule" do
|
171
|
+
saved_schedule = Configuration.load_task_schedule.call(:test)
|
172
|
+
created_schedule = @scheduler.create_schedule(:test, 10.seconds)
|
173
|
+
created_schedule.start_date.to_s(:seconds).should == saved_schedule.next_occurrence.to_s(:seconds)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
178
|
+
|
179
|
+
describe "a schedule being created with a saved schedule with the same name and different frequency" do
|
180
|
+
it "derives its start time from the frequency" do
|
181
|
+
@scheduler.should_receive(:derive_start_time_from_frequency)
|
182
|
+
@scheduler.create_schedule(:test, 15.seconds)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
describe "a schedule being created without a saved schedule" do
|
187
|
+
it "derives its start time from the frequency" do
|
188
|
+
@scheduler.should_receive(:derive_start_time_from_frequency)
|
189
|
+
@scheduler.create_schedule(:new_test, 10.seconds)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
after(:all) do
|
195
|
+
Configuration.load_task_schedule = nil;
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
describe "#next_task_time" do
|
201
|
+
context "when there are multiple tasks" do
|
202
|
+
it "should return the soonest time at which a task is scheduled" do
|
203
|
+
task1 = stub('task1')
|
204
|
+
task1.stub(:next_occurrence).and_return(10.minutes.from_now)
|
205
|
+
task2 = stub('task2')
|
206
|
+
task2.stub(:next_occurrence).and_return(5.minutes.from_now)
|
207
|
+
task3 = stub('task3')
|
208
|
+
task3.stub(:next_occurrence).and_return(15.minutes.from_now)
|
209
|
+
schedule = Scheduler.new
|
210
|
+
schedule.tasks << task1
|
211
|
+
schedule.tasks << task2
|
212
|
+
schedule.tasks << task3
|
213
|
+
schedule.next_task_time.should == task2.next_occurrence
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
describe "#tasks_at_time" do
|
219
|
+
context "when there are multiple tasks" do
|
220
|
+
it "should return all the tasks whose next_occurrence is at the specified time" do
|
221
|
+
in_five_minutes = 5.minutes.from_now
|
222
|
+
task1 = stub('task1')
|
223
|
+
task1.stub(:next_occurrence).and_return(in_five_minutes)
|
224
|
+
task2 = stub('task2')
|
225
|
+
task2.stub(:next_occurrence).and_return(10.minutes.from_now)
|
226
|
+
task3 = stub('task3')
|
227
|
+
task3.stub(:next_occurrence).and_return(in_five_minutes)
|
228
|
+
schedule = Scheduler.new
|
229
|
+
schedule.tasks << task1
|
230
|
+
schedule.tasks << task2
|
231
|
+
schedule.tasks << task3
|
232
|
+
schedule.tasks_at_time(in_five_minutes).should =~ [task1, task3]
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
describe "methods created by .define_frequencies" do
|
238
|
+
before :all do
|
239
|
+
@scheduler = Scheduler.new
|
240
|
+
end
|
241
|
+
|
242
|
+
describe "#yearly?" do
|
243
|
+
it "should return true if a frequency is divisible by years" do
|
244
|
+
@scheduler.yearly?(3.years).should == true
|
245
|
+
end
|
246
|
+
|
247
|
+
it "should return false if a frequency is divisible by years" do
|
248
|
+
@scheduler.yearly?(3.days).should == false
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
describe "#monthly?" do
|
253
|
+
it "should return true if a frequency is divisible by months" do
|
254
|
+
@scheduler.monthly?(3.months).should == true
|
255
|
+
end
|
256
|
+
|
257
|
+
it "should return false if a frequency is not divisible by months" do
|
258
|
+
@scheduler.monthly?(3.days).should == false
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
describe "#weekly?" do
|
263
|
+
it "should return true if a frequency is divisible by weeks" do
|
264
|
+
@scheduler.weekly?(3.weeks).should == true
|
265
|
+
end
|
266
|
+
|
267
|
+
it "should return false if a frequency is not divisible by weeks" do
|
268
|
+
@scheduler.weekly?(3.days).should == false
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
describe "#daily?" do
|
273
|
+
it "should return true if a frequency is divisible by days" do
|
274
|
+
@scheduler.daily?(3.days).should == true
|
275
|
+
end
|
276
|
+
|
277
|
+
it "should return false if a frequency is not divisible by days" do
|
278
|
+
@scheduler.daily?(3.hours).should == false
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
describe "#hourly?" do
|
283
|
+
it "should return true if a frequency is divisible by hours" do
|
284
|
+
@scheduler.hourly?(3.hours).should == true
|
285
|
+
end
|
286
|
+
|
287
|
+
it "should return false if a frequency is not divisible by hours" do
|
288
|
+
@scheduler.hourly?(3.minutes).should == false
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
describe "#minutely?" do
|
293
|
+
it "should return true if a frequency is divisible by minutes" do
|
294
|
+
@scheduler.minutely?(3.minutes).should == true
|
295
|
+
end
|
296
|
+
|
297
|
+
it "should return false if a frequency is not divisible by years" do
|
298
|
+
@scheduler.minutely?(3.seconds).should == false
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
describe "#secondly?" do
|
303
|
+
it "should return true if a frequency is divisible by seconds" do
|
304
|
+
@scheduler.secondly?(3.years).should == true
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
end
|
310
|
+
end
|
data/spec/spec_helper.rb
ADDED
data/spec/task_spec.rb
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Recurrent
|
4
|
+
describe Task do
|
5
|
+
before(:all) do
|
6
|
+
Configuration.logging = "quiet"
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "#execute" do
|
10
|
+
before :each do
|
11
|
+
@executing_task_time = 5.minutes.ago
|
12
|
+
@current_time = Time.now
|
13
|
+
@task = Task.new :name => 'execute test', :logger => Logger.new('some identifier')
|
14
|
+
end
|
15
|
+
|
16
|
+
context "The task is still running a previous execution" do
|
17
|
+
before :each do
|
18
|
+
@task.thread = Thread.new { Thread.current["execution_time"] = @executing_task_time; sleep(1) }
|
19
|
+
end
|
20
|
+
|
21
|
+
it "calls #handle_still_running and does not execute the task" do
|
22
|
+
@task.should_receive(:handle_still_running).with(@current_time)
|
23
|
+
Thread.should_not_receive(:new)
|
24
|
+
@task.execute(@current_time)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "doesn't call #handle_still_running" do
|
29
|
+
@task.should_not_receive(:handle_still_running)
|
30
|
+
@task.execute(@current_time)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "creates a thread" do
|
34
|
+
Thread.should_receive(:new)
|
35
|
+
@task.execute(@current_time)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "sets its execution_time" do
|
39
|
+
@task.execute(@current_time)
|
40
|
+
@task.thread['execution_time'].should == @current_time
|
41
|
+
end
|
42
|
+
|
43
|
+
it "calls the action" do
|
44
|
+
@task.action.should_receive(:call)
|
45
|
+
@task.execute(@current_time)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "#next_occurrence" do
|
50
|
+
context "a task that occurs ever 10 seconds and has just occurred" do
|
51
|
+
subject do
|
52
|
+
current_time = Time.new
|
53
|
+
current_time.change(:sec => 0, :usec => 0)
|
54
|
+
Timecop.freeze(current_time)
|
55
|
+
Task.new(:name => :test, :schedule => Scheduler.new.create_schedule(:test, 10.seconds, current_time))
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should occur 10 seconds from now" do
|
59
|
+
subject.next_occurrence.should == 10.seconds.from_now
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should cache its next occurrence while it's still valid" do
|
63
|
+
subject.schedule.should_receive(:next_occurrence).and_return(10.seconds.from_now)
|
64
|
+
subject.next_occurrence
|
65
|
+
subject.schedule.should_not_receive(:next_occurrence)
|
66
|
+
subject.next_occurrence
|
67
|
+
end
|
68
|
+
|
69
|
+
after(:each) do
|
70
|
+
Timecop.return
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "#handle_still_running" do
|
77
|
+
before(:all) do
|
78
|
+
@executing_task_time = 5.minutes.ago
|
79
|
+
@current_time = Time.now
|
80
|
+
@task = Task.new :name => 'handle_still_running_test', :logger => Logger.new('some identifier')
|
81
|
+
@task.thread = Thread.new { Thread.current["execution_time"] = @executing_task_time }
|
82
|
+
end
|
83
|
+
|
84
|
+
context "When no method for handling a still running task is configured" do
|
85
|
+
it "just logs that the task is still running" do
|
86
|
+
@task.logger.should_receive(:info).with("handle_still_running_test: Execution from #{@executing_task_time.to_s(:seconds)} still running, aborting this execution.")
|
87
|
+
@task.handle_still_running(@current_time)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context "When a method for handling a still running task is configured" do
|
92
|
+
before(:each) do
|
93
|
+
Configuration.handle_slow_task { |options| 'testing is fun' }
|
94
|
+
end
|
95
|
+
|
96
|
+
it "logs that the task is still running and calls the method" do
|
97
|
+
@task.logger.should_receive(:info).with("handle_still_running_test: Execution from #{@executing_task_time.to_s(:seconds)} still running, aborting this execution.")
|
98
|
+
Configuration.handle_slow_task.should_receive(:call).with('handle_still_running_test', @current_time)
|
99
|
+
@task.handle_still_running(@current_time)
|
100
|
+
end
|
101
|
+
|
102
|
+
after(:each) do
|
103
|
+
Configuration.handle_slow_task = nil
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe "#save?" do
|
109
|
+
describe "A task initialized with :save => true" do
|
110
|
+
it "returns true" do
|
111
|
+
Task.new(:save => true).save?.should == true
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe "A task initialized with :save => false" do
|
116
|
+
it "returns false" do
|
117
|
+
Task.new(:save => false).save?.should == false
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "A task initialized with no :save option" do
|
122
|
+
it "returns false" do
|
123
|
+
Task.new.save?.should == false
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe "#save_results" do
|
129
|
+
context "When no method for saving results is configured" do
|
130
|
+
it "logs that information" do
|
131
|
+
t = Task.new :name => 'save_results_test', :logger => Logger.new('some identifier')
|
132
|
+
t.logger.should_receive(:info).with("save_results_test: Wants to save its return value.")
|
133
|
+
t.logger.should_receive(:info).with("save_results_test: No method to save return values is configured.")
|
134
|
+
t.save_results('some value')
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context "When a method for saving results is configured" do
|
139
|
+
before(:each) do
|
140
|
+
Configuration.save_task_return_value = lambda { |options| 'testing is fun'}
|
141
|
+
@task = Task.new :name => 'save_results_test', :logger => Logger.new('some identifier')
|
142
|
+
@current_time = Time.now
|
143
|
+
@task.thread = Thread.new { Thread.current["execution_time"] = @current_time }
|
144
|
+
end
|
145
|
+
|
146
|
+
it "calls the method and logs that the value was saved" do
|
147
|
+
@task.logger.should_receive(:info).with("save_results_test: Wants to save its return value.")
|
148
|
+
Configuration.save_task_return_value.should_receive(:call).with(:name => 'save_results_test',
|
149
|
+
:return_value => 'some value',
|
150
|
+
:executed_at => @current_time,
|
151
|
+
:executed_by => 'some identifier')
|
152
|
+
@task.logger.should_receive(:info).with("save_results_test: Return value saved.")
|
153
|
+
@task.save_results('some value')
|
154
|
+
end
|
155
|
+
|
156
|
+
after(:each) do
|
157
|
+
Configuration.save_task_return_value = nil
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
describe "#running?" do
|
164
|
+
describe "A task with a live thread" do
|
165
|
+
it "returns true" do
|
166
|
+
t = Task.new
|
167
|
+
t.thread = Thread.new { sleep 1 }
|
168
|
+
t.running?.should be_true
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
describe "A task with a dead thread" do
|
173
|
+
it "returns false" do
|
174
|
+
t = Task.new
|
175
|
+
t.thread = Thread.new { sleep 1 }
|
176
|
+
t.thread.kill
|
177
|
+
t.running?.should be_false
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
describe "A task with no thread" do
|
182
|
+
it "returns false" do
|
183
|
+
t = Task.new
|
184
|
+
t.thread = nil
|
185
|
+
t.running?.should be_false
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
data/spec/worker_spec.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Recurrent
|
4
|
+
describe Worker do
|
5
|
+
describe "#wait_until" do
|
6
|
+
it "waits until a specified time" do
|
7
|
+
Timecop.freeze(Time.local(2011, 7, 26, 11, 35, 00))
|
8
|
+
waiting_thread = Thread.new { Worker.new.wait_until(Time.local(2011, 7, 26, 11, 40, 00)) }
|
9
|
+
waiting_thread.alive?.should be_true
|
10
|
+
Timecop.travel(Time.local(2011, 7, 26, 11, 40, 00))
|
11
|
+
sleep(0.5)
|
12
|
+
waiting_thread.alive?.should be_false
|
13
|
+
Timecop.return
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,221 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: recurrent
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Adam Kittelson
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-08-05 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: ice_cube
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - "="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 23
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
- 6
|
33
|
+
- 8
|
34
|
+
version: 0.6.8
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: activesupport
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 3
|
46
|
+
segments:
|
47
|
+
- 0
|
48
|
+
version: "0"
|
49
|
+
type: :runtime
|
50
|
+
version_requirements: *id002
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
name: i18n
|
53
|
+
prerelease: false
|
54
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 3
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
version: "0"
|
63
|
+
type: :runtime
|
64
|
+
version_requirements: *id003
|
65
|
+
- !ruby/object:Gem::Dependency
|
66
|
+
name: trollop
|
67
|
+
prerelease: false
|
68
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
hash: 3
|
74
|
+
segments:
|
75
|
+
- 0
|
76
|
+
version: "0"
|
77
|
+
type: :runtime
|
78
|
+
version_requirements: *id004
|
79
|
+
- !ruby/object:Gem::Dependency
|
80
|
+
name: rspec
|
81
|
+
prerelease: false
|
82
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
hash: 3
|
88
|
+
segments:
|
89
|
+
- 0
|
90
|
+
version: "0"
|
91
|
+
type: :development
|
92
|
+
version_requirements: *id005
|
93
|
+
- !ruby/object:Gem::Dependency
|
94
|
+
name: autotest
|
95
|
+
prerelease: false
|
96
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
hash: 3
|
102
|
+
segments:
|
103
|
+
- 0
|
104
|
+
version: "0"
|
105
|
+
type: :development
|
106
|
+
version_requirements: *id006
|
107
|
+
- !ruby/object:Gem::Dependency
|
108
|
+
name: timecop
|
109
|
+
prerelease: false
|
110
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
hash: 3
|
116
|
+
segments:
|
117
|
+
- 0
|
118
|
+
version: "0"
|
119
|
+
type: :development
|
120
|
+
version_requirements: *id007
|
121
|
+
- !ruby/object:Gem::Dependency
|
122
|
+
name: pry
|
123
|
+
prerelease: false
|
124
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
125
|
+
none: false
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
hash: 3
|
130
|
+
segments:
|
131
|
+
- 0
|
132
|
+
version: "0"
|
133
|
+
type: :development
|
134
|
+
version_requirements: *id008
|
135
|
+
- !ruby/object:Gem::Dependency
|
136
|
+
name: pry-doc
|
137
|
+
prerelease: false
|
138
|
+
requirement: &id009 !ruby/object:Gem::Requirement
|
139
|
+
none: false
|
140
|
+
requirements:
|
141
|
+
- - ">="
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
hash: 3
|
144
|
+
segments:
|
145
|
+
- 0
|
146
|
+
version: "0"
|
147
|
+
type: :development
|
148
|
+
version_requirements: *id009
|
149
|
+
description: Task scheduler that doesn't need to bootstrap your Rails environment every time it executes a task the way running a rake task via cron does.
|
150
|
+
email:
|
151
|
+
- adam@zencoder.com
|
152
|
+
executables:
|
153
|
+
- recurrent
|
154
|
+
extensions: []
|
155
|
+
|
156
|
+
extra_rdoc_files: []
|
157
|
+
|
158
|
+
files:
|
159
|
+
- .autotest
|
160
|
+
- .gitignore
|
161
|
+
- .rspec
|
162
|
+
- Gemfile
|
163
|
+
- LICENSE
|
164
|
+
- README.markdown
|
165
|
+
- Rakefile
|
166
|
+
- autotest/discover.rb
|
167
|
+
- bin/recurrent
|
168
|
+
- lib/recurrent.rb
|
169
|
+
- lib/recurrent/configuration.rb
|
170
|
+
- lib/recurrent/ice_cube_extensions.rb
|
171
|
+
- lib/recurrent/logger.rb
|
172
|
+
- lib/recurrent/scheduler.rb
|
173
|
+
- lib/recurrent/task.rb
|
174
|
+
- lib/recurrent/version.rb
|
175
|
+
- lib/recurrent/worker.rb
|
176
|
+
- recurrent.gemspec
|
177
|
+
- spec/logger_spec.rb
|
178
|
+
- spec/scheduler_spec.rb
|
179
|
+
- spec/spec_helper.rb
|
180
|
+
- spec/task_spec.rb
|
181
|
+
- spec/worker_spec.rb
|
182
|
+
has_rdoc: true
|
183
|
+
homepage: http://github.com/zencoder/recurrent
|
184
|
+
licenses: []
|
185
|
+
|
186
|
+
post_install_message:
|
187
|
+
rdoc_options: []
|
188
|
+
|
189
|
+
require_paths:
|
190
|
+
- - lib
|
191
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
192
|
+
none: false
|
193
|
+
requirements:
|
194
|
+
- - ">="
|
195
|
+
- !ruby/object:Gem::Version
|
196
|
+
hash: 3
|
197
|
+
segments:
|
198
|
+
- 0
|
199
|
+
version: "0"
|
200
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
201
|
+
none: false
|
202
|
+
requirements:
|
203
|
+
- - ">="
|
204
|
+
- !ruby/object:Gem::Version
|
205
|
+
hash: 3
|
206
|
+
segments:
|
207
|
+
- 0
|
208
|
+
version: "0"
|
209
|
+
requirements: []
|
210
|
+
|
211
|
+
rubyforge_project:
|
212
|
+
rubygems_version: 1.3.9.2
|
213
|
+
signing_key:
|
214
|
+
specification_version: 3
|
215
|
+
summary: Task scheduler that doesn't need to bootstrap your Rails environment every time it executes a task the way running a rake task via cron does.
|
216
|
+
test_files:
|
217
|
+
- spec/logger_spec.rb
|
218
|
+
- spec/scheduler_spec.rb
|
219
|
+
- spec/spec_helper.rb
|
220
|
+
- spec/task_spec.rb
|
221
|
+
- spec/worker_spec.rb
|