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
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'ultra_marathon/logger'
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
module UltraMarathon
|
6
|
+
module Logging
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
## Private Instance Methods
|
10
|
+
|
11
|
+
def logger
|
12
|
+
@logger ||= self.class.logger_class.new
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
|
17
|
+
## Public Class Methods
|
18
|
+
|
19
|
+
# If the instance variable is callable, the result of invoking that block
|
20
|
+
# is set to be the instance variable. Otherwise returns it, defaulting
|
21
|
+
# to the included Logger class
|
22
|
+
def logger_class
|
23
|
+
if @logger_class && @logger_class.respond_to?(:call)
|
24
|
+
@logger_class = @logger_class.call
|
25
|
+
else
|
26
|
+
@logger_class ||= Logger
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
## Private Class Methods
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# Sets the log class. Can take a callable object or class
|
35
|
+
def log_class(log_class)
|
36
|
+
@logger_class = log_class
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module UltraMarathon
|
5
|
+
class Store
|
6
|
+
include Enumerable
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :store, :[], :[]=, :delete, :empty?, :length, :size
|
10
|
+
|
11
|
+
## Public Instance Methods
|
12
|
+
|
13
|
+
def initialize(new_runners=[])
|
14
|
+
new_runners.each do |new_runner|
|
15
|
+
add(new_runner)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def <<(runner)
|
20
|
+
store[runner.name] = runner
|
21
|
+
end
|
22
|
+
alias_method :add, :<<
|
23
|
+
|
24
|
+
def each(&block)
|
25
|
+
runners.each(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def pluck(&block)
|
29
|
+
runners.map(&block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def names
|
33
|
+
Set.new(store.keys)
|
34
|
+
end
|
35
|
+
|
36
|
+
def includes_all?(query_names)
|
37
|
+
(Set.new(query_names) - self.names).empty?
|
38
|
+
end
|
39
|
+
|
40
|
+
# When determining attributes, the user should always check for existence.
|
41
|
+
# If they don't, return nil.
|
42
|
+
def success?(name)
|
43
|
+
if exists? name
|
44
|
+
store[name].success
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def failed?(name)
|
49
|
+
if exists? name
|
50
|
+
!success? name
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def exists?(name)
|
55
|
+
store.key? name
|
56
|
+
end
|
57
|
+
alias_method :exist?, :exists?
|
58
|
+
|
59
|
+
def ==(other)
|
60
|
+
if other.is_a? self.class
|
61
|
+
other.names == self.names
|
62
|
+
else
|
63
|
+
false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
## Private Instance Methods
|
70
|
+
|
71
|
+
def runners
|
72
|
+
store.values
|
73
|
+
end
|
74
|
+
|
75
|
+
def store
|
76
|
+
@store ||= Hash.new
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'active_support/core_ext/proc'
|
3
|
+
require 'ultra_marathon/callbacks'
|
4
|
+
require 'ultra_marathon/logging'
|
5
|
+
|
6
|
+
module UltraMarathon
|
7
|
+
class SubRunner
|
8
|
+
include Callbacks
|
9
|
+
include Logging
|
10
|
+
attr_accessor :run_block, :success
|
11
|
+
attr_reader :sub_context, :options, :name
|
12
|
+
|
13
|
+
callbacks :before_run, :after_run, :after_all, :on_error, :on_reset
|
14
|
+
after_all :log_header_and_sub_context
|
15
|
+
|
16
|
+
on_error lambda { self.success = false }
|
17
|
+
on_error lambda { |error| logger.error error }
|
18
|
+
|
19
|
+
# The :context option is required, because you'll never want to run a
|
20
|
+
# SubRunner in context of itself.
|
21
|
+
# SubContext is necessary because we want to run in the context of the
|
22
|
+
# other class, but do other things (like log) in the context of this one.
|
23
|
+
def initialize(options, run_block)
|
24
|
+
@name = options[:name]
|
25
|
+
@options = options
|
26
|
+
@sub_context = SubContext.new(options[:context], run_block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def run!
|
30
|
+
begin
|
31
|
+
self.success = true
|
32
|
+
run_sub_context
|
33
|
+
rescue StandardError => error
|
34
|
+
invoke_on_error_callbacks(error)
|
35
|
+
ensure
|
36
|
+
invoke_after_all_callbacks
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def reset
|
41
|
+
invoke_on_reset_callbacks
|
42
|
+
end
|
43
|
+
|
44
|
+
# Set of all sub runners that should be run before this one.
|
45
|
+
# This class cannot do anything with this information, but it is useful
|
46
|
+
# to the enveloping runner.
|
47
|
+
def parents
|
48
|
+
@parents ||= Set.new(options[:requires])
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def run_sub_context
|
54
|
+
invoke_before_run_callbacks
|
55
|
+
sub_context.call
|
56
|
+
invoke_after_run_callbacks
|
57
|
+
end
|
58
|
+
|
59
|
+
def log_header_and_sub_context
|
60
|
+
logger.info log_header
|
61
|
+
log_sub_context
|
62
|
+
end
|
63
|
+
|
64
|
+
def log_sub_context
|
65
|
+
logger.info sub_context.logger.contents
|
66
|
+
end
|
67
|
+
|
68
|
+
def log_header
|
69
|
+
"Running '#{name}' SubRunner"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class SubContext
|
74
|
+
include Logging
|
75
|
+
attr_reader :context, :run_block
|
76
|
+
|
77
|
+
def initialize(context, run_block)
|
78
|
+
@context = context
|
79
|
+
# Ruby cannot marshal procs or lambdas, so we need to define a method.
|
80
|
+
# Binding to self allows us to intercept logging calls.
|
81
|
+
define_singleton_method :call, &run_block.bind(self)
|
82
|
+
end
|
83
|
+
|
84
|
+
# If the original context responds, delegate to it
|
85
|
+
def method_missing(method, *args, &block)
|
86
|
+
if context.respond_to? method
|
87
|
+
context.send(method, *args, &block)
|
88
|
+
else
|
89
|
+
super
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module UltraMarathon
|
2
|
+
class Version
|
3
|
+
MAJOR = 0 unless defined? MAJOR
|
4
|
+
MINOR = 0 unless defined? MINOR
|
5
|
+
PATCH = 1 unless defined? PATCH
|
6
|
+
PRE = nil unless defined? PRE
|
7
|
+
|
8
|
+
class << self
|
9
|
+
|
10
|
+
# @return [String]
|
11
|
+
def to_s
|
12
|
+
[MAJOR, MINOR, PATCH, PRE].compact.join('.')
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
module TestHelpers
|
3
|
+
|
4
|
+
def wait_for_lock(mutex)
|
5
|
+
ensure_path_to_mutex(mutex)
|
6
|
+
total_wait_time = 0.0
|
7
|
+
while File.exists?(mutex_path(mutex))
|
8
|
+
sleep(0.1)
|
9
|
+
total_wait_time += 0.1
|
10
|
+
if total_wait_time > 60 * 5
|
11
|
+
raise "Took too long to obtain a lock"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
File.open(mutex_path(mutex), "w") {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def release_lock(mutex)
|
18
|
+
File.delete(mutex_path(mutex)) if File.exists? mutex_path(mutex)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# returns the mutext path, making sure to only dump crap in tmp/
|
24
|
+
def mutex_path(mutex)
|
25
|
+
if mutex.start_with? 'tmp'
|
26
|
+
mutex
|
27
|
+
else
|
28
|
+
'tmp/' << mutex
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Given a mutex 'log/maintenance/walrus_maintenance.log',
|
33
|
+
# ensures that 'tmp/log/maintenance/' directory exists
|
34
|
+
def ensure_path_to_mutex(mutex)
|
35
|
+
path = mutex_path(mutex)
|
36
|
+
if path =~ /\Atmp\/.+\/([^\/]+)\z/
|
37
|
+
FileUtils.mkdir_p path[0...-$1.length]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'support/file_mutexes'
|
2
|
+
|
3
|
+
module TestHelpers
|
4
|
+
|
5
|
+
# create an anonymous test class so we don't pollute the global namespace
|
6
|
+
def anonymous_test_class(inherited_class=Object, &block)
|
7
|
+
Class.new(inherited_class).tap do |klass|
|
8
|
+
klass.class_eval(&block) if block_given?
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe UltraMarathon::AbstractRunner do
|
4
|
+
let(:test_class) { anonymous_test_class(UltraMarathon::AbstractRunner) }
|
5
|
+
let(:test_instance) { test_class.new }
|
6
|
+
|
7
|
+
describe '#run!' do
|
8
|
+
subject { test_instance.run! }
|
9
|
+
|
10
|
+
describe 'with one run block' do
|
11
|
+
before(:each) { test_class.send :run, &run_block }
|
12
|
+
|
13
|
+
context 'when everything is fine' do
|
14
|
+
let(:run_block) { Proc.new { logger.info 'Look at me go!' } }
|
15
|
+
|
16
|
+
it 'should run the given block in the context of the instance' do
|
17
|
+
subject
|
18
|
+
test_instance.logger.contents.should include "Look at me go!\n"
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should be a success' do
|
22
|
+
subject
|
23
|
+
test_instance.success.should be
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'when things go wrong' do
|
28
|
+
let(:run_block) { Proc.new { raise 'Benoit!' } }
|
29
|
+
|
30
|
+
it 'should catch the error' do
|
31
|
+
expect { subject }.to_not raise_error
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should logger.info the error message' do
|
35
|
+
subject
|
36
|
+
test_instance.logger.contents.should include 'Benoit!'
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should set success to false' do
|
40
|
+
subject
|
41
|
+
test_instance.success.should_not be
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe 'with multiple run blocks' do
|
47
|
+
|
48
|
+
context 'when everything is fine' do
|
49
|
+
|
50
|
+
before(:each) do
|
51
|
+
test_class.send(:run, &->{ logger.info 'I am the Walrus' })
|
52
|
+
test_class.send(:run, :yolo, &->{ logger.info 'Koo koo ka choo' })
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should run both blocks' do
|
56
|
+
subject
|
57
|
+
test_instance.logger.contents.should include 'I am the Walrus'
|
58
|
+
test_instance.logger.contents.should include 'Koo koo ka choo'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'when one goes awry' do
|
63
|
+
before(:each) do
|
64
|
+
test_class.send(:run, :walrus, &->{ raise 'I am the Walrus' })
|
65
|
+
test_class.send(:run, :wat, &->{ logger.info 'Koo koo ka choo' })
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should still run the other' do
|
69
|
+
subject
|
70
|
+
test_instance.logger.contents.should include 'I am the Walrus'
|
71
|
+
test_instance.logger.contents.should include 'Koo koo ka choo'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'when one run relies on another' do
|
76
|
+
context 'when everything is okay' do
|
77
|
+
before(:each) do
|
78
|
+
test_class.send(:run, :wat, requires: [:walrus], &->{ logger.info 'Koo koo ka choo' })
|
79
|
+
test_class.send(:run, :walrus, &->{ logger.info 'I am the Walrus' })
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should run both blocks' do
|
83
|
+
subject
|
84
|
+
test_instance.logger.contents.should include 'I am the Walrus'
|
85
|
+
test_instance.logger.contents.should include 'Koo koo ka choo'
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should run the parents before the children' do
|
89
|
+
subject
|
90
|
+
walrus_log_position = test_instance.logger.contents.index('I am the Walrus')
|
91
|
+
wat_log_position = test_instance.logger.contents.index('Koo koo ka choo')
|
92
|
+
walrus_log_position.should be < wat_log_position
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context 'when the parent fails' do
|
97
|
+
before(:each) do
|
98
|
+
test_class.send(:run, :wat, requires: [:walrus], &->{ logger.info 'Koo koo ka choo' })
|
99
|
+
test_class.send(:run, :walrus, &->{ raise 'I am the Walrus!' })
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'should logger.info the error' do
|
103
|
+
subject
|
104
|
+
test_instance.logger.contents.should include 'I am the Walrus!'
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'should not run the child' do
|
108
|
+
subject
|
109
|
+
test_instance.logger.contents.should_not include 'Koo koo ka choo'
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe 'callbacks' do
|
117
|
+
before(:each) { test_class.send :run, &run_block }
|
118
|
+
|
119
|
+
subject { test_instance.run! }
|
120
|
+
|
121
|
+
describe 'before_run callback' do
|
122
|
+
let(:run_block) { Proc.new { logger.info 'Blastoff!' } }
|
123
|
+
|
124
|
+
before(:each) do
|
125
|
+
test_class.before_run ->{ logger.info '3-2-1' }
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'should invoke before_run callbacks before run!' do
|
129
|
+
subject
|
130
|
+
test_instance.logger.contents.should include '3-2-1'
|
131
|
+
test_instance.logger.contents.index('3-2-1').should be < test_instance.logger.contents.index('Blastoff!')
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe 'after_run callback' do
|
136
|
+
let(:run_block) { Proc.new { logger.info 'Blastoff!' } }
|
137
|
+
|
138
|
+
before(:each) do
|
139
|
+
test_class.after_run ->{ logger.info 'We have liftoff!' }
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'should invoke before_run callbacks before run!' do
|
143
|
+
subject
|
144
|
+
test_instance.logger.contents.should include 'We have liftoff!'
|
145
|
+
test_instance.logger.contents.index('We have liftoff!').should be > test_instance.logger.contents.index('Blastoff!')
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe '#reset' do
|
151
|
+
it 'should change success to true' do
|
152
|
+
test_instance.success = false
|
153
|
+
test_instance.reset
|
154
|
+
test_instance.success.should be
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'returns itself' do
|
158
|
+
test_instance.reset.should be test_instance
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|