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.
- checksums.yaml +7 -0
- data/README.md +234 -0
- data/Rakefile +23 -0
- data/lib/parallel_minion.rb +8 -0
- data/lib/parallel_minion/horde.rb +41 -0
- data/lib/parallel_minion/minion.rb +237 -0
- data/lib/parallel_minion/railtie.rb +20 -0
- data/lib/parallel_minion/version.rb +3 -0
- data/test/config/database.yml +5 -0
- data/test/minion_scope_test.rb +74 -0
- data/test/minion_test.rb +109 -0
- data/test/test_db.sqlite3 +0 -0
- metadata +75 -0
checksums.yaml
ADDED
@@ -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
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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,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,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
|
data/test/minion_test.rb
ADDED
@@ -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
|