recurrent 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,7 @@
1
+ .DS_Store
2
+ coverage
3
+ .rvmrc
4
+ Gemfile.lock
5
+ .bundle
6
+ *.gem
7
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
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.
File without changes
@@ -0,0 +1,5 @@
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+
4
+ Bundler::GemHelper.install_tasks
5
+ RSpec::Core::RakeTask.new
@@ -0,0 +1 @@
1
+ Autotest.add_discovery { "rspec2" }
@@ -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
@@ -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,3 @@
1
+ module Recurrent
2
+ VERSION = '0.0.1'
3
+ 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
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'recurrent'
5
+ require 'timecop'
6
+ require 'pry'
7
+
@@ -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
@@ -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