ultra_marathon 0.1.7 → 0.1.10

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