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