parallel_minion 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a490fae075880a809d3d3542381454e2737ee560
4
+ data.tar.gz: bec1bb591e8d19b490821f5153cb195083e9e58a
5
+ SHA512:
6
+ metadata.gz: 49c1e58bcb4011633cd0f80d666a6872f8fea1719cb5e2085dc463e6afd0ed71672ee1e49c5846fca668a0704beebdf22f0b816d804a7590f88e02571bf7ecd6
7
+ data.tar.gz: 383505b36f1990379463117143231f2c069dc3451658a05fcc8d5c303a1394cdf76a232a4439f0d90660e8af3305ddc6445f5c74171f75f70940df7f0f43f534
@@ -0,0 +1,234 @@
1
+ parallel_minion
2
+ ===============
3
+
4
+ Parallel Minion supports easily handing work off to Minions (Threads) so that tasks
5
+ that would normally be performed sequentially can easily be executed in parallel.
6
+ This allows Ruby and Rails applications to very easily do many tasks at the same
7
+ time so that results are returned more quickly.
8
+
9
+ Our use-case for Minions is where an application grew to a point where it would
10
+ be useful to run some of the steps in fulfilling a single request in parallel.
11
+
12
+ ## Features:
13
+
14
+ Exceptions
15
+
16
+ - Any exceptions raised in Minions are captured and propagated back to the
17
+ calling thread when #result is called
18
+ - Makes exception handling simple with a drop-in replacement in existing code
19
+
20
+ Timeouts
21
+
22
+ - Timeout when a Minion does not return within a specified time
23
+ - Timeouts are a useful feature when one of the Minions fails to respond in a
24
+ reasonable amount of time. For example when a call to a remote service hangs
25
+ we can send back a partial response of other work that was completed rather
26
+ than just "hanging" or failing completely.
27
+
28
+ Logging
29
+
30
+ - Built-in support to log the duration of all Minion tasks to make future analysis
31
+ of performance issues much easier
32
+ - Logs any exceptions thrown to assist fwith problem diagnosis
33
+
34
+ ## Example
35
+
36
+ Simple example
37
+
38
+ ```ruby
39
+ ParallelMinion::Minion.new(10.days.ago, description: 'Doing something else in parallel', timeout: 1000) do |date|
40
+ MyTable.where('created_at <= ?', date).count
41
+ end
42
+ ```
43
+
44
+ ## Example
45
+
46
+ For example, in the code below there are several steps that are performed sequentially:
47
+
48
+ ```ruby
49
+ # Contrived example to show how to do parallel code execution
50
+ # with (unreal) sample durations in the comments
51
+
52
+ def process_request(request)
53
+ # Count number of entries in a table.
54
+ # Average response time 150ms
55
+ person_count = Person.where(state: 'FL').count
56
+
57
+ # Count the number of requests for this user (usually more complex with were clauses etc.)
58
+ # Average response time 320ms
59
+ request_count = Requests.where(user_id: request.user.id).count
60
+
61
+ # Call an external provider
62
+ # Average response time 1800ms ( Sometimes "hangs" when supplier does not respond )
63
+ inventory = inventory_supplier.check_inventory(request.product.id)
64
+
65
+ # Call another provider for more info on the user
66
+ # Average response time 1500ms
67
+ user_info = user_supplier.more_info(request.user.name)
68
+
69
+ # Build up the reply
70
+ reply = MyReply.new(user_id: request.user.id)
71
+
72
+ reply.number_of_people = person_count
73
+ reply.number_of_requests = request_count
74
+ reply.user_details = user_info.details
75
+ if inventory.product_available?
76
+ reply.available = true
77
+ reply.quantity = 100
78
+ else
79
+ reply.available = false
80
+ end
81
+
82
+ reply
83
+ end
84
+ ```
85
+ The average response time when calling #process_request is around 3,780 milli-seconds.
86
+
87
+ The first step could be to run the supplier calls in parallel.
88
+ Through log analysis we have determined that the first supplier call takes on average
89
+ 1,800 ms and we have decided that it should not wait longer than 2,200 ms for a response.
90
+
91
+ ```ruby
92
+ # Now with a single parallel call
93
+
94
+ def process_request(request)
95
+ # Count number of entries in a table.
96
+ # Average response time 150ms
97
+ person_count = Person.where(state: 'FL').count
98
+
99
+ # Count the number of requests for this user (usually more complex with were clauses etc.)
100
+ # Average response time 320ms
101
+ request_count = Requests.where(user_id: request.user.id).count
102
+
103
+ # Call an external provider
104
+ # Average response time 1800ms ( Sometimes "hangs" when supplier does not respond )
105
+ inventory_minion = ParallelMinion::Minion.new(request.product.id, description: 'Inventory Lookup', timeout: 2200) do |product_id|
106
+ inventory_supplier.check_inventory(product_id)
107
+ end
108
+
109
+ # Call another provider for more info on the user
110
+ # Average response time 1500ms
111
+ user_info = user_supplier.more_info(request.user.name)
112
+
113
+ # Build up the reply
114
+ reply = MyReply.new(user_id: request.user.id)
115
+
116
+ reply.number_of_people = person_count
117
+ reply.number_of_requests = request_count
118
+ reply.user_details = user_info.details
119
+
120
+ # Get inventory result from Inventory Lookup minion
121
+ inventory = inventory_minion.result
122
+
123
+ if inventory.product_available?
124
+ reply.available = true
125
+ reply.quantity = 100
126
+ else
127
+ reply.available = false
128
+ end
129
+
130
+ reply
131
+ end
132
+ ```
133
+
134
+ The above changes drop the average processing time from 3,780 milli-seconds to
135
+ 2,280 milli-seconds.
136
+
137
+ By moving the supplier call to the top of the function call it can be optimized
138
+ to about 1,970 milli-seconds.
139
+
140
+ We can further parallelize the processing to gain even greater performance.
141
+
142
+ ```ruby
143
+ # Now with two parallel calls
144
+
145
+ def process_request(request)
146
+ # Call an external provider
147
+ # Average response time 1800ms ( Sometimes "hangs" when supplier does not respond )
148
+ inventory_minion = ParallelMinion::Minion.new(request.product.id, description: 'Inventory Lookup', timeout: 2200) do |product_id|
149
+ inventory_supplier.check_inventory(product_id)
150
+ end
151
+
152
+ # Count the number of requests for this user (usually more complex with were clauses etc.)
153
+ # Average response time 320ms
154
+ request_count_minion = ParallelMinion::Minion.new(request.user.id, description: 'Request Count', timeout: 500) do |user_id|
155
+ Requests.where(user_id: user_id).count
156
+ end
157
+
158
+ # Leave the current thread some work to do too
159
+
160
+ # Count number of entries in a table.
161
+ # Average response time 150ms
162
+ person_count = Person.where(state: 'FL').count
163
+
164
+ # Call another provider for more info on the user
165
+ # Average response time 1500ms
166
+ user_info = user_supplier.more_info(request.user.name)
167
+
168
+ # Build up the reply
169
+ reply = MyReply.new(user_id: request.user.id)
170
+
171
+ reply.number_of_people = person_count
172
+ # The request_count is retrieved from the request_count_minion first since it
173
+ # should complete before the inventory_minion
174
+ reply.number_of_requests = request_count_minion.result
175
+ reply.user_details = user_info.details
176
+
177
+ # Get inventory result from Inventory Lookup minion
178
+ inventory = inventory_minion.result
179
+
180
+ if inventory.product_available?
181
+ reply.available = true
182
+ reply.quantity = 100
183
+ else
184
+ reply.available = false
185
+ end
186
+
187
+ reply
188
+ end
189
+ ```
190
+
191
+ The above #process_request method should now take on average 1,810 milli-seconds
192
+ which is significantly faster than the 3,780 milli-seconds it took to perform
193
+ the exact same request, but using only a single thread
194
+
195
+ The exact breakdown of which calls to do in the main thread versus a Minion is determined
196
+ through experience and trial and error over time. The key is logging the duration
197
+ of each call which Minion does by default so that the exact processing breakdown
198
+ can be fine-tuned over time.
199
+
200
+ Meta
201
+ ----
202
+
203
+ * Code: `git clone git://github.com/reidmorrison/parallel_minion.git`
204
+ * Home: <https://github.com/reidmorrison/parallel_minion>
205
+ * Bugs: <http://github.com/reidmorrison/parallel_minion/issues>
206
+ * Gems: <http://rubygems.org/gems/parallel_minion>
207
+
208
+ This project uses [Semantic Versioning](http://semver.org/).
209
+
210
+ Author
211
+ -------
212
+
213
+ Reid Morrison :: reidmo@gmail.com :: @reidmorrison
214
+
215
+ Contributors
216
+ ------------
217
+
218
+
219
+ License
220
+ -------
221
+
222
+ Copyright 2013 Reid Morrison
223
+
224
+ Licensed under the Apache License, Version 2.0 (the "License");
225
+ you may not use this file except in compliance with the License.
226
+ You may obtain a copy of the License at
227
+
228
+ http://www.apache.org/licenses/LICENSE-2.0
229
+
230
+ Unless required by applicable law or agreed to in writing, software
231
+ distributed under the License is distributed on an "AS IS" BASIS,
232
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
233
+ See the License for the specific language governing permissions and
234
+ limitations under the License.
@@ -0,0 +1,23 @@
1
+ lib = File.expand_path('../lib/', __FILE__)
2
+ $:.unshift lib unless $:.include?(lib)
3
+
4
+ require 'rubygems'
5
+ require 'rubygems/package'
6
+ require 'rake/clean'
7
+ require 'rake/testtask'
8
+ require 'parallel_minion/version'
9
+
10
+ desc "Build gem"
11
+ task :gem do |t|
12
+ Gem::Package.build(Gem::Specification.load('parallel_minion.gemspec'))
13
+ end
14
+
15
+ desc "Run Test Suite"
16
+ task :test do
17
+ Rake::TestTask.new(:functional) do |t|
18
+ t.test_files = FileList['test/*_test.rb']
19
+ t.verbose = true
20
+ end
21
+
22
+ Rake::Task['functional'].invoke
23
+ end
@@ -0,0 +1,8 @@
1
+ require 'thread'
2
+ require 'semantic_logger'
3
+
4
+ module ParallelMinion
5
+ autoload :Minion, 'parallel_minion/minion'
6
+ end
7
+
8
+ require 'parallel_minion/railtie' if defined?(Rails)
@@ -0,0 +1,41 @@
1
+ require 'minion'
2
+
3
+ # A Horde is way to manage a group of Minions
4
+ #
5
+ # Horde supports the following features for minions
6
+ # - Limit the number of Minions in the Horde to prevent overloading the system
7
+ # - Queue up requests for Minions when the lmit is reached
8
+ # - Optionally block submitting work for Minions when the queued up requests
9
+ # reach a specified number
10
+ class Horde
11
+ include SemanticLogger::Loggable
12
+
13
+ # Returns the description for this Horde
14
+ attr_reader :description
15
+
16
+ # Returns the maximum number of Minions active in this Horde at any time
17
+ attr_reader :capacity
18
+
19
+ # Returns the maximum number of queued up requests before blocking
20
+ # new requests for work
21
+ attr_reader :max_queue_size
22
+
23
+ # Create a new Horde of Minions
24
+ #
25
+ # Parameters
26
+ # :description
27
+ # Description for this task that the Minion is performing
28
+ # Put in the log file along with the time take to complete the task
29
+ #
30
+ # :capacity
31
+ # Maximum number of Minions active in this Horde at any time
32
+ # Default: 10
33
+ #
34
+ # :max_queue_size
35
+ # Maximum number of queued up requests before blocking
36
+ # new requests for work
37
+ # Default: -1 (unlimited)
38
+ #
39
+ def initialize(params={})
40
+ end
41
+ end
@@ -0,0 +1,237 @@
1
+ # Instruct a Minion to perform a specific task in a separate thread
2
+ module ParallelMinion
3
+ class Minion
4
+ include SemanticLogger::Loggable
5
+
6
+ # Returns [String] the description supplied on the initializer
7
+ attr_reader :description
8
+
9
+ # Returns [Exception] the exception that was raised otherwise nil
10
+ attr_reader :exception
11
+
12
+ # Returns [Integer] the maximum duration in milli-seconds that the Minion may take to complete the task
13
+ attr_reader :timeout
14
+
15
+ # Give an infinite amount of time to wait for a Minion to complete a task
16
+ INFINITE = -1
17
+
18
+ # Sets whether to run in Synchronous mode
19
+ #
20
+ # By Setting synchronous to true all Minions that have not yet been started
21
+ # will run in the thread from which they are started and not in their own
22
+ # threads
23
+ #
24
+ # This is useful:
25
+ # - to run tests under the Capybara gem
26
+ # - when debugging code so that all code is run sequentially in the current thread
27
+ #
28
+ # Note: Do not set this setting to true in Production
29
+ def self.synchronous=(synchronous)
30
+ @@synchronous = synchronous
31
+ end
32
+
33
+ # Returns whether running in Synchronous mode
34
+ def self.synchronous?
35
+ @@synchronous
36
+ end
37
+
38
+ # The list of classes for which the current scope must be copied into the
39
+ # new Minion (Thread)
40
+ #
41
+ # Example:
42
+ # ...
43
+ def self.scoped_classes
44
+ @@scoped_classes
45
+ end
46
+
47
+ # Create a new thread and
48
+ # Log the time for the thread to complete processing
49
+ # The exception without stack trace is logged whenever an exception is
50
+ # thrown in the thread
51
+ # Re-raises any unhandled exception in the calling thread when it call #result
52
+ # Copy the logging tags and specified ActiveRecord scopes to the new thread
53
+ #
54
+ # Parameters
55
+ # :description [String]
56
+ # Description for this task that the Minion is performing
57
+ # Put in the log file along with the time take to complete the task
58
+ #
59
+ # :timeout [Integer]
60
+ # Maximum amount of time in milli-seconds that the task may take to complete
61
+ # before #result times out
62
+ # Set to Minion::INFINITE to give the thread an infinite amount of time to complete
63
+ # Default: Minion::INFINITE
64
+ #
65
+ # Notes:
66
+ # - :timeout does not affect what happens to the Minion running the
67
+ # the task, it only affects how long #result will take to return.
68
+ # - The Minion will continue to run even after the timeout has been exceeded
69
+ # - If :synchronous is true, or ParallelMinion::Minion.synchronous is
70
+ # set to true, then :timeout is ignored and assumed to be Minion::INFINITE
71
+ # since the code is run in the calling thread when the Minion is created
72
+ #
73
+ # :synchronous [Boolean]
74
+ # Whether the Minion should run in the current thread
75
+ # Not recommended in Production, but is useful for debugging purposes
76
+ # Default: false
77
+ #
78
+ # *args
79
+ # Any number of arguments can be supplied that are passed into the block
80
+ # in the order they are listed
81
+ # It is recommended to duplicate and/or freeze objects passed as arguments
82
+ # so that they are not modified at the same time by multiple threads
83
+ #
84
+ # Proc / lambda
85
+ # A block of code must be supplied that the Minion will execute
86
+ # NOTE: This block will be executed within the scope of the created minion
87
+ # instance and _not_ within the scope of where the Proc/lambda was
88
+ # originally created.
89
+ # This is done to force all parameters to be passed in explicitly
90
+ # and should be read-only or duplicates of the original data
91
+ #
92
+ # The overhead for moving the task to a Minion (separate thread) vs running it
93
+ # sequentially is about 0.3 ms if performing other tasks in-between starting
94
+ # the task and requesting its result.
95
+ #
96
+ # The following call adds 0.5 ms to total processing time vs running the
97
+ # code in-line:
98
+ # ParallelMinion::Minion.new(description: 'Count', timeout: 5) { 1 }.result
99
+ #
100
+ # NOTE:
101
+ # On JRuby it is very important to add the following setting to .jrubyrc
102
+ # thread.pool.enabled=true
103
+ #
104
+ # Example:
105
+ # ParallelMinion::Minion.new(10.days.ago, description: 'Doing something else in parallel', timeout: 1000) do |date|
106
+ # MyTable.where('created_at <= ?', date).count
107
+ # end
108
+ def initialize(*args, &block)
109
+ raise "Missing mandatory block that Minion must perform" unless block
110
+ @start_time = Time.now
111
+ @exception = nil
112
+
113
+ options = self.class.extract_options!(args).dup
114
+
115
+ @timeout = (options.delete(:timeout) || Minion::INFINITE).to_f
116
+ @description = (options.delete(:description) || 'Minion').to_s
117
+ @log_exception = options.delete(:log_exception)
118
+ @synchronous = options.delete(:synchronous) || self.class.synchronous?
119
+
120
+ # Warn about any unknown options.
121
+ options.each_pair { |key,val| logger.warn "Ignoring unknown option: #{key.inspect} => #{val.inspect}" }
122
+
123
+ # Run the supplied block of code in the current thread for testing or
124
+ # debugging purposes
125
+ if @synchronous == true
126
+ begin
127
+ logger.info("Started synchronously #{@description}")
128
+ logger.benchmark_info("Completed synchronously #{@description}", log_exception: @log_exception) do
129
+ @result = instance_exec(*args, &block)
130
+ end
131
+ rescue Exception => exc
132
+ @exception = exc
133
+ end
134
+ return
135
+ end
136
+
137
+ tags = (logger.tags || []).dup
138
+
139
+ # Copy current scopes for new thread. Only applicable for AR models
140
+ scopes = self.class.current_scopes.dup if defined?(ActiveRecord::Base)
141
+
142
+ @thread = Thread.new(*args) do
143
+ # Copy logging tags from parent thread
144
+ logger.tagged(*tags) do
145
+ # Set the current thread name to the description for this Minion
146
+ # so that all log entries in this thread use this thread name
147
+ Thread.current.name = "#{@description}-#{Thread.current.object_id}"
148
+ logger.info("Started #{@description}")
149
+
150
+ begin
151
+ logger.benchmark_info("Completed #{@description}", log_exception: @log_exception) do
152
+ # Use the current scope for the duration of the task execution
153
+ if scopes.nil? || (scopes.size == 0)
154
+ @result = instance_exec(*args, &block)
155
+ else
156
+ # Each Class to scope requires passing a block to .scoping
157
+ proc = Proc.new { instance_exec(*args, &block) }
158
+ first = scopes.shift
159
+ scopes.each {|scope| proc = Proc.new { scope.scoping(&proc) } }
160
+ @result = first.scoping(&proc)
161
+ end
162
+ end
163
+ rescue Exception => exc
164
+ @exception = exc
165
+ nil
166
+ ensure
167
+ # Return any database connections used by this thread back to the pool
168
+ ActiveRecord::Base.clear_active_connections! if defined?(ActiveRecord::Base)
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ # Returns the result when the thread completes
175
+ # Returns nil if the thread has not yet completed
176
+ # Raises any unhandled exception in the thread, if any
177
+ #
178
+ # Note: The result of any thread cannot be nil
179
+ def result
180
+ # Return nil if Minion is still working and has time left to finish
181
+ if working?
182
+ ms = time_left
183
+ return if @thread.join(ms.nil? ? nil: ms / 1000).nil?
184
+ end
185
+
186
+ # Return the exception, if any, otherwise the task result
187
+ exception.nil? ? @result : Kernel.raise(exception)
188
+ end
189
+
190
+ # Returns [Boolean] whether the minion is still working on the assigned task
191
+ def working?
192
+ synchronous? ? false : @thread.alive?
193
+ end
194
+
195
+ # Returns [Boolean] whether the minion has completed working on the task
196
+ def completed?
197
+ synchronous? ? true : @thread.stop?
198
+ end
199
+
200
+ # Returns [Boolean] whether the minion failed while performing the assigned task
201
+ def failed?
202
+ !exception.nil?
203
+ end
204
+
205
+ # Returns the amount of time left in milli-seconds that this Minion has to finish its task
206
+ # Returns 0 if no time is left
207
+ # Returns nil if their is no time limit. I.e. :timeout was set to Minion::INFINITE (infinite time left)
208
+ def time_left
209
+ return nil if @timeout == INFINITE
210
+ duration = @timeout - (Time.now - @start_time) * 1000
211
+ duration <= 0 ? 0 : duration
212
+ end
213
+
214
+ # Returns [Boolean] whether synchronous mode has been enabled for this minion instance
215
+ def synchronous?
216
+ @synchronous
217
+ end
218
+
219
+ # Returns the current scopes for each of the models for which scopes will be
220
+ # copied to the Minions
221
+ def self.current_scopes
222
+ # Apparently #scoped is deprecated, but its replacement #all does not behave the same
223
+ @@scoped_classes.collect {|klass| klass.scoped.dup}
224
+ end
225
+
226
+ protected
227
+
228
+ @@synchronous = false
229
+ @@scoped_classes = []
230
+
231
+ # Extract options from a hash.
232
+ def self.extract_options!(args)
233
+ args.last.is_a?(Hash) ? args.pop : {}
234
+ end
235
+
236
+ end
237
+ end
@@ -0,0 +1,20 @@
1
+ module ParallelMinion #:nodoc:
2
+ class Railtie < Rails::Railtie #:nodoc:
3
+ #
4
+ # Make the ParallelMinion config available in the Rails application config
5
+ #
6
+ # Example: Make debugging easier
7
+ # in file config/environments/development.rb
8
+ #
9
+ # Rails::Application.configure do
10
+ #
11
+ # # Run Minions in the current thread to make debugging easier
12
+ # config.parallel_minion.synchronous = true
13
+ #
14
+ # # Add a model so that its current scope is copied to the Minion
15
+ # config.parallel_minion.scoped_classes << MyScopedModel
16
+ # end
17
+ config.parallel_minion = ::ParallelMinion::Minion
18
+
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module ParallelMinion #:nodoc
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,5 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: test/test_db.sqlite3
4
+ pool: 5
5
+ timeout: 5000
@@ -0,0 +1,74 @@
1
+ # Allow test to be run in-place without requiring a gem install
2
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
3
+
4
+ require 'semantic_logger'
5
+ # Register an appender if one is not already registered
6
+ SemanticLogger.default_level = :trace
7
+ SemanticLogger.add_appender('test.log', &SemanticLogger::Appender::Base.colorized_formatter) if SemanticLogger.appenders.size == 0
8
+
9
+ require 'rubygems'
10
+ require 'erb'
11
+ require 'test/unit'
12
+ # Since we want both the AR and Mongoid extensions loaded we need to require them first
13
+ require 'active_record'
14
+ require 'active_record/relation'
15
+ # Should redefines Proc#bind so must include after Rails
16
+ require 'shoulda'
17
+ require 'parallel_minion'
18
+
19
+ ActiveRecord::Base.logger = SemanticLogger[ActiveRecord]
20
+ ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read('test/config/database.yml')).result)
21
+ ActiveRecord::Base.establish_connection('test')
22
+
23
+ ActiveRecord::Schema.define :version => 0 do
24
+ create_table :people, :force => true do |t|
25
+ t.string :name
26
+ t.string :state
27
+ t.string :zip_code
28
+ end
29
+ end
30
+
31
+ class Person < ActiveRecord::Base
32
+ end
33
+
34
+ class MinionScopeTest < Test::Unit::TestCase
35
+
36
+ context ParallelMinion::Minion do
37
+ [false, true].each do |synchronous|
38
+ context ".new with synchronous: #{synchronous.inspect}" do
39
+ setup do
40
+ Person.create(name: 'Jack', state: 'FL', zip_code: 38729)
41
+ Person.create(name: 'John', state: 'FL', zip_code: 35363)
42
+ Person.create(name: 'Jill', state: 'FL', zip_code: 73534)
43
+ Person.create(name: 'Joe', state: 'NY', zip_code: 45325)
44
+ Person.create(name: 'Jane', state: 'NY', zip_code: 45325)
45
+ Person.create(name: 'James', state: 'CA', zip_code: 123123)
46
+ # Instruct Minions to adhere to any dynamic scopes for Person model
47
+ ParallelMinion::Minion.scoped_classes << Person
48
+ ParallelMinion::Minion.synchronous = synchronous
49
+ end
50
+
51
+ teardown do
52
+ Person.destroy_all
53
+ ParallelMinion::Minion.scoped_classes.clear
54
+ SemanticLogger.flush
55
+ end
56
+
57
+ should 'copy across model scope' do
58
+ assert_equal 6, Person.count
59
+
60
+ Person.unscoped.where(state: 'FL').scoping { Person.count }
61
+
62
+ Person.unscoped.where(state: 'FL').scoping do
63
+ assert_equal 3, Person.count
64
+ minion = ParallelMinion::Minion.new(description: 'Scope Test', log_exception: :full) do
65
+ Person.count
66
+ end
67
+ assert_equal 3, minion.result
68
+ end
69
+ end
70
+
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,109 @@
1
+ # Allow test to be run in-place without requiring a gem install
2
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
3
+
4
+ require 'test/unit'
5
+ require 'shoulda'
6
+ require 'parallel_minion'
7
+
8
+ # Register an appender if one is not already registered
9
+ SemanticLogger.default_level = :trace
10
+ SemanticLogger.add_appender('test.log', &SemanticLogger::Appender::Base.colorized_formatter) if SemanticLogger.appenders.size == 0
11
+
12
+ # Test ParallelMinion standalone without Rails
13
+ # Run this test standalone to verify it has no Rails dependencies
14
+ class MinionTest < Test::Unit::TestCase
15
+ include SemanticLogger::Loggable
16
+
17
+ context ParallelMinion::Minion do
18
+
19
+ [false, true].each do |synchronous|
20
+ context ".new with synchronous: #{synchronous.inspect}" do
21
+ setup do
22
+ ParallelMinion::Minion.synchronous = synchronous
23
+ end
24
+
25
+ should 'without parameters' do
26
+ minion = ParallelMinion::Minion.new { 196 }
27
+ assert_equal 196, minion.result
28
+ end
29
+
30
+ should 'with a description' do
31
+ minion = ParallelMinion::Minion.new(description: 'Test') { 197 }
32
+ assert_equal 197, minion.result
33
+ end
34
+
35
+ should 'with an argument' do
36
+ p1 = { name: 198 }
37
+ minion = ParallelMinion::Minion.new(p1, description: 'Test') do |v|
38
+ v[:name]
39
+ end
40
+ assert_equal 198, minion.result
41
+ end
42
+
43
+ should 'raise exception' do
44
+ minion = ParallelMinion::Minion.new(description: 'Test') { raise "An exception" }
45
+ assert_raise RuntimeError do
46
+ minion.result
47
+ end
48
+ end
49
+
50
+ # TODO Blocks still have access to their original scope if variables cannot be
51
+ # resolved first by the parameters, then by the values in Minion itself
52
+ # should 'not have access to local variables' do
53
+ # name = 'Jack'
54
+ # minion = ParallelMinion::Minion.new(description: 'Test') { puts name }
55
+ # assert_raise NameError do
56
+ # minion.result
57
+ # end
58
+ # end
59
+
60
+ should 'run minion' do
61
+ hash = { value: 23 }
62
+ value = 47
63
+ minion = ParallelMinion::Minion.new(hash, description: 'Test') do |h|
64
+ value = 321
65
+ h[:value] = 123
66
+ 456
67
+ end
68
+ assert_equal 456, minion.result
69
+ assert_equal 123, hash[:value]
70
+ assert_equal 321, value
71
+ end
72
+
73
+ should 'copy across logging tags' do
74
+ minion = nil
75
+ logger.tagged('TAG') do
76
+ assert_equal 'TAG', logger.tags.last
77
+ minion = ParallelMinion::Minion.new(description: 'Tag Test') do
78
+ logger.tags.last
79
+ end
80
+ end
81
+ assert_equal 'TAG', minion.result
82
+ end
83
+
84
+ should 'handle multiple minions concurrently' do
85
+ # Start 10 minions
86
+ minions = 10.times.collect do |i|
87
+ # Each Minion returns its index in the collection
88
+ ParallelMinion::Minion.new(i, description: "Minion:#{i}") {|counter| counter }
89
+ end
90
+ assert_equal 10, minions.count
91
+ # Fetch the result from each Minion
92
+ minions.each_with_index do |minion, index|
93
+ assert_equal index, minion.result
94
+ end
95
+ end
96
+
97
+ should 'timeout' do
98
+ minion = ParallelMinion::Minion.new(description: 'Test', timeout: 100) { sleep 1 }
99
+ # Only Parallel Minions time-out when they exceed timeout
100
+ unless synchronous
101
+ assert_equal nil, minion.result
102
+ end
103
+ end
104
+ end
105
+
106
+ end
107
+
108
+ end
109
+ end
Binary file
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: parallel_minion
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Reid Morrison
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-12-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: semantic_logger
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Parallel Minion supports easily handing work off to Minions (Threads)
28
+ so that tasks that would normally be performed sequentially can easily be executed
29
+ in parallel
30
+ email:
31
+ - reidmo@gmail.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - lib/parallel_minion/horde.rb
37
+ - lib/parallel_minion/minion.rb
38
+ - lib/parallel_minion/railtie.rb
39
+ - lib/parallel_minion/version.rb
40
+ - lib/parallel_minion.rb
41
+ - Rakefile
42
+ - README.md
43
+ - test/config/database.yml
44
+ - test/minion_scope_test.rb
45
+ - test/minion_test.rb
46
+ - test/test_db.sqlite3
47
+ homepage: https://github.com/ClarityServices/semantic_logger
48
+ licenses:
49
+ - Apache License V2.0
50
+ metadata: {}
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubyforge_project:
67
+ rubygems_version: 2.1.11
68
+ signing_key:
69
+ specification_version: 4
70
+ summary: Concurrent processing made easy with Minions (Threads)
71
+ test_files:
72
+ - test/config/database.yml
73
+ - test/minion_scope_test.rb
74
+ - test/minion_test.rb
75
+ - test/test_db.sqlite3