chrono_trigger 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,36 @@
1
+ module ChronoTrigger
2
+
3
+ class ConfigurationException < Exception; end
4
+
5
+ class Shell
6
+
7
+ DEFAULT_TRIGGERS = "lib/triggers/*.rb"
8
+ #Load triggers defined in the trigger files by evaluating them in the context of this Shell instance.
9
+ def load_triggers(files = Dir.glob("#{DEFAULT_TRIGGERS}"))
10
+ files.each { |file| self.instance_eval(File.read(file), file) }
11
+ end
12
+
13
+ #Instantiate a trigger and evaluate the passed in block in the context of the trigger.
14
+ #This is the initial method call when setting up a configuration using the DSL.
15
+ def trigger(name, &block)
16
+ raise ConfigurationException.new("No configuration specified for trigger #{name}") unless block_given?
17
+
18
+ trigger = Trigger.new(name)
19
+ trigger.instance_eval(&block)
20
+
21
+ triggers << trigger
22
+ trigger
23
+ end
24
+
25
+ #Run execute on any trigger who's cron entry matches the current time.
26
+ def execute_triggers
27
+ now = Time.now
28
+ triggers.map {|trigger| trigger.execute_on_match(now)}
29
+ end
30
+
31
+ def triggers
32
+ @triggers ||= []
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ Dir[File.join(File.dirname(__FILE__), '..', 'tasks', '*.rake')].each do |f|
2
+ load f
3
+ end
@@ -0,0 +1,127 @@
1
+ module ChronoTrigger
2
+ class Trigger
3
+
4
+ attr_accessor :name
5
+
6
+ def initialize(name)
7
+ self.name = name
8
+ end
9
+
10
+ #Define the code to be run when the cron job is ready to be executed.
11
+ def runs(&block)
12
+ @exec_block = block
13
+ end
14
+
15
+ #Specify what days the task should run on.
16
+ #Values are :monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday
17
+ def on(*days)
18
+ cron_entry.set_days(days)
19
+ end
20
+
21
+ #Specify what calendar day the task should run on; think monthly jobs.
22
+ #Values are 1-31
23
+ def monthly_on(*calendar_day)
24
+ cron_entry.set_calendar_days(calendar_day)
25
+ end
26
+
27
+ #Specify what hours and minutes the task should run at
28
+ #(e.g. :minute=>10, :hour=>3 or :minute=>[10,20,30], :hour=>[1,2,3])
29
+ def at(options={})
30
+ validate_hours_or_minutes!(options)
31
+
32
+ if hour = (options[:hour] || options[:hours])
33
+ cron_entry.set_hours(hour)
34
+ end
35
+
36
+ if minute = (options[:minute] || options[:minutes])
37
+ cron_entry.set_minutes(minute)
38
+ end
39
+ end
40
+
41
+ #Specify a repeating interval of hours and minutes to run.
42
+ #Specifying minutes not divisible by 60 result in an exception, use #at instead.
43
+ def every(options={})
44
+ validate_hours_or_minutes!(options)
45
+ if minutes = (options[:minutes] || options[:minute])
46
+ cron_entry.set_minutes(extract_minutes_for_every(minutes))
47
+ end
48
+
49
+ if hours = (options[:hours] || options[:hour])
50
+ cron_entry.set_hours(extract_hours_for_every(hours))
51
+ end
52
+ end
53
+
54
+ def dates
55
+ @dates ||= []
56
+ end
57
+
58
+ #Execute this Trigger's code block if the datetime param matches this Trigger's cron entry.
59
+ def execute_on_match(datetime)
60
+ self.execute if cron_entry.matches?(datetime)
61
+ end
62
+
63
+ def execute
64
+ defined?(ActiveRecord) ? execute_with_active_record : execute_without_active_record
65
+ end
66
+
67
+
68
+ private
69
+ def cron_entry
70
+ @cron_entry ||= CronEntry.new
71
+ end
72
+
73
+ #Raise an exception unless minutes and hours are set.
74
+ def validate_hours_or_minutes!(options={})
75
+ unless (options[:hours] || options[:hour]) || (options[:minutes] || options[:minute])
76
+ raise ChronoTrigger::ConfigurationException.new("Hours or minutes not specified in call to 'at' or 'every' method.")
77
+ end
78
+ end
79
+
80
+ def extract_minutes_for_every(minutes)
81
+ extract_for_every(minutes, 60)
82
+ end
83
+
84
+ def extract_hours_for_every(hours)
85
+ extract_for_every(hours, 24)
86
+ end
87
+
88
+ #Extract an array of integers representing the minutes or hours a task should be run at.
89
+ #Raise an exception if time_value is not evenly divisible by base.
90
+ def extract_for_every(time_value, base)
91
+ unless (base % time_value == 0)
92
+ raise ChronoTrigger::ConfigurationException.new("#{time_value} is not evenly divisible by #{base}. Consider using #at instead.")
93
+ end
94
+
95
+ (0...base).select {|num| num % time_value == 0}
96
+ end
97
+
98
+ # When ActiveRecord is defined, attempt to rescue ConnectionNotEstablished errors once,
99
+ # and reestablish the connection to the database. If this fails, normal exception logging will take place.
100
+ #
101
+ def execute_with_active_record
102
+ begin
103
+ @exec_block.call
104
+ rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::StatementInvalid
105
+ ActiveRecord::Base.connection.reconnect!
106
+ execute_without_active_record
107
+ rescue Exception
108
+ log_exception
109
+ end
110
+ end
111
+
112
+ # Execute the execution block and log all exceptions.
113
+ #
114
+ def execute_without_active_record
115
+ begin
116
+ @exec_block.call
117
+ rescue Exception
118
+ log_exception
119
+ end
120
+ end
121
+
122
+ def log_exception
123
+ STDERR.puts "Exception #{$!.inspect} caught in Trigger##{self.name}. Backtrace:"
124
+ STDERR.puts $!.backtrace
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,14 @@
1
+ namespace :chrono_trigger do
2
+
3
+ run_task_name = defined?(RAILS_ROOT) ? {:run => :environment} : :run
4
+ desc "Execute all triggers in loop, sleeping 1 minute between checks."
5
+ task run_task_name do
6
+ shell = ChronoTrigger::Shell.new
7
+ shell.load_triggers
8
+ loop do
9
+ shell.execute_triggers
10
+ sleep 1.minute.to_i
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,31 @@
1
+ trigger "trigger1" do
2
+ runs { puts "trigger 1 runs every 1 minutes; executed at #{Time.now}" }
3
+ every :minutes=>1
4
+ end
5
+
6
+ trigger "exception trigger" do
7
+ runs { raise Exception.new("test exception")}
8
+ every :minutes=>2
9
+ end
10
+
11
+ trigger "trigger2" do
12
+ runs { puts "trigger 2 runs every 5 minutes; executed at #{Time.now}"}
13
+ every :minutes=>5
14
+ end
15
+
16
+ trigger "trigger3" do
17
+ runs { puts "trigger 3 runs at 9:48; executed at #{Time.now}"}
18
+ at :hour=>9, :minute=>48
19
+ end
20
+
21
+ trigger "trigger4" do
22
+ runs { puts "trigger 4 runs on monday at 9:53 and 9:56; executed at #{Time.now}"}
23
+ on :monday
24
+ at :hour=>9, :minute=>[53, 56]
25
+ end
26
+
27
+ trigger "trigger5" do
28
+ runs { puts "trigger 5 runs on thursday at 9:58/59; executed at #{Time.now}"}
29
+ on :thursday
30
+ at :hour=>9, :minute=>[58, 59]
31
+ end
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # File: script/console
3
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
4
+
5
+ libs = " -r irb/completion"
6
+ # Perhaps use a console_lib to store any extra methods I may want available in the cosole
7
+ # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
8
+ libs << " -r #{File.dirname(__FILE__) + '/../lib/chrono_trigger.rb'}"
9
+ puts "Loading chrono_trigger gem"
10
+ exec "#{irb} #{libs} --simple-prompt"
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/generate'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Generate.new.run(ARGV)
@@ -0,0 +1,11 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class TestChronoTrigger < Test::Unit::TestCase
4
+
5
+ def setup
6
+ end
7
+
8
+ def test_truth
9
+ assert true
10
+ end
11
+ end
@@ -0,0 +1,198 @@
1
+ class TestCronEntry < Test::Unit::TestCase
2
+
3
+ class << self
4
+
5
+ def should_match(options={})
6
+ context "and a datetime of #{options.inspect}" do
7
+ setup do
8
+ @datetime = time_from_options(options)
9
+ end
10
+
11
+ should "return true on a call to matches?" do
12
+ assert @cron.matches?(@datetime)
13
+ end
14
+ end #and a datetime of options[]
15
+ end
16
+
17
+ def should_not_match(options={})
18
+ context "and a datetime of #{options.inspect}" do
19
+ setup do
20
+ @datetime = time_from_options(options)
21
+ end
22
+
23
+ should "return false on a call to matches?" do
24
+ assert !@cron.matches?(@datetime)
25
+ end
26
+ end #and a datetime of options[]
27
+ end
28
+
29
+ end
30
+
31
+ context "A CronEntry, @cron," do
32
+ setup do
33
+ @cron = ChronoTrigger::CronEntry.new
34
+ end
35
+
36
+ context "with a minutes entry of 10 minutes" do
37
+ setup do
38
+ @cron.set_minutes(10)
39
+ end
40
+
41
+ should_match(:minutes=>10)
42
+
43
+ should_not_match(:minutes=>11)
44
+
45
+ context "and 25 minutes" do
46
+ setup do
47
+ @cron.set_minutes(10, 25)
48
+ end
49
+
50
+ should_match(:minutes=>25)
51
+
52
+ should_match(:minutes=>10)
53
+
54
+ should_not_match(:minutes=>12)
55
+
56
+ context "and a day entry of monday" do
57
+ setup do
58
+ @cron.set_days(:monday)
59
+ end
60
+
61
+ should_match(:minutes=>10, :wday=>1)
62
+
63
+ should_not_match(:minutes=>10, :wday=>2)
64
+
65
+ context "and wednesday" do
66
+ setup do
67
+ @cron.set_days(:monday, :wednesday)
68
+ end
69
+
70
+ should_match(:minutes=>10, :wday=>1)
71
+
72
+ should_match(:minutes=>10, :wday=>3)
73
+
74
+ should_not_match(:minutes=>25, :wday=>5)
75
+
76
+ should_not_match(:minutes=>11, :wday=>3)
77
+
78
+ context "and an hour entry of 2" do
79
+ setup do
80
+ @cron.set_hours(5)
81
+ end
82
+
83
+ should_match(:minutes=>25, :wday=>3, :hour=>5)
84
+
85
+ should_not_match(:minutes=>25, :wday=>3, :hour=>6)
86
+ end #and an hour entry of 2
87
+ end #and wednesday
88
+ end #and a day entry of monday
89
+ end #and 25 minutes
90
+ end #with a minutes entry of 10 minutes
91
+
92
+ context "with a day entry of monday" do
93
+ setup do
94
+ @cron.set_days(:monday)
95
+ end
96
+
97
+ context "and no minutes_entry" do
98
+ setup do
99
+ @cron.set_minutes(nil)
100
+ end
101
+
102
+ should "raise a ChronoTrigger::CronEntry:ConfigException exception on matches?" do
103
+ assert_raise ChronoTrigger::ConfigurationException do
104
+ @cron.matches?(time_from_options)
105
+ end
106
+ end
107
+ end #and no minutes_entry
108
+ end #with a day entry
109
+
110
+ context "with a calendar_day entry of 25" do
111
+ setup do
112
+ @cron.set_calendar_days(25)
113
+ end
114
+
115
+ should "raise an exception when setting a calendar_day and no hour and minutes" do
116
+ assert_raise ChronoTrigger::ConfigurationException do
117
+ @cron.matches?(time_from_options(:day => 25))
118
+ end
119
+ end
120
+
121
+ context "with a hour entry of 10" do
122
+ setup do
123
+ @cron.set_hours(10)
124
+ end
125
+
126
+ should "raise an exception when setting a calendar_day and hour but no minutes" do
127
+ assert_raise ChronoTrigger::ConfigurationException do
128
+ @cron.matches?(time_from_options(:hour=> 10, :day => 25))
129
+ end
130
+ end
131
+
132
+ context "with a minutes entry of 5" do
133
+ setup do
134
+ @cron.set_minutes(5)
135
+ end
136
+
137
+ should_match(:minutes => 5, :hour => 10, :day => 25)
138
+ should_not_match(:minutes => 4, :hour => 10, :day => 25)
139
+ should_not_match(:minutes => 5, :hour => 11, :day => 25)
140
+ should_not_match(:minutes => 5, :hour => 10, :day => 26)
141
+
142
+ context "with an additional calendar_day entry of 26" do
143
+ setup do
144
+ @cron.set_calendar_days([25, 26])
145
+ end
146
+
147
+ should_match(:minutes => 5, :hour => 10, :day => 25)
148
+ should_match(:minutes => 5, :hour => 10, :day => 26)
149
+
150
+ should "raise an exception when setting a calendar_day is outside the acceptable range" do
151
+ assert_raise ChronoTrigger::ConfigurationException do
152
+ @cron.set_calendar_days(-1)
153
+ end
154
+ end
155
+ end
156
+
157
+ context "with a day entry of tuesday" do
158
+ setup do
159
+ @cron.set_days(:wednesday)
160
+ end
161
+
162
+ should "raise an exception when setting a calendar_day with a day" do
163
+ assert_raise ChronoTrigger::ConfigurationException do
164
+ @cron.matches?(time_from_options(:minutes => 5, :hour => 10, :day => 25, :wday => 3))
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ should "raise an exception when setting an hour entry greater than 25" do
173
+ assert_raise ChronoTrigger::ConfigurationException do
174
+ @cron.set_hours(25)
175
+ end
176
+ end
177
+ end #A CronEntry, @cron,
178
+
179
+
180
+ private
181
+ def time_from_options(options={})
182
+ datetime = Time.utc(options[:year] || 2000,
183
+ options[:month] || "jan",
184
+ options[:day]||1,
185
+ options[:hour]||0,
186
+ options[:minutes]||0,
187
+ options[:second]||0)
188
+
189
+ if wday = options[:wday]
190
+ while datetime.wday != wday
191
+ datetime += 1.day
192
+ end
193
+ end
194
+
195
+ datetime
196
+ end
197
+
198
+ end