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.
@@ -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