xpool 0.9.0.2 → 0.10.0

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.
data/.travis.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  rvm:
2
- - 1.9.2
2
+ # - 1.9.2
3
3
  - 1.9.3
4
4
  - 2.0.0
5
5
  - ruby-head
data/.yardopts ADDED
@@ -0,0 +1,5 @@
1
+ -m markdown -M redcarpet
2
+ -
3
+ README.md
4
+ LICENSE.txt
5
+ ChangeLog.txt
data/ChangeLog.txt CHANGED
@@ -1,6 +1,52 @@
1
+ == v0.10.0
2
+ - default to two subprocesses if CPU core count cannot be guessed.
3
+ Which is change from the old default of five.
4
+
5
+ - add XPool#resize
6
+ Resize the pool at runtime.
7
+ If subprocesses are removed from the pool a graceful shutdown is performed on
8
+ each subprocess that is removed (unlike resize!, which forces a shutdown).
9
+
10
+ - add XPool#expand
11
+ Expand the pool with X subprocesses, where X is the argument to expand(…).
12
+
13
+ - add XPool#shrink
14
+ Shrink the pool by X subprocesses, where X is the argument to shrink(…).
15
+ A graceful shutdown is performed on any subprocesses removed from the pool.
16
+
17
+ - add XPool#shrink!
18
+ Shrink the pool by X subprocesses, where X is the argument to shrink!(…)
19
+ A forceful shutdown is performed on any subprocesses removed from the pool.
20
+
21
+ - XPool#resize! now accepts a Fixnum, not a Range.
22
+ If you want to resize the pool to two subprocesses, the API is now:
23
+ pool.resize!(2)
24
+
25
+ - XPool#failed_processes persists after a shutdown
26
+ If a process in the pool has failed and the pool is subsequently shutdown the
27
+ failed process will remain in XPool#failed_processes. When a failed process
28
+ is restarted the pool is repopulated with an active subprocess and can be
29
+ used to schedule work again.
30
+
31
+ - optimize XPool#resize!
32
+ A few optimizations:
33
+
34
+ * when new_size == current_size, there's no resize. nothing happens. it's
35
+ a no-op.
36
+
37
+ * when new_size > current_size, only X new subprocesses are spawned, where X is
38
+ the difference between "new_size - current_size".
39
+
40
+ * when new_size < current_size, all subprocesses indexed after new_size are
41
+ shutdown and removed from the pool. no new subprocesses are spawned.
42
+
43
+ This new behavior reuses subprocesses that are already active in the pool when
44
+ it can, where as before we always spawned new subprocesses, no matter what the
45
+ case.
46
+
1
47
  == v0.9.0.1,v0.9.0.2
2
48
  - doc improvements
3
- revised & improved the README & API documentation.
49
+ Revised & improved the README & API documentation.
4
50
 
5
51
  == v0.9.0
6
52
  - upgrade to ichannel v5.1.1
data/Gemfile CHANGED
@@ -3,5 +3,6 @@ group :development do
3
3
  gem 'rake'
4
4
  gem 'pry'
5
5
  gem 'ruby-prof'
6
+ gem 'mocha'
6
7
  end
7
8
  gemspec
data/README.md CHANGED
@@ -10,37 +10,50 @@ __OVERVIEW__
10
10
 
11
11
  __DESCRIPTION__
12
12
 
13
- xpool is a lightweight process pool. The pool manages a group of subprocesses
14
- that are used when the pool is asked to dispatch a 'unit of work'. A
15
- 'unit of work' is defined as any object that implements the `run` method.
16
-
17
- All subprocesses in the pool have their own message queue that the pool places
18
- work onto according to a very simple algorithm: the subprocess who has scheduled
19
- the least amount of work is the subprocess who is asked to put the work on its
20
- queue. This helps ensure an even distribution of work among all subprocesses in
21
- the pool. The message queue that each subprocess has is also what ensures
22
- work can be queued when the pool becomes dry (all subprocesses are busy).
23
-
24
- Incase a unit of work raises an exception that it does not handle xpool will
25
- rescue the exception and mark the process as 'failed'. A failed process can be
26
- restarted, and it is also possible to access the backtrace of a failed process
27
- through `XPool` and `XPool::Process` objects. The exception is also re-raised
28
- so that you can see a process has failed from the output ruby prints when an
29
- exception is left unhandled.
30
-
31
- __POOL SIZE__
13
+ xpool is a lightweight process pool. A pool manages a group of subprocesses
14
+ that are used when it is asked to dispatch a 'unit of work'. A 'unit of work'
15
+ is defined as any object that implements the `run` method.
16
+
17
+ In order to send a 'unit of work' between processes each subprocess has its own
18
+ 'message queue' that the pool writes to when it has been asked to schedule a
19
+ unit of work. A unit of work is serialized(on write to queue), and
20
+ deserialized(on read from queue). The serializer used under the hood is called
21
+ [Marshal](http://rubydoc.info/stdlib/core/Marshal) and might be familiar to
22
+ you already.
23
+
24
+ The logic for scheduling a unit of work is straightforward. A pool asks each
25
+ and every subprocess under its control how frequently its message queue has
26
+ been written to. The subprocess with the queue that has the least writes is told
27
+ to schedule the next unit of work. In practical terms this means if you have a
28
+ pool with five subprocesses and schedule a unit of work five times, each
29
+ subprocess in the pool would have executed the unit of work once.
30
+
31
+ A pool can become "dry" whenever all its subprocesses are busy. If you schedule
32
+ a unit of work on a dry pool the same scheduling logic apllies but instead of
33
+ the unit of work executing right away it will be executed whenever the
34
+ assigned subprocess is no longer busy. It is also possible to query the pool
35
+ and ask if it is dry, but you can also ask an individual subprocess if it is
36
+ busy.
32
37
 
33
38
  By default xpool will create a pool with X subprocesses, where X is the number
34
39
  of cores on your CPU. This seems like a reasonable default, but if you should
35
40
  decide to choose otherwise you can set the size of the pool when it is
36
- initialized. The pool can also be resized at runtime if you decide you need to
37
- scale up or down.
41
+ initialized. The pool can also be resized at runtime if you decide you need
42
+ to scale up or down.
43
+
44
+ A unit of work may fail whenever an exception is left unhandled. When this
45
+ happens xpool rescues the exception, marks the process as "failed", and
46
+ re-raises the exception so that the failure can be seen. Finally, the process
47
+ running the unit of work exits, and pool is down one process. A failed process
48
+ can be restarted and interacted with, though, so it is possible to recover.
38
49
 
39
50
  __EXAMPLES__
40
51
 
41
52
  The examples don't demonstrate everything that xpool can do. The
42
- [API docs](http://rubydoc.info/github/robgleeson/xpool)
43
- cover the missing pieces.
53
+ [API docs](http://rubydoc.info/github/robgleeson/xpool)
54
+ and
55
+ [docs/](https://github.com/robgleeson/xpool/tree/master/docs)
56
+ directory cover the missing pieces.
44
57
 
45
58
  _1._
46
59
 
@@ -0,0 +1,43 @@
1
+ __ABSTRACT__
2
+
3
+ A unit of work may fail whenever an exception is left unhandled. When this
4
+ happens xpool rescues the exception, marks the process as "failed", and
5
+ re-raises the exception so that the failure can be seen. Finally, the
6
+ process running the unit of work exits, and pool is down one process. A failed
7
+ process can be restarted and interacted with, though, so it is possible to
8
+ recover.
9
+
10
+ __DETAILS__
11
+
12
+ A quick summary of the classes and methods that are most interesting when
13
+ interacting with failed processes:
14
+
15
+ - [XPool#failed_processes](http://rubydoc.info/github/robgleeson/xpool/XPool#failed_processes-instance_method)
16
+ Returns an array of
17
+ [XPool::Process](http://rubydoc.info/github/robgleeson/xpool/XPool/Process)
18
+ objects that are in a failed state.
19
+
20
+ - [XPool::Process](http://rubydoc.info/github/robgleeson/xpool/XPool/Process)
21
+ Provides an object oriented interface on top of a subprocess in the pool.
22
+ The most interesting methods when a process is in a 'failed' state might be
23
+ [XPool::Process#restart](http://rubydoc.info/github/robgleeson/xpool/XPool/Process#restart-instance_method)
24
+ and
25
+ [XPool::Process#backtrace](http://rubydoc.info/github/robgleeson/xpool/XPool/Process#backtrace-instance_method).
26
+
27
+ __EXAMPLES__
28
+
29
+ __1.__
30
+
31
+ ```ruby
32
+ class Unit
33
+ def run
34
+ raise RuntimeError, "", []
35
+ end
36
+ end
37
+
38
+ pool = XPool.new 2
39
+ pool.schedule Unit.new
40
+ sleep 0.05
41
+ pool.failed_processes.each(&:restart)
42
+ pool.shutdown
43
+ ```
data/lib/xpool.rb CHANGED
@@ -41,6 +41,55 @@ class XPool
41
41
  @pool = Array.new(size) { Process.new }
42
42
  end
43
43
 
44
+ #
45
+ # @param [Fixnum] number
46
+ # The number of subprocesses to add to the pool.
47
+ #
48
+ # @return
49
+ # (see XPool#resize!)
50
+ #
51
+ def expand(number)
52
+ resize! size + number
53
+ end
54
+
55
+ #
56
+ # @param [Fixnum] number
57
+ # The number of subprocesses to remove from the pool.
58
+ # A graceful shutdown is performed.
59
+ #
60
+ # @raise
61
+ # (see XPool#shrink!)
62
+ #
63
+ # @return
64
+ # (see Xpool#shrink!)
65
+ #
66
+ def shrink(number)
67
+ present_size = size
68
+ raise_if number > present_size,
69
+ ArgumentError,
70
+ "cannot shrink pool by #{number}. pool is only #{present_size} in size."
71
+ resize present_size - number
72
+ end
73
+
74
+ #
75
+ # @param [Fixnum] number
76
+ # The number of subprocesses to remove from the pool.
77
+ # A forceful shutdown is performed.
78
+ #
79
+ # @raise [ArgumentError]
80
+ # When _number_ is greater than {#size}.
81
+ #
82
+ # @return
83
+ # (see XPool#resize!)
84
+ #
85
+ def shrink!(number)
86
+ present_size = size
87
+ raise_if number > present_size,
88
+ ArgumentError,
89
+ "cannot shrink pool by #{number}. pool is only #{present_size} in size."
90
+ resize! present_size - number
91
+ end
92
+
44
93
  #
45
94
  # @return [Array<XPool::Process>]
46
95
  # Returns an Array of failed processes.
@@ -104,23 +153,32 @@ class XPool
104
153
  end
105
154
 
106
155
  #
107
- # Resize the pool.
108
- # All subprocesses in the pool are abruptly stopped through {#shutdown!} and
109
- # a new pool the size of _range_ is created.
156
+ # Resize the pool (gracefully, if neccesary)
157
+ #
158
+ # @param
159
+ # (see XPool#resize!)
160
+ #
161
+ # @return [void]
162
+ #
163
+ def resize(new_size)
164
+ _resize new_size, false
165
+ end
166
+
167
+ #
168
+ # Resize the pool (with force, if neccesary).
110
169
  #
111
170
  # @example
112
171
  # pool = XPool.new 5
113
- # pool.resize! 1..3
172
+ # pool.resize! 3
114
173
  # pool.shutdown
115
174
  #
116
- # @param [Range] range
175
+ # @param [Fixnum] new_size
117
176
  # The new size of the pool.
118
177
  #
119
178
  # @return [void]
120
179
  #
121
- def resize!(range)
122
- shutdown!
123
- @pool = range.to_a.map { Process.new }
180
+ def resize!(new_size)
181
+ _resize new_size, true
124
182
  end
125
183
 
126
184
  #
@@ -149,9 +207,7 @@ class XPool
149
207
  # Returns the number of alive subprocesses in the pool.
150
208
  #
151
209
  def size
152
- @pool.count do |process|
153
- process.alive?
154
- end
210
+ @pool.count(&:alive?)
155
211
  end
156
212
 
157
213
  #
@@ -162,9 +218,13 @@ class XPool
162
218
  @pool.all?(&:busy?)
163
219
  end
164
220
 
165
- #
166
- # Count the number of CPU cores available.
167
- #
221
+ private
222
+ def raise_if(predicate, e, m)
223
+ if predicate
224
+ raise e, m
225
+ end
226
+ end
227
+
168
228
  def number_of_cpu_cores
169
229
  case RbConfig::CONFIG['host_os']
170
230
  when /linux/
@@ -174,7 +234,26 @@ class XPool
174
234
  when /solaris/
175
235
  Integer(`kstat -m cpu_info | grep -w core_id | uniq | wc -l`)
176
236
  else
177
- 5
237
+ 2
238
+ end
239
+ end
240
+
241
+ def _resize(new_size, with_force)
242
+ if Range === new_size
243
+ warn "[DEPRECATED] XPool#resize! no longer accepts a Range." \
244
+ "Please use a Fixnum instead."
245
+ new_size = range.to_a.size
246
+ end
247
+ new_size -= 1
248
+ old_size = size - 1
249
+ if new_size == old_size
250
+ # do nothing
251
+ elsif new_size < old_size
252
+ meth = with_force ? :shutdown! : :shutdown
253
+ @pool[new_size+1..old_size].each(&meth)
254
+ @pool = @pool[0..new_size]
255
+ else
256
+ @pool += Array.new(new_size - old_size) { Process.new }
178
257
  end
179
258
  end
180
259
  end
data/lib/xpool/process.rb CHANGED
@@ -130,7 +130,7 @@ private
130
130
  Process.wait @id
131
131
  rescue Errno::ECHILD, Errno::ESRCH
132
132
  ensure
133
- @states = {dead: true}
133
+ @states = {dead: true} unless failed?
134
134
  @shutdown = true
135
135
  @channel.close
136
136
  @s_channel.close
data/lib/xpool/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class XPool
2
- VERSION = "0.9.0.2"
2
+ VERSION = "0.10.0"
3
3
  end
data/test/setup.rb CHANGED
@@ -2,6 +2,7 @@ require 'bundler/setup'
2
2
  require 'xpool'
3
3
  require 'test/unit'
4
4
  require 'fileutils'
5
+ require 'mocha/setup'
5
6
  require_relative 'support/sleeper'
6
7
  require_relative 'support/io_writer'
7
8
  require_relative 'support/raiser'
data/test/xpool_test.rb CHANGED
@@ -1,10 +1,12 @@
1
1
  require_relative 'setup'
2
2
  class XPoolTest < Test::Unit::TestCase
3
+ POOL_SIZE = 2
3
4
  def setup
4
- @pool = XPool.new 5
5
+ @pool = XPool.new POOL_SIZE
5
6
  end
6
7
 
7
8
  def teardown
9
+ mocha_teardown
8
10
  @pool.shutdown!
9
11
  end
10
12
 
@@ -14,38 +16,38 @@ class XPoolTest < Test::Unit::TestCase
14
16
  end
15
17
 
16
18
  def test_size_with_graceful_shutdown
17
- assert_equal 5, @pool.size
19
+ assert_equal POOL_SIZE, @pool.size
18
20
  @pool.shutdown
19
21
  assert_equal 0, @pool.size
20
22
  end
21
23
 
22
24
  def test_size_with_forceful_shutdown
23
- assert_equal 5, @pool.size
25
+ assert_equal POOL_SIZE, @pool.size
24
26
  @pool.shutdown!
25
27
  assert_equal 0, @pool.size
26
28
  end
27
29
 
28
30
  def test_queue
29
- @pool.resize! 1..1
30
- writers = Array.new(5) { IOWriter.new }
31
+ @pool.resize! 1
32
+ writers = Array.new(POOL_SIZE) { IOWriter.new }
31
33
  writers.each { |writer| @pool.schedule writer }
32
34
  @pool.shutdown
33
35
  writers.each { |writer| assert writer.wrote_to_disk? }
34
36
  end
35
37
 
36
38
  def test_distribution_of_work
37
- subprocesses = (0..4).map { @pool.schedule Sleeper.new(0.1) }
39
+ subprocesses = Array.new(POOL_SIZE) { @pool.schedule Sleeper.new(0.1) }
38
40
  subprocesses.each { |subprocess| assert_equal 1, subprocess.frequency }
39
41
  end
40
42
 
41
43
  def test_resize!
42
- @pool.resize! 1..1
44
+ @pool.resize! 1
43
45
  assert_equal 1, @pool.instance_variable_get(:@pool).size
44
46
  end
45
47
 
46
48
  def test_dry?
47
49
  refute @pool.dry?
48
- 5.times { @pool.schedule Sleeper.new(0.5) }
50
+ POOL_SIZE.times { @pool.schedule Sleeper.new(0.5) }
49
51
  sleep 0.1
50
52
  assert @pool.dry?
51
53
  end
@@ -54,6 +56,42 @@ class XPoolTest < Test::Unit::TestCase
54
56
  @pool.schedule Raiser.new
55
57
  sleep 0.1
56
58
  assert_equal 1, @pool.failed_processes.size
57
- assert_equal 4, @pool.size
59
+ assert_equal POOL_SIZE - 1, @pool.size
60
+ end
61
+
62
+ def test_failed_processes_after_shutdown
63
+ @pool.schedule Raiser.new
64
+ @pool.shutdown
65
+ refute @pool.failed_processes.empty?
66
+ end
67
+
68
+ def test_failed_process_to_repopulate_pool
69
+ @pool.schedule Raiser.new
70
+ @pool.shutdown
71
+ @pool.failed_processes.each(&:restart)
72
+ assert_equal 1, @pool.size
73
+ end
74
+
75
+ def test_expand
76
+ @pool.expand 1
77
+ assert_equal POOL_SIZE + 1, @pool.size
78
+ end
79
+
80
+ def test_shrink
81
+ XPool::Process.any_instance.expects(:shutdown).once
82
+ @pool.shrink 1
83
+ assert_equal POOL_SIZE - 1, @pool.size
84
+ end
85
+
86
+ def test_shrink!
87
+ XPool::Process.any_instance.expects(:shutdown!).once
88
+ @pool.shrink! 1
89
+ assert_equal POOL_SIZE - 1, @pool.size
90
+ end
91
+
92
+ def test_shrink_with_excess_number
93
+ assert_raises ArgumentError do
94
+ @pool.shrink! POOL_SIZE + 1
95
+ end
58
96
  end
59
97
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xpool
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0.2
4
+ version: 0.10.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-22 00:00:00.000000000 Z
12
+ date: 2013-03-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: ichannel
@@ -37,6 +37,7 @@ files:
37
37
  - .gitignore
38
38
  - .pryrc
39
39
  - .travis.yml
40
+ - .yardopts
40
41
  - ChangeLog.txt
41
42
  - Gemfile
42
43
  - LICENSE.txt
@@ -44,6 +45,7 @@ files:
44
45
  - Rakefile
45
46
  - bench/.gitkeep
46
47
  - bench/pool-schedule.rb
48
+ - docs/unhandled_exceptions.md
47
49
  - lib/xpool.rb
48
50
  - lib/xpool/process.rb
49
51
  - lib/xpool/version.rb
@@ -68,7 +70,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
68
70
  version: '0'
69
71
  segments:
70
72
  - 0
71
- hash: -610761221262095322
73
+ hash: -4088700810187342227
72
74
  required_rubygems_version: !ruby/object:Gem::Requirement
73
75
  none: false
74
76
  requirements:
@@ -77,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
79
  version: '0'
78
80
  segments:
79
81
  - 0
80
- hash: -610761221262095322
82
+ hash: -4088700810187342227
81
83
  requirements: []
82
84
  rubyforge_project:
83
85
  rubygems_version: 1.8.23