xpool 0.1.1 → 0.3.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/ChangeLog.txt +7 -0
- data/README.md +29 -8
- data/lib/xpool.rb +45 -21
- data/lib/xpool/process.rb +22 -4
- data/lib/xpool/version.rb +1 -1
- data/test/xpool_test.rb +19 -7
- metadata +5 -4
data/ChangeLog.txt
ADDED
data/README.md
CHANGED
@@ -4,17 +4,17 @@ __OVERVIEW__
|
|
4
4
|
|:----------------|:--------------------------------------------------
|
5
5
|
| Homepage | https://github.com/robgleeson/xpool
|
6
6
|
| Documentation | http://rubydoc.info/gems/xpool/frames
|
7
|
-
| CI | [](https://travis-ci.org/robgleeson/XPool)
|
8
8
|
| Author | Rob Gleeson
|
9
9
|
|
10
10
|
|
11
11
|
__DESCRIPTION__
|
12
12
|
|
13
|
-
A lightweight UNIX(X) Process Pool
|
13
|
+
A lightweight UNIX(X) Process Pool written in Ruby. The size of the pool
|
14
14
|
is dynamic and it can be resized at runtime if needs be. 'Units of work' are
|
15
|
-
what you can schedule and
|
16
|
-
the pool dries up(all
|
17
|
-
next available subprocess will pick it up.
|
15
|
+
what you can schedule and each unit of work is dispatched by a free subprocess
|
16
|
+
in the the pool. If the pool dries up(all subprocesses are busy) the units
|
17
|
+
of work are queued & the next available subprocess will pick it up.
|
18
18
|
|
19
19
|
There are also all the other features you might expect, such as an interface to
|
20
20
|
shutdown gracefully or to shutdown immediately. Graceful shutdowns can operate
|
@@ -25,7 +25,7 @@ __EXAMPLES__
|
|
25
25
|
|
26
26
|
_1._
|
27
27
|
|
28
|
-
A demo of how you'd create a pool of
|
28
|
+
A demo of how you'd create a pool of 5 subprocesses:
|
29
29
|
|
30
30
|
```ruby
|
31
31
|
#
|
@@ -38,7 +38,7 @@ class Unit
|
|
38
38
|
sleep 1
|
39
39
|
end
|
40
40
|
end
|
41
|
-
pool = XPool.new
|
41
|
+
pool = XPool.new 5
|
42
42
|
5.times { pool.schedule Unit.new }
|
43
43
|
pool.shutdown
|
44
44
|
```
|
@@ -68,11 +68,32 @@ class Unit
|
|
68
68
|
sleep 5
|
69
69
|
end
|
70
70
|
end
|
71
|
-
pool = XPool.new
|
71
|
+
pool = XPool.new 5
|
72
72
|
pool.schedule Unit.new
|
73
73
|
pool.shutdown 3
|
74
74
|
```
|
75
75
|
|
76
|
+
__DEBUGGING OUTPUT__
|
77
|
+
|
78
|
+
XPool can print helpful debugging information if you set `XPool.debug`
|
79
|
+
to true:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
XPool.debug = true
|
83
|
+
```
|
84
|
+
|
85
|
+
Or you can temporarily enable debugging output for the duration of a block:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
XPool.debug do
|
89
|
+
pool = XPool.new 5
|
90
|
+
pool.shutdown
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
The debugging information you'll see is all about how the pool is operating.
|
95
|
+
It can be interesting to look over even if you're not bug hunting.
|
96
|
+
|
76
97
|
__INSTALL__
|
77
98
|
|
78
99
|
$ gem install xpool
|
data/lib/xpool.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
class XPool
|
2
|
-
require 'json'
|
3
2
|
require 'ichannel'
|
4
3
|
require 'timeout'
|
5
4
|
require 'logger'
|
5
|
+
require 'rbconfig'
|
6
6
|
require_relative "xpool/version"
|
7
7
|
require_relative "xpool/process"
|
8
8
|
|
@@ -23,7 +23,7 @@ class XPool
|
|
23
23
|
@debug = boolean
|
24
24
|
end
|
25
25
|
|
26
|
-
def self.log(msg, type = :info)
|
26
|
+
def self.log(msg, type = :info)
|
27
27
|
@logger = @logger || Logger.new(STDOUT)
|
28
28
|
if @debug
|
29
29
|
@logger.public_send type, msg
|
@@ -36,9 +36,9 @@ class XPool
|
|
36
36
|
#
|
37
37
|
# @return [XPool]
|
38
38
|
#
|
39
|
-
def initialize(size=
|
39
|
+
def initialize(size=number_of_cpu_cores)
|
40
40
|
@channel = IChannel.new Marshal
|
41
|
-
@pool = Array.new size do
|
41
|
+
@pool = Array.new size do
|
42
42
|
spawn
|
43
43
|
end
|
44
44
|
end
|
@@ -46,24 +46,22 @@ class XPool
|
|
46
46
|
#
|
47
47
|
# A graceful shutdown of the pool.
|
48
48
|
#
|
49
|
-
# All busy subprocesses finish up any code they're running & exit normally
|
49
|
+
# All busy subprocesses finish up any code they're running & exit normally
|
50
50
|
# afterwards.
|
51
51
|
#
|
52
|
-
# @param [Fixnum] timeout
|
52
|
+
# @param [Fixnum] timeout
|
53
53
|
# An optional amount of seconds to wait before forcing a shutdown through
|
54
54
|
# {#shutdown!}.
|
55
55
|
#
|
56
56
|
# @see XPool::Process#spawn
|
57
|
-
#
|
57
|
+
#
|
58
58
|
# @return [void]
|
59
59
|
#
|
60
60
|
def shutdown(timeout=nil)
|
61
61
|
if timeout
|
62
62
|
begin
|
63
63
|
Timeout.timeout(timeout) do
|
64
|
-
@pool.each
|
65
|
-
process.shutdown
|
66
|
-
end
|
64
|
+
@pool.each(&:shutdown)
|
67
65
|
end
|
68
66
|
rescue Timeout::Error
|
69
67
|
XPool.log "'#{timeout}' seconds elapsed, switching to hard shutdown."
|
@@ -85,12 +83,12 @@ class XPool
|
|
85
83
|
|
86
84
|
#
|
87
85
|
# Resize the pool.
|
88
|
-
# All subprocesses in the pool are abruptly stopped through {#shutdown!} and
|
86
|
+
# All subprocesses in the pool are abruptly stopped through {#shutdown!} and
|
89
87
|
# a new pool the size of _range_ is created.
|
90
88
|
#
|
91
89
|
# @example
|
92
|
-
# pool = XPool.new
|
93
|
-
# pool.
|
90
|
+
# pool = XPool.new 5
|
91
|
+
# pool.resize! 1..3
|
94
92
|
# pool.shutdown
|
95
93
|
#
|
96
94
|
# @param [Range] range
|
@@ -108,30 +106,40 @@ class XPool
|
|
108
106
|
#
|
109
107
|
# Dispatch a unit of work in a subprocess.
|
110
108
|
#
|
111
|
-
# @param
|
109
|
+
# @param
|
112
110
|
# (see Process#schedule)
|
113
111
|
#
|
114
|
-
# @return
|
112
|
+
# @return
|
115
113
|
# (see Process#schedule)
|
116
114
|
#
|
117
115
|
def schedule(unit, *args)
|
118
116
|
@channel.put unit: unit, args: args
|
119
117
|
end
|
120
118
|
|
119
|
+
#
|
120
|
+
# @return [Fixnum]
|
121
|
+
# Returns the number of alive subprocesses in the pool.
|
122
|
+
#
|
123
|
+
def size
|
124
|
+
@pool.count do |process|
|
125
|
+
process.alive?
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
121
129
|
private
|
122
130
|
def spawn
|
123
131
|
pid = fork do
|
124
132
|
trap :SIGUSR1 do
|
125
133
|
XPool.log "#{::Process.pid} got request to shutdown."
|
126
|
-
@shutdown_requested = true
|
134
|
+
@shutdown_requested = true
|
127
135
|
end
|
128
136
|
loop do
|
129
137
|
begin
|
130
138
|
#
|
131
|
-
# I've noticed that select can wait an infinite amount of time for
|
132
|
-
# a UNIXSocket to become readable. It usually happens on the tenth or
|
133
|
-
# so iteration. By checking if we have data to read first we elimate
|
134
|
-
# this problem but it is a band aid for a bigger issue I don't
|
139
|
+
# I've noticed that select can wait an infinite amount of time for
|
140
|
+
# a UNIXSocket to become readable. It usually happens on the tenth or
|
141
|
+
# so iteration. By checking if we have data to read first we elimate
|
142
|
+
# this problem but it is a band aid for a bigger issue I don't
|
135
143
|
# understand right now.
|
136
144
|
#
|
137
145
|
if @channel.readable?
|
@@ -146,6 +154,22 @@ private
|
|
146
154
|
end
|
147
155
|
end
|
148
156
|
end
|
149
|
-
Process.new pid
|
157
|
+
Process.new pid
|
158
|
+
end
|
159
|
+
|
160
|
+
#
|
161
|
+
# Count the number of CPU cores available.
|
162
|
+
#
|
163
|
+
def number_of_cpu_cores
|
164
|
+
case RbConfig::CONFIG['host_os']
|
165
|
+
when /linux/
|
166
|
+
Dir.glob('/sys/devices/system/cpu/cpu[0-9]*').count
|
167
|
+
when /darwin|bsd/
|
168
|
+
Integer(`sysctl -n hw.ncpu`)
|
169
|
+
when /solaris/
|
170
|
+
Integer(`kstat -m cpu_info | grep -w core_id | uniq | wc -l`)
|
171
|
+
else
|
172
|
+
5
|
173
|
+
end
|
150
174
|
end
|
151
175
|
end
|
data/lib/xpool/process.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
class XPool::Process
|
2
2
|
#
|
3
|
-
# @param [Fixnum] id
|
3
|
+
# @param [Fixnum] id
|
4
4
|
# The Process ID.
|
5
5
|
#
|
6
6
|
def initialize(id)
|
@@ -10,8 +10,8 @@ class XPool::Process
|
|
10
10
|
#
|
11
11
|
# A graceful shutdown of the process.
|
12
12
|
#
|
13
|
-
# The signal 'SIGUSR1' is caught in the subprocess and exit is
|
14
|
-
# performed through Kernel#exit after the process has finished
|
13
|
+
# The signal 'SIGUSR1' is caught in the subprocess and exit is
|
14
|
+
# performed through Kernel#exit after the process has finished
|
15
15
|
# executing its work.
|
16
16
|
#
|
17
17
|
# @return [void]
|
@@ -28,13 +28,31 @@ class XPool::Process
|
|
28
28
|
def shutdown!
|
29
29
|
_shutdown 'SIGKILL'
|
30
30
|
end
|
31
|
-
|
31
|
+
|
32
|
+
#
|
33
|
+
# @return [Boolean]
|
34
|
+
# Returns true when the process is alive.
|
35
|
+
#
|
36
|
+
def alive?
|
37
|
+
!dead?
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# @return [Boolean]
|
42
|
+
# Returns true when the process is no longer running.
|
43
|
+
#
|
44
|
+
def dead?
|
45
|
+
@dead
|
46
|
+
end
|
47
|
+
|
32
48
|
private
|
33
49
|
def _shutdown(sig)
|
34
50
|
begin
|
35
51
|
Process.kill sig, @id
|
36
52
|
Process.wait @id
|
37
53
|
rescue SystemCallError
|
54
|
+
ensure
|
55
|
+
@dead = true
|
38
56
|
end
|
39
57
|
end
|
40
58
|
end
|
data/lib/xpool/version.rb
CHANGED
data/test/xpool_test.rb
CHANGED
@@ -10,7 +10,7 @@ class XPoolTest < Test::Unit::TestCase
|
|
10
10
|
def initialize
|
11
11
|
file = Tempfile.new '__xpool_test'
|
12
12
|
@path = file.path
|
13
|
-
file.close
|
13
|
+
file.close false
|
14
14
|
end
|
15
15
|
|
16
16
|
def run
|
@@ -34,9 +34,21 @@ class XPoolTest < Test::Unit::TestCase
|
|
34
34
|
def teardown
|
35
35
|
@pool.shutdown
|
36
36
|
end
|
37
|
-
|
38
37
|
|
39
|
-
|
38
|
+
|
39
|
+
def test_size_with_graceful_shutdown
|
40
|
+
assert_equal 5, @pool.size
|
41
|
+
@pool.shutdown
|
42
|
+
assert_equal 0, @pool.size
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_size_with_forceful_shutdown
|
46
|
+
assert_equal 5, @pool.size
|
47
|
+
@pool.shutdown!
|
48
|
+
assert_equal 0, @pool.size
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_queue
|
40
52
|
@pool.resize! 1..1
|
41
53
|
units = Array.new(5) { XUnit.new }
|
42
54
|
units.each do |unit|
|
@@ -48,18 +60,18 @@ class XPoolTest < Test::Unit::TestCase
|
|
48
60
|
end
|
49
61
|
end
|
50
62
|
|
51
|
-
def test_parallelism
|
52
|
-
5.times do
|
63
|
+
def test_parallelism
|
64
|
+
5.times do
|
53
65
|
@pool.schedule Unit.new
|
54
66
|
end
|
55
67
|
assert_nothing_raised Timeout::Error do
|
56
|
-
Timeout.timeout 2 do
|
68
|
+
Timeout.timeout 2 do
|
57
69
|
@pool.shutdown
|
58
70
|
end
|
59
71
|
end
|
60
72
|
end
|
61
73
|
|
62
|
-
def test_resize!
|
74
|
+
def test_resize!
|
63
75
|
@pool.resize! 1..1
|
64
76
|
assert_equal 1, @pool.instance_variable_get(:@pool).size
|
65
77
|
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.
|
4
|
+
version: 0.3.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: 2012-12-
|
12
|
+
date: 2012-12-29 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: ichannel
|
@@ -35,6 +35,7 @@ extensions: []
|
|
35
35
|
extra_rdoc_files: []
|
36
36
|
files:
|
37
37
|
- .gitignore
|
38
|
+
- ChangeLog.txt
|
38
39
|
- Gemfile
|
39
40
|
- LICENSE.txt
|
40
41
|
- README.md
|
@@ -59,7 +60,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
59
60
|
version: '0'
|
60
61
|
segments:
|
61
62
|
- 0
|
62
|
-
hash: -
|
63
|
+
hash: -2000550100409961336
|
63
64
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
65
|
none: false
|
65
66
|
requirements:
|
@@ -68,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
69
|
version: '0'
|
69
70
|
segments:
|
70
71
|
- 0
|
71
|
-
hash: -
|
72
|
+
hash: -2000550100409961336
|
72
73
|
requirements: []
|
73
74
|
rubyforge_project:
|
74
75
|
rubygems_version: 1.8.23
|