ultra_marathon 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b8225719b7d1b1c43162a163d33bc7c3ebf03be5
4
+ data.tar.gz: f194c974595a922b4f33d7dc9666e70869e07f98
5
+ SHA512:
6
+ metadata.gz: c945bed22739aeb22407234ac12da19b5fc9a12912f1a4255c8d48e153fa86e36dca07278cbd52bf0525c637f95ede2d053d2d0ab90ae49a4e6d6069266642dd
7
+ data.tar.gz: 183b2985c9366a663139a244d0eee485b012448d383ec4b23c9add579dac13ade4395be7cb4bc8ae85dfa65add94b966936c79153febf542d0e1af774f81e093
data/LICENSE.md ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2013 Chris Maddox
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # UltraMarathon
2
+
3
+ Fault tolerant platform for long running jobs.
4
+
5
+ ## Usage
6
+
7
+ The `UltraMarathon::AbstractRunner` class itself provides the functionality for
8
+ running complex jobs. It is best inheirited to fully customize.
9
+
10
+ ### DSL
11
+
12
+ A simple DSL, currently consisting of only the `run` command, are used to
13
+ specify independent chunks of work. The first argument is the name of the run
14
+ block. Omitted, this defaults to ':main', though names must be unique within a
15
+ given Runner so for a runner with N run blocks, N - 1 must be manually named.
16
+
17
+ ```ruby
18
+ class MailRunner < UltraMarathon::AbstractRunner
19
+
20
+ # :main run block
21
+ run do
22
+ raise 'boom'
23
+ end
24
+
25
+ # :eat run block
26
+ run :eat do
27
+ add_butter
28
+ add_syrup
29
+ eat!
30
+ end
31
+
32
+ # Omitted for brevity
33
+ def add_butter; end
34
+ def add_syrup; end
35
+ def eat!; end
36
+
37
+ end
38
+
39
+ # Run the runner:
40
+ MailRunner.run!
41
+ ```
42
+
43
+ Note that, while the run blocks are defined in the context of the class, they
44
+ will be run within an individual instance. Any methods should be defined as
45
+ instance methods.
46
+
47
+ In this instance, the `eat` run block will still run even if the `main` block is
48
+ executed first. Errors are caught — though they can be evaluated using the
49
+ `on_error` callback, detailed below — and the runner will attempt to complete as
50
+ many run blocks as it is able.
51
+
52
+ ### Dependencies
53
+
54
+ Independent blocks are not guaranteed to run in any order, unless specifying
55
+ dependents using the `:requires` option.
56
+
57
+ ```ruby
58
+ class WalrusRunner < UltraMarathon::AbstractRunner
59
+
60
+ run :bubbles, requires: [:don_scuba_gear] do
61
+ obtain_bubbles
62
+ end
63
+
64
+ run :don_scuba_gear do
65
+ aquire_snorkel
66
+ wear_flippers_on_flippers
67
+ end
68
+
69
+ end
70
+ ```
71
+
72
+ In this instance, `bubbles` will not be run until `don_scuba_gear` successfully
73
+ finishes. If `don_scuba_gear` explicitly fails, such as by raising an error,
74
+ `bubbles` will never be run.
75
+
76
+ ### Callbacks
77
+
78
+ `UltraMarathon::AbstractRunner` includes numerous life-cycle callbacks for
79
+ tangential code execution. Callbacks may be either callable objects
80
+ (procs/lambdas) or symbols of methods defined on the runner.
81
+
82
+ The basic flow of execution is as follows:
83
+
84
+ - `before_run`
85
+ - (`run!`)
86
+ - `after_run`
87
+ - `after_all`
88
+ - (`reset`)
89
+ - `on_reset`
90
+
91
+ If there is an error raised in any run block, any `on_error` callbacks will be
92
+ invoked, passing in the error if the callback takes arguments.
93
+
94
+ ```ruby
95
+ class NewsRunner < UltraMarathon::AbstractRunner
96
+ before_run :fetch_new_yorker_stories
97
+ after_run :get_learning_on
98
+ on_error :contemplate_existence
99
+
100
+ run do
101
+ NewYorker.fetch_rss_feed!
102
+ end
103
+
104
+ private
105
+
106
+ def contemplate_existence(error)
107
+ if error.is_a? HighBrowError
108
+ puts 'Not cultured enough to understand :('
109
+ else
110
+ puts "Error: #{error.message}"
111
+ end
112
+ end
113
+
114
+ end
115
+ ```
116
+
117
+ #### Options
118
+
119
+ Callbacks can, additionally, take a hash of options. Currently `:if` and
120
+ `:unless` are supported. They too can be callable objects or symbols.
121
+
122
+ ```ruby
123
+ class MercurialRunner < UltraMarathon::AbstractRunner
124
+ after_all :celebrate, :if => :success?
125
+ after_all :cry, unless: ->{ success? }
126
+
127
+ run do
128
+ raise 'hell' if rand(2) % 2 == 0
129
+ end
130
+
131
+ end
132
+ ```
133
+
134
+ ### Success, Failure, and Reseting
135
+
136
+ If any part of a runner fails, either by raising an error or explicitly setting
137
+ `self.success = false`, the entire run will be considered a failure. Any run blocks
138
+ which rely on an unsuccessful run block will also be considered failed.
139
+
140
+ A failed runner can be reset, which essentially changes the failed runners to
141
+ being unrun and returns the success flag to true. It will then execute any
142
+ `on_reset` callbacks before returning itself.
143
+
144
+ ```ruby
145
+ class WatRunner < UltraMarathon::AbstractRunner
146
+ after_reset ->{ $global_variable = 42 }
147
+ after_run ->{ puts 'all is well in the universe'}
148
+ run do
149
+ unless $global_variable == 42
150
+ puts 'wrong!'
151
+ raise 'boom'
152
+ end
153
+ end
154
+
155
+ end
156
+
157
+ WatRunner.run!.reset.run!
158
+ #=> boom
159
+ #=> all is well in the universe
160
+ ```
@@ -0,0 +1,204 @@
1
+ require 'ultra_marathon/callbacks'
2
+ require 'ultra_marathon/instrumentation'
3
+ require 'ultra_marathon/logging'
4
+ require 'ultra_marathon/sub_runner'
5
+ require 'ultra_marathon/store'
6
+
7
+ module UltraMarathon
8
+ class AbstractRunner
9
+ include Logging
10
+ include Instrumentation
11
+ include Callbacks
12
+ attr_accessor :success
13
+ callbacks :before_run, :after_run, :after_all, :on_error, :on_reset
14
+
15
+ after_all :write_log
16
+ on_error lambda { self.success = false }
17
+ on_error lambda { |error| logger.error(error) }
18
+
19
+ ## Public Instance Methods
20
+
21
+ # Runs the run block safely in the context of the instance
22
+ def run!
23
+ if self.class.run_blocks.any?
24
+ begin
25
+ self.success = true
26
+ invoke_before_run_callbacks
27
+ instrument { run_unrun_sub_runners }
28
+ self.success = failed_sub_runners.empty?
29
+ invoke_after_run_callbacks
30
+ rescue StandardError => error
31
+ invoke_on_error_callbacks(error)
32
+ ensure
33
+ invoke_after_all_callbacks
34
+ end
35
+ self
36
+ end
37
+ end
38
+
39
+ def success?
40
+ !!success
41
+ end
42
+
43
+ # Resets success to being true, unsets the failed sub_runners to [], and
44
+ # sets the unrun sub_runners to be the uncompleted/failed ones
45
+ def reset
46
+ reset_failed_runners
47
+ @success = true
48
+ invoke_on_reset_callbacks
49
+ self
50
+ end
51
+
52
+ private
53
+
54
+ ## Private Class Methods
55
+
56
+ class << self
57
+
58
+ # This is where the magic happens.
59
+ # Called in the class context, it will be safely executed in
60
+ # the context of the instance.
61
+ #
62
+ # E.g.
63
+ #
64
+ # class BubblesRunner < AbstractRunner
65
+ # run do
66
+ # fire_the_missiles
67
+ # take_a_nap
68
+ # end
69
+ #
70
+ # def fire_the_missiles
71
+ # puts 'But I am le tired'
72
+ # end
73
+ #
74
+ # def take_a_nap
75
+ # puts 'zzzzzz'
76
+ # end
77
+ # end
78
+ #
79
+ # BubblesRunner.new.run!
80
+ # # => 'But I am le tired'
81
+ # # => 'zzzzzz'
82
+ def run(name=:main, options={}, &block)
83
+ name = name.to_sym
84
+ if !run_blocks.key? name
85
+ options[:name] = name
86
+ self.run_blocks[name] = [options, block]
87
+ else
88
+ raise NameError.new("Run block named #{name} already exists!")
89
+ end
90
+ end
91
+
92
+ def run_blocks
93
+ @run_blocks ||= Hash.new
94
+ end
95
+ end
96
+
97
+ ## Private Instance Methods
98
+
99
+ # Memoizes the sub runners based on the run blocks and their included
100
+ # options.
101
+ def unrun_sub_runners
102
+ @unrun_sub_runners ||= begin
103
+ self.class.run_blocks.reduce(Store.new) do |runner_store, (_name, (options, block))|
104
+ runner_store << new_sub_runner(options, block)
105
+ runner_store
106
+ end
107
+ end
108
+ end
109
+
110
+ # Creates a new sub runner, defaulting the context to `self`
111
+ def new_sub_runner(options, block)
112
+ defaults = {
113
+ context: self
114
+ }
115
+ options = defaults.merge(options)
116
+ SubRunner.new(options, block)
117
+ end
118
+
119
+ # Stores sub runners which ran and were a success
120
+ def successful_sub_runners
121
+ @successful_sub_runners ||= Store.new
122
+ end
123
+
124
+ # Stores sub runners which ran and failed
125
+ # Also store children of those which failed
126
+ def failed_sub_runners
127
+ @failed_sub_runners ||= Store.new
128
+ end
129
+
130
+ # If all of the parents have been successfully run (or there are no
131
+ # parents), runs the sub_runner.
132
+ # If any one of the parents has failed, considers the runner a failure
133
+ # If some parents have not yet completed, carries on
134
+ def run_unrun_sub_runners
135
+ unrun_sub_runners.each do |sub_runner|
136
+ if sub_runner_can_run? sub_runner
137
+ run_sub_runner(sub_runner)
138
+ elsif sub_runner.parents.any? { |name| failed_sub_runners.exists? name }
139
+ failed_sub_runners << sub_runner
140
+ unrun_sub_runners.delete sub_runner.name
141
+ end
142
+ end
143
+ run_unrun_sub_runners unless complete?
144
+ end
145
+
146
+ # Runs the sub runner, adding it to the appropriate sub runner store based
147
+ # on its success or failure and removes it from the unrun_sub_runners
148
+ def run_sub_runner(sub_runner)
149
+ sub_runner.run!
150
+ logger.info sub_runner.logger.contents
151
+ if sub_runner.success
152
+ successful_sub_runners << sub_runner
153
+ else
154
+ failed_sub_runners << sub_runner
155
+ end
156
+ unrun_sub_runners.delete sub_runner.name
157
+ end
158
+
159
+ ## TODO: timeout option
160
+ def complete?
161
+ unrun_sub_runners.empty?
162
+ end
163
+
164
+ # A sub runner can run if all prerequisites have been satisfied.
165
+ # This means all parent runners - those specified by name using the
166
+ # :requires options - have successfully completed.
167
+ def sub_runner_can_run?(sub_runner)
168
+ successful_sub_runners.includes_all?(sub_runner.parents)
169
+ end
170
+
171
+ # Resets all failed sub runners, then sets them as
172
+ # @unrun_sub_runners and @failed_sub_runners to an empty Store
173
+ def reset_failed_runners
174
+ failed_sub_runners.each(&:reset)
175
+ @unrun_sub_runners = failed_sub_runners
176
+ @failed_sub_runners = Store.new
177
+ end
178
+
179
+ def write_log
180
+ logger.info summary
181
+ end
182
+
183
+ def summary
184
+ """
185
+
186
+ Status: #{status}
187
+ Start Time: #{formatted_start_time}
188
+ End Time: #{formatted_end_time}
189
+ Total Time: #{formatted_total_time}
190
+
191
+ Successful SubRunners: #{successful_sub_runners.size}
192
+ Failed SubRunners: #{failed_sub_runners.size}
193
+ """
194
+ end
195
+
196
+ def status
197
+ if success
198
+ 'Success'
199
+ else
200
+ 'Failure'
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,176 @@
1
+ require 'set'
2
+ require 'active_support/concern'
3
+ require 'active_support/core_ext/proc'
4
+
5
+ module UltraMarathon
6
+ module Callbacks
7
+ extend ActiveSupport::Concern
8
+
9
+ private
10
+
11
+ ## Private Instance Methods
12
+
13
+ # Check if the options' hash of conditions are met.
14
+ # Supports :if, :unless with callable objects/symbols
15
+ def callback_conditions_met?(options)
16
+ conditions_met = true
17
+ if options.key? :if
18
+ conditions_met &&= call_proc_or_symbol(options[:if])
19
+ elsif options.key? :unless
20
+ conditions_met &&= !call_proc_or_symbol(options[:unless])
21
+ end
22
+ conditions_met
23
+ end
24
+
25
+ # Exectutes the object in the context of the instance,
26
+ # whether an explicitly callable object or a string/symbol
27
+ # representation of one
28
+ def call_proc_or_symbol(object, args=[], options={})
29
+ options = options.dup
30
+ options[:context] ||= self
31
+ bound_proc = bind_to_context(object, options[:context])
32
+ evaluate_block_with_arguments(bound_proc, args)
33
+ end
34
+
35
+ # Binds a proc to the given context. If a symbol is passed in,
36
+ # retrieves the bound method.
37
+ def bind_to_context(symbol_or_proc, context)
38
+ if symbol_or_proc.is_a?(Symbol)
39
+ context.method(symbol_or_proc)
40
+ elsif symbol_or_proc.respond_to? :call
41
+ symbol_or_proc.bind(context)
42
+ else
43
+ raise ArgumentError.new("Cannot bind #{callback.class} to #{context}. Expected callable object or symbol.")
44
+ end
45
+ end
46
+
47
+ # Applies a block in context with the correct number of arguments.
48
+ # If there are more arguments than the arity, takes the first n
49
+ # arguments where (n) is the arity of the block.
50
+ # If there are the same number of arguments, splats them into the block.
51
+ # Otherwise throws an argument error.
52
+ def evaluate_block_with_arguments(block, args)
53
+ # If block.arity < 0, when a block takes a variable number of args,
54
+ # the one's complement (-n-1) is the number of required arguments
55
+ required_arguments = block.arity < 0 ? ~block.arity : block.arity
56
+ if args.length >= required_arguments
57
+ if block.arity < 0
58
+ instance_exec(*args, &block)
59
+ else
60
+ instance_exec(*args.first(block.arity), &block)
61
+ end
62
+ else
63
+ raise ArgumentError.new("wrong number of arguments (#{args.size} for #{required_arguments})")
64
+ end
65
+ end
66
+
67
+ module ClassMethods
68
+ ## Public Class Methods
69
+
70
+ # Add one or more new callbacks for class
71
+ # E.g.
72
+ #
73
+ # callbacks :after_save
74
+ #
75
+ # Defines a class method `after_save` which
76
+ # takes an object responding to :call (Proc or lambda)
77
+ # or a symbol to be called in the context of the instance
78
+ #
79
+ # Also defines `invoke_after_save_callbacks` instance method
80
+ # for designating when the callbacks should be invoked
81
+ def callbacks(*callback_names)
82
+ new_callbacks = Set.new(callback_names) - _callback_names
83
+ new_callbacks.each do |callback_name|
84
+ add_callbacks_accessor callback_name
85
+ define_callback callback_name
86
+ add_invoke_callback callback_name
87
+ end
88
+ self._callback_names = new_callbacks
89
+ end
90
+
91
+ private
92
+
93
+ ## Private Class Methods
94
+
95
+ # Only keep unique callback names
96
+ def _callback_names=(new_callbacks)
97
+ @_callback_names = _callback_names | new_callbacks
98
+ end
99
+
100
+ def _callback_names
101
+ @_callback_names ||= Set.new
102
+ end
103
+
104
+ # On inheritance, the child should inheirit all callbacks of the
105
+ # parent. We don't use class variables because we don't want sibling
106
+ # classes to share callbacks
107
+ def inherited(base)
108
+ base.send(:callbacks, *_callback_names)
109
+ _callback_names.each do |callback_name|
110
+ parent_callbacks = send :"#{callback_name}_callbacks"
111
+ base.instance_variable_set(:"@#{callback_name}_callbacks", parent_callbacks)
112
+ end
113
+ end
114
+
115
+ # Defines class level accessor that memoizes the set of callbacks
116
+ # E.g.
117
+ #
118
+ # def self.after_save_callbacks
119
+ # @after_save_callbacks ||= []
120
+ # end
121
+ def add_callbacks_accessor(callback_name)
122
+ accessor_name = "#{callback_name}_callbacks"
123
+ instance_variable_name = :"@#{accessor_name}"
124
+ define_singleton_method("#{callback_name}_callbacks") do
125
+ instance_variable_get(instance_variable_name) ||
126
+ instance_variable_set(instance_variable_name, [])
127
+ end
128
+ end
129
+
130
+ # Validates that the callback is valid and adds it to the callback array
131
+ def define_callback(callback_name)
132
+ add_callback_setter(callback_name)
133
+ add_callback_array_writer(callback_name)
134
+ end
135
+
136
+ def add_callback_setter(callback_name)
137
+ define_singleton_method(callback_name) do |callback, options={}|
138
+ if valid_callback? callback
139
+ send("#{callback_name}_callbacks") << [callback, options]
140
+ else
141
+ raise ArgumentError.new("Expected callable object or symbol, got #{callback.class}")
142
+ end
143
+ end
144
+ end
145
+
146
+ # Callbacks should either be callable (Procs, lambdas) or a symbol
147
+ def valid_callback?(callback)
148
+ callback.respond_to?(:call) || callback.is_a?(Symbol)
149
+ end
150
+
151
+ # Use protected since this is used by parent classes
152
+ # to inherit callbacks
153
+ def add_callback_array_writer(callback_name)
154
+ attr_writer "#{callback_name}_callbacks"
155
+ protected "#{callback_name}_callbacks="
156
+ end
157
+
158
+ # Clears all callbacks. Useful for testing and inherited classes
159
+ def clear_callbacks!
160
+ _callback_names.each do |callback_name|
161
+ instance_variable_set(:"@#{callback_name}_callbacks", nil)
162
+ end
163
+ end
164
+
165
+ def add_invoke_callback(callback_name)
166
+ define_method("invoke_#{callback_name}_callbacks") do |*args|
167
+ callbacks = self.class.send :"#{callback_name}_callbacks"
168
+ callbacks.each do |callback, options|
169
+ next unless callback_conditions_met?(options)
170
+ call_proc_or_symbol(callback, args, options)
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,48 @@
1
+ module UltraMarathon
2
+ module Instrumentation
3
+ TIME_FORMAT = "%02d:%02d:%02d"
4
+ attr_reader :start_time, :end_time
5
+
6
+ ## Public Instance Methods
7
+
8
+ # returns the total time, in seconds
9
+ def total_time
10
+ (end_time - start_time).to_i
11
+ end
12
+
13
+ def formatted_total_time
14
+ duration = total_time
15
+ seconds = (duration % 60).floor
16
+ minutes = (duration / 60).floor
17
+ hours = (duration / 3600).floor
18
+ sprintf(TIME_FORMAT, hours, minutes, seconds)
19
+ end
20
+
21
+ def formatted_start_time
22
+ format_time(start_time)
23
+ end
24
+
25
+ def formatted_end_time
26
+ format_time(end_time)
27
+ end
28
+
29
+ private
30
+ def format_time(time)
31
+ sprintf(TIME_FORMAT, time.hour, time.min, time.sec)
32
+ end
33
+
34
+ ## Private Instance Methods
35
+
36
+ # Instruments given block, setting its start time and end time
37
+ # Returns the result of the block
38
+ def instrument(&block)
39
+ @start_time = Time.now
40
+ begin
41
+ return_value = yield
42
+ ensure
43
+ @end_time = Time.now
44
+ end
45
+ return_value
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,61 @@
1
+ require 'forwardable'
2
+
3
+ module UltraMarathon
4
+ class Logger
5
+ extend Forwardable
6
+ NEW_LINE = "\n".freeze
7
+
8
+ ## Public Instance Methods
9
+
10
+ # Adds the line plus a newline
11
+ def info(line)
12
+ return if line.empty?
13
+ log_string << padded_line(line)
14
+ end
15
+
16
+ def error(error)
17
+ if error.is_a? Exception
18
+ log_formatted_error(error)
19
+ else
20
+ info error
21
+ end
22
+ end
23
+
24
+ # Returns a copy of the log data so it cannot be externally altered
25
+ def contents
26
+ log_string.dup
27
+ end
28
+
29
+ alias_method :emerg, :info
30
+ alias_method :warning, :info
31
+ alias_method :notice, :info
32
+ alias_method :debug, :info
33
+ alias_method :err, :error
34
+ alias_method :panic, :emerg
35
+ alias_method :warn, :warning
36
+
37
+ private
38
+
39
+ ## Private Instance Methods
40
+
41
+ def log_string
42
+ @log_string ||= ''
43
+ end
44
+
45
+ def padded_line(line)
46
+ if line.end_with? NEW_LINE
47
+ line
48
+ else
49
+ line << NEW_LINE
50
+ end
51
+ end
52
+
53
+ def log_formatted_error(error)
54
+ info error.message
55
+ formatted_backtrace = error.backtrace.map.with_index do |backtrace_line, line_number|
56
+ sprintf('%03i) %s', line_number, backtrace_line)
57
+ end
58
+ info formatted_backtrace.join("\n")
59
+ end
60
+ end
61
+ end