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 +7 -0
- data/LICENSE.md +19 -0
- data/README.md +160 -0
- data/lib/ultra_marathon/abstract_runner.rb +204 -0
- data/lib/ultra_marathon/callbacks.rb +176 -0
- data/lib/ultra_marathon/instrumentation.rb +48 -0
- data/lib/ultra_marathon/logger.rb +61 -0
- data/lib/ultra_marathon/logging.rb +40 -0
- data/lib/ultra_marathon/store.rb +79 -0
- data/lib/ultra_marathon/sub_runner.rb +93 -0
- data/lib/ultra_marathon/version.rb +18 -0
- data/lib/ultra_marathon.rb +10 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/file_mutexes.rb +41 -0
- data/spec/support/test_helpers.rb +11 -0
- data/spec/ultra_marathon/abstract_runner_spec.rb +161 -0
- data/spec/ultra_marathon/callbacks_spec.rb +244 -0
- data/spec/ultra_marathon/instrumentation_spec.rb +70 -0
- data/spec/ultra_marathon/logging_spec.rb +37 -0
- data/spec/ultra_marathon/store_spec.rb +148 -0
- data/spec/ultra_marathon/sub_runner_spec.rb +52 -0
- data/ultra_marathon.gemspec +20 -0
- metadata +73 -0
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
|