ultra_marathon 0.1.7 → 0.1.10

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c7d97a30d4a344932ef8d315457e5072ec08da58
4
- data.tar.gz: 1aaf3b10556ce6344fe795438a1d176254b43a77
3
+ metadata.gz: c86ad7025f579980952497fa5613049fc05f119f
4
+ data.tar.gz: a2c6d98d6e09a74e071fdb253e76a6831ee7be48
5
5
  SHA512:
6
- metadata.gz: cb05ac7278f5cb8d6537650dde46c3fb8377d8460afe832e243b87079162e0c20ec9065903257aaf3e34666378099ee6ebc8c871a29ec2b99e675bb3fa39fd11
7
- data.tar.gz: 32a14722374518f955594603987ebde23ed1443ee8579164c7021b18e611b032a1f038f200e9a5be1ce068149d4b70613824d898d5f52a370ef4140c4275512f
6
+ metadata.gz: 98f72f85992e4b2af816575ff76bcec3adeb6e536409c95ecbc49841bd8fe915104f0ee058cb12ed1417eaf904738be1437a2852f94037a5dced3a9f0c45ba23
7
+ data.tar.gz: 517904e35cd8cec588e7ec2dde635f59c9f9a8dfcc43f54a970bef170ec1ca29ca4183e7b919e3f53ac5976ba2e8042a178446bc8a0aaf885c6109f8877bebd6
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # UltraMarathon
2
+ [![Build Status](https://travis-ci.org/tyre/ultra_marathon.svg?branch=master)](https://travis-ci.org/tyre/ultra_marathon)
2
3
 
3
4
  Fault tolerant platform for long running jobs.
4
5
 
@@ -86,6 +87,101 @@ In this instance, `bubbles` will not be run until `don_scuba_gear` successfully
86
87
  finishes. If `don_scuba_gear` explicitly fails, such as by raising an error,
87
88
  `bubbles` will never be run.
88
89
 
90
+ ### Collections
91
+
92
+ Sometimes you want to run a given run block once for each of a given set. Just
93
+ pass the `:collection` option and all of your dreams will come true. Each
94
+ iteration will be passed one item along with the index.
95
+
96
+ ```ruby
97
+ class RangeRunner < UltraMarathon::AbstractRunner
98
+
99
+ run :counting!, collection: (1..100) do |number, index|
100
+ if index == 0
101
+ puts "We start with #{number}"
102
+ else
103
+ puts "And then comes #{number}"
104
+ end
105
+ end
106
+
107
+ end
108
+ ```
109
+
110
+ The only requirement is that the `:collection` option responds to #each. But
111
+ what if it doesn't? Just pass in the `:iterator` option! This option was added
112
+ specifically for Rails ActiveRecord::Association instances that can fetch in
113
+ batches using `:find_each`
114
+
115
+ ```ruby
116
+ # Crow inherits from ActiveRecord::Base
117
+
118
+ class MurderRunner < UltraMarathon::AbstractRunner
119
+
120
+ run :coming_of_age, collection: :crows_to_bless, iterator: :find_each do |youngster_crow|
121
+ youngster_crow.update_attribute(blessed: true)
122
+ end
123
+
124
+ def crows_to_bless
125
+ Crow.unblessed.where(age: 10)
126
+ end
127
+
128
+ end
129
+
130
+ ```
131
+
132
+ ### Threading
133
+
134
+ Passing `threaded: true` will run that run block in its own thread. This is particularly useful for collections or run blocks which contain external API calls, hit a database, or any other candidate for concurrency.
135
+
136
+ ```ruby
137
+ class NapRunner < UltraMarathon::AbstractRunner
138
+ run :mass_nap, collection: (1..100), threaded: true do
139
+ sleep(0.01)
140
+ end
141
+ end
142
+
143
+ ```
144
+
145
+ #### Example
146
+
147
+ As we will see in the example below, for longer-running processes that Ruby can run concurrently, threading is a muy bueno idea.
148
+
149
+
150
+ ```ruby
151
+ require 'benchmark'
152
+ require 'ultra_marathon'
153
+
154
+ class ThreadedNapRunner < UltraMarathon::AbstractRunner
155
+ run_collection :mass_nap, items: (1..100), threaded: true do |n|
156
+ sleep(1)
157
+ end
158
+ end
159
+
160
+ class UnthreadedNapRunner < UltraMarathon::AbstractRunner
161
+ run_collection :mass_nap, items: (1..100), threaded: false do |n|
162
+ sleep(1)
163
+ end
164
+ end
165
+
166
+ Benchmark.bmbm do |reporter|
167
+ reporter.report(:threaded) { ThreadedNapRunner.new.run! }
168
+ reporter.report(:unthreaded) { UnthreadedNapRunner.new.run! }
169
+ end
170
+
171
+ # Rehearsal ----------------------------------------------
172
+ # threaded 1.270000 0.080000 1.350000 ( 1.346384)
173
+ # unthreaded 0.060000 0.010000 0.070000 (100.141031)
174
+ # ------------------------------------- total: 1.420000sec
175
+ #
176
+ # user system total real
177
+ # threaded 1.320000 0.060000 1.380000 ( 1.377940)
178
+ # unthreaded 0.060000 0.000000 0.060000 (100.128980)
179
+ ```
180
+
181
+ Note, however, that threading is not free. In the final benchmark, we would expect a runtime of ~1 second but saw over a third of a second slower. If we run the same test with a run block of `(n * 10_000).times { 'derp' }`, for example, the unthreaded version is about 10% faster.
182
+
183
+ tl;dr don't thread because it sounds cool. Use it when you need it.
184
+
89
185
  ### Callbacks
90
186
 
91
187
  `UltraMarathon::AbstractRunner` includes numerous life-cycle callbacks for
@@ -183,6 +279,6 @@ class WatRunner < UltraMarathon::AbstractRunner
183
279
  end
184
280
 
185
281
  WatRunner.run!.reset.run!
186
- #=> boom
282
+ #=> wrong
187
283
  #=> all is well in the universe
188
284
  ```
@@ -0,0 +1,13 @@
1
+ class Class
2
+ def attr_memo_reader(name, memoization_block)
3
+ define_method(name) do
4
+ instance_variable_get(:"@#{name}") ||
5
+ instance_variable_set(:"@#{name}", instance_exec(&memoization_block))
6
+ end
7
+ end
8
+
9
+ def attr_memo_accessor(name, memoization_block)
10
+ attr_memo_reader(name, memoization_block)
11
+ attr_writer name
12
+ end
13
+ end
@@ -0,0 +1,4 @@
1
+ require 'core_ext/class'
2
+ require 'core_ext/object'
3
+ require 'core_ext/proc'
4
+ require 'core_ext/string'
@@ -0,0 +1,12 @@
1
+ class Object
2
+ # Many times an option can either be a callable object (Proc/Lambda) or
3
+ # not (symbol/string/integer). This will call with the included arguments,
4
+ # if it is callable, or return the object if not.
5
+ def try_call(*args)
6
+ if respond_to? :call
7
+ call(*args)
8
+ else
9
+ self
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ class String
2
+
3
+ # Included for the kids.
4
+ def sploin(seperator, joiner, &block)
5
+ ary = split(seperator)
6
+ ary.map!(&block) if block_given?
7
+ ary.join(joiner)
8
+ end
9
+ end
@@ -1,10 +1,11 @@
1
- require 'core_ext/proc'
1
+ require 'core_ext/extensions'
2
2
  require 'ultra_marathon/abstract_runner'
3
3
  require 'ultra_marathon/callbacks'
4
4
  require 'ultra_marathon/instrumentation'
5
5
  require 'ultra_marathon/logging'
6
6
  require 'ultra_marathon/sub_runner'
7
7
  require 'ultra_marathon/store'
8
+ require 'ultra_marathon/collection_runner'
8
9
 
9
10
  module UltraMarathon
10
11
 
@@ -1,80 +1,39 @@
1
- require 'ultra_marathon/callbacks'
2
- require 'ultra_marathon/instrumentation'
3
- require 'ultra_marathon/logging'
1
+ require 'ultra_marathon/base_runner'
4
2
  require 'ultra_marathon/sub_runner'
5
3
  require 'ultra_marathon/store'
6
4
 
7
5
  module UltraMarathon
8
- class AbstractRunner
9
- include Logging
10
- include Instrumentation
11
- include Callbacks
12
- attr_accessor :success
13
- callbacks :before_run, :after_run, :on_error, :on_reset
14
-
6
+ class AbstractRunner < BaseRunner
15
7
  after_run :write_log
16
8
  on_error lambda { self.success = false }
17
9
  on_error lambda { |error| logger.error(error) }
18
10
 
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 = nil
26
- invoke_before_run_callbacks
27
- instrument(:run_unrun_sub_runners) { run_unrun_sub_runners }
28
- # If any of the sub runners explicitly set the success flag, don't override it
29
- self.success = failed_sub_runners.empty? if self.success.nil?
30
- rescue StandardError => error
31
- invoke_on_error_callbacks(error)
32
- ensure
33
- invoke_after_run_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 = nil
48
- invoke_on_reset_callbacks
49
- self
50
- end
51
-
52
11
  private
53
12
 
54
13
  ## Private Class Methods
55
14
 
56
15
  class << self
16
+ attr_memo_reader :run_blocks, -> { Hash.new }
57
17
 
58
18
  # This is where the magic happens.
59
19
  # Called in the class context, it will be safely executed in
60
20
  # the context of the instance.
61
21
  #
62
- # E.g.
63
- #
64
- # class BubblesRunner < AbstractRunner
65
- # run do
66
- # fire_the_missiles
67
- # take_a_nap
68
- # end
22
+ # @example
23
+ # class BubblesRunner < AbstractRunner
24
+ # run do
25
+ # fire_the_missiles
26
+ # take_a_nap
27
+ # end
69
28
  #
70
- # def fire_the_missiles
71
- # puts 'But I am le tired'
72
- # end
29
+ # def fire_the_missiles
30
+ # puts 'But I am le tired'
31
+ # end
73
32
  #
74
- # def take_a_nap
75
- # puts 'zzzzzz'
76
- # end
77
- # end
33
+ # def take_a_nap
34
+ # puts 'zzzzzz'
35
+ # end
36
+ # end
78
37
  #
79
38
  # BubblesRunner.new.run!
80
39
  # # => 'But I am le tired'
@@ -89,8 +48,9 @@ module UltraMarathon
89
48
  end
90
49
  end
91
50
 
92
- def run_blocks
93
- @run_blocks ||= Hash.new
51
+ def run_collection(name=:main, items=[], options={}, &block)
52
+ options.merge!(collection: true, items: items)
53
+ run(name, options, &block)
94
54
  end
95
55
  end
96
56
 
@@ -109,97 +69,22 @@ module UltraMarathon
109
69
 
110
70
  # Creates a new sub runner, defaulting the context to `self`
111
71
  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
72
+ options = {
73
+ context: self,
74
+ collection: false,
75
+ items: []
76
+ }.merge(options)
77
+ if options[:collection]
78
+ CollectionRunner.new(options.delete(:items), options, &block)
153
79
  else
154
- failed_sub_runners << sub_runner
80
+ SubRunner.new(options, block)
155
81
  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
82
  end
178
83
 
179
84
  def write_log
180
- logger.info summary
181
- end
182
-
183
- def summary
184
- run_instrumentation = instrumentations[:run_unrun_sub_runners]
185
- """
186
-
187
- Status: #{status}
188
- Run Start Time: #{run_instrumentation.formatted_start_time}
189
- End Time: #{run_instrumentation.formatted_end_time}
190
- Total Time: #{run_instrumentation.formatted_total_time}
191
-
192
- Successful SubRunners: #{successful_sub_runners.size}
193
- Failed SubRunners: #{failed_sub_runners.size}
194
- """
85
+ log_all_sub_runners
86
+ log_summary
195
87
  end
196
88
 
197
- def status
198
- if success
199
- 'Success'
200
- else
201
- 'Failure'
202
- end
203
- end
204
89
  end
205
90
  end
@@ -0,0 +1,216 @@
1
+ require 'ultra_marathon/callbacks'
2
+ require 'ultra_marathon/instrumentation'
3
+ require 'ultra_marathon/logging'
4
+
5
+ module UltraMarathon
6
+ class BaseRunner
7
+ RUN_INSTRUMENTATION_NAME = '__run!'.freeze
8
+ include Logging
9
+ include Instrumentation
10
+ include Callbacks
11
+
12
+ attr_accessor :success
13
+ attr_memo_accessor :running_sub_runners, -> { Store.new }
14
+ attr_memo_reader :successful_sub_runners, -> { Store.new }
15
+ attr_memo_reader :failed_sub_runners, -> { Store.new }
16
+
17
+ callbacks :before_run, :after_run, :on_error, :on_reset, :after_initialize
18
+
19
+ ## Public Class Methods
20
+
21
+ def self.new(*args, &block)
22
+ super(*args, &block).tap do |instance|
23
+ instance.send(:invoke_after_initialize_callbacks)
24
+ end
25
+ end
26
+
27
+ ## Public Instance Methods
28
+
29
+ # Runs the run block safely in the context of the instance
30
+ def run!
31
+ if unrun_sub_runners.any?
32
+ instrument RUN_INSTRUMENTATION_NAME do
33
+ begin
34
+ self.success = nil
35
+ invoke_before_run_callbacks
36
+ instrument(:__run_unrun_sub_runners) { run_unrun_sub_runners }
37
+ # If any of the sub runners explicitly set the success flag, don't override it
38
+ self.success = failed_sub_runners.empty? if self.success.nil?
39
+ rescue StandardError => error
40
+ invoke_on_error_callbacks(error)
41
+ end
42
+ end
43
+ invoke_after_run_callbacks
44
+ end
45
+ self
46
+ end
47
+
48
+ def success?
49
+ !!success
50
+ end
51
+
52
+ # Resets success to being true, unsets the failed sub_runners to [], and
53
+ # sets the unrun sub_runners to be the uncompleted/failed ones
54
+ def reset
55
+ reset_failed_runners
56
+ @success = nil
57
+ invoke_on_reset_callbacks
58
+ self
59
+ end
60
+
61
+ def run_instrumentation
62
+ instrumentations[RUN_INSTRUMENTATION_NAME]
63
+ end
64
+
65
+ private
66
+
67
+ ## Private Instance Methods
68
+
69
+ # If all of the parents have been successfully run (or there are no
70
+ # parents), runs the sub_runner.
71
+ # If any one of the parents has failed, considers the runner a failure
72
+ # If some parents have not yet completed, carries on
73
+ def run_unrun_sub_runners
74
+ until complete?
75
+ unrun_sub_runners.each do |sub_runner|
76
+ if sub_runner_can_run? sub_runner
77
+ running_sub_runners << sub_runner.run!
78
+ elsif sub_runner.parents.any? { |name| failed_sub_runners.exists? name }
79
+ failed_sub_runners << sub_runner
80
+ unrun_sub_runners.delete sub_runner.name
81
+ end
82
+ end
83
+ clean_up_completed_sub_runners
84
+ end
85
+ end
86
+
87
+ # Cleans up all dead threads, settings
88
+ def clean_up_completed_sub_runners
89
+ completed_runners, unfinished_runners = running_sub_runners.partition(&:complete?)
90
+ completed_runners.each do |sub_runner|
91
+ clean_up_sub_runner(sub_runner)
92
+ end
93
+ self.running_sub_runners = unfinished_runners
94
+ end
95
+
96
+ # Adds a run sub runner to the appropriate sub runner store based
97
+ # on its success or failure and removes it from the unrun_sub_runners
98
+ # Also merges its instrumentation to the group's instrumentation
99
+ def clean_up_sub_runner(sub_runner)
100
+ if sub_runner.success
101
+ successful_sub_runners << sub_runner
102
+ else
103
+ failed_sub_runners << sub_runner
104
+ end
105
+ instrumentations.merge!(sub_runner.instrumentations)
106
+ unrun_sub_runners.delete sub_runner.name
107
+ end
108
+
109
+ ## TODO: timeout option
110
+ def complete?
111
+ running_sub_runners.empty? && unrun_sub_runners.empty?
112
+ end
113
+
114
+ # Resets all failed sub runners, then sets them as
115
+ # unrun_sub_runners and failed_sub_runners to an empty Store
116
+ def reset_failed_runners
117
+ failed_sub_runners.each(&:reset)
118
+ @unrun_sub_runners = failed_sub_runners
119
+ @failed_sub_runners = Store.new
120
+ end
121
+
122
+ # A sub runner can run if all prerequisites have been satisfied.
123
+ # This means all parent runners - those specified by name using the
124
+ # :requires options - have successfully completed.
125
+ def sub_runner_can_run?(sub_runner)
126
+ successful_sub_runners.includes_all?(sub_runner.parents)
127
+ end
128
+
129
+ def status
130
+ if success?
131
+ 'Success'
132
+ else
133
+ 'Failure'
134
+ end
135
+ end
136
+
137
+ def log_all_sub_runners
138
+ log_failed_sub_runners if failed_sub_runners.any?
139
+ log_successful_sub_runners if successful_sub_runners.any?
140
+ end
141
+
142
+ def log_failed_sub_runners
143
+ logger.info """
144
+
145
+ == Failed SubRunners ==
146
+
147
+ """
148
+ log_sub_runners(failed_sub_runners)
149
+ end
150
+
151
+ def log_successful_sub_runners
152
+ logger.info """
153
+
154
+ == Successful SubRunners ==
155
+
156
+ """
157
+ log_sub_runners(successful_sub_runners)
158
+ end
159
+
160
+
161
+ def log_sub_runners(sub_runners)
162
+ sub_runners.each do |sub_runner|
163
+ logger.info(sub_runner.logger.contents << "\n")
164
+ end
165
+ end
166
+
167
+ def log_summary
168
+ run_profile = instrumentations[:run!]
169
+ failed_names = failed_sub_runners.names.map(&:to_s).join(', ')
170
+ succcessful_names = successful_sub_runners.names.map(&:to_s).join(', ')
171
+ unrun_names = unrun_sub_runners.names.map(&:to_s).join(', ')
172
+ logger.info """
173
+
174
+ Status: #{status}
175
+
176
+ Failed (#{failed_sub_runners.size}): #{failed_names}
177
+ Successful (#{successful_sub_runners.size}): #{succcessful_names}
178
+ Unrun (#{unrun_sub_runners.size}): #{unrun_names}
179
+
180
+ #{time_summary}
181
+
182
+ """
183
+ end
184
+
185
+ def sub_runner_instrumentations
186
+ @sub_runner_instrumentations ||= begin
187
+ sub_runner_profiles = instrumentations.select do |profile|
188
+ profile.name.to_s.start_with? 'sub_runner.'
189
+ end
190
+ UltraMarathon::Instrumentation::Store.new(sub_runner_profiles)
191
+ end
192
+ end
193
+
194
+ def time_summary
195
+ """
196
+ Run Start Time: #{run_instrumentation.formatted_start_time}
197
+ End Time: #{run_instrumentation.formatted_end_time}
198
+ Total Time: #{run_instrumentation.formatted_total_time}
199
+
200
+ #{sub_runner_summary if sub_runner_instrumentations.any?}
201
+ """
202
+ end
203
+
204
+ def sub_runner_summary
205
+ median_profile = sub_runner_instrumentations.median
206
+ max_profile = sub_runner_instrumentations.max
207
+ min_profile = sub_runner_instrumentations.min
208
+ """
209
+ Max SubRunner Runtime: #{max_profile.name} (#{max_profile.total_time})
210
+ Min SubRunner Runtime: #{min_profile.name} (#{min_profile.total_time})
211
+ Median SubRunner Runtime: #{median_profile.name} (#{median_profile.total_time})
212
+ SubRunner Runtime Standard Deviation: #{sub_runner_instrumentations.standard_deviation}
213
+ """
214
+ end
215
+ end
216
+ end