parallel_minion 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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