xpool 0.1.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/.gitignore +17 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +77 -0
- data/Rakefile +9 -0
- data/lib/xpool.rb +151 -0
- data/lib/xpool/process.rb +40 -0
- data/lib/xpool/version.rb +3 -0
- data/test/setup.rb +5 -0
- data/test/xpool_test.rb +66 -0
- data/xpool.gemspec +18 -0
- metadata +80 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Rob Gleeson
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
__OVERVIEW__
|
2
|
+
|
3
|
+
| Project | XPool
|
4
|
+
|:----------------|:--------------------------------------------------
|
5
|
+
| Homepage | https://github.com/robgleeson/xpool
|
6
|
+
| Documentation | http://rubydoc.info/gems/xpool/frames
|
7
|
+
| CI | [](https://travis-ci.org/robgleeson/XPool)
|
8
|
+
| Author | Rob Gleeson
|
9
|
+
|
10
|
+
|
11
|
+
__DESCRIPTION__
|
12
|
+
|
13
|
+
A lightweight UNIX(X) Process Pool implementation. The size of the pool
|
14
|
+
is dynamic and it can be resized at runtime if needs be. 'Units of work' are
|
15
|
+
what you can schedule and they are dispatched by a subprocess in the pool. If
|
16
|
+
the pool dries up(all processes are busy) the units of work are queued & the
|
17
|
+
next available subprocess will pick it up.
|
18
|
+
|
19
|
+
There are also all the other features you might expect, such as an interface to
|
20
|
+
shutdown gracefully or to shutdown immediately. Graceful shutdowns can operate
|
21
|
+
within a timeout that when passed shuts down the pool immediately.
|
22
|
+
|
23
|
+
|
24
|
+
__EXAMPLES__
|
25
|
+
|
26
|
+
_1._
|
27
|
+
|
28
|
+
A demo of how you'd create a pool of 10 subprocesses:
|
29
|
+
|
30
|
+
#
|
31
|
+
# Make sure you define your units of work before
|
32
|
+
# you create a process pool or you'll get strange
|
33
|
+
# serialization errors.
|
34
|
+
#
|
35
|
+
class Unit
|
36
|
+
def run
|
37
|
+
sleep 1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
pool = XPool.new 10
|
41
|
+
5.times { pool.schedule Unit.new }
|
42
|
+
pool.shutdown
|
43
|
+
|
44
|
+
_2._
|
45
|
+
|
46
|
+
A demo of how you'd resize the pool from 10 to 5 subprocesses at runtime:
|
47
|
+
|
48
|
+
class Unit
|
49
|
+
def run
|
50
|
+
sleep 5
|
51
|
+
end
|
52
|
+
end
|
53
|
+
pool = XPool.new 10
|
54
|
+
pool.resize! 1..5
|
55
|
+
pool.shutdown
|
56
|
+
|
57
|
+
_3._
|
58
|
+
|
59
|
+
A demo of how you'd gracefully shutdown but force a hard shutdown if 3 seconds
|
60
|
+
pass by & all subprocesses have not exited:
|
61
|
+
|
62
|
+
class Unit
|
63
|
+
def run
|
64
|
+
sleep 5
|
65
|
+
end
|
66
|
+
end
|
67
|
+
pool = XPool.new 10
|
68
|
+
pool.schedule Unit.new
|
69
|
+
pool.shutdown 3
|
70
|
+
|
71
|
+
__INSTALL__
|
72
|
+
|
73
|
+
$ gem install xpool
|
74
|
+
|
75
|
+
__LICENSE__
|
76
|
+
|
77
|
+
MIT. See `LICENSE.txt`
|
data/Rakefile
ADDED
data/lib/xpool.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
class XPool
|
2
|
+
require 'json'
|
3
|
+
require 'ichannel'
|
4
|
+
require 'timeout'
|
5
|
+
require 'logger'
|
6
|
+
require_relative "xpool/version"
|
7
|
+
require_relative "xpool/process"
|
8
|
+
|
9
|
+
def self.debug
|
10
|
+
if block_given?
|
11
|
+
begin
|
12
|
+
@debug = true
|
13
|
+
yield
|
14
|
+
ensure
|
15
|
+
@debug = false
|
16
|
+
end
|
17
|
+
else
|
18
|
+
@debug
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.debug=(boolean)
|
23
|
+
@debug = boolean
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.log(msg, type = :info)
|
27
|
+
@logger = @logger || Logger.new(STDOUT)
|
28
|
+
if @debug
|
29
|
+
@logger.public_send type, msg
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# @param [Fixnum] size
|
35
|
+
# The number of subprocesses to spawn.
|
36
|
+
#
|
37
|
+
# @return [XPool]
|
38
|
+
#
|
39
|
+
def initialize(size=10)
|
40
|
+
@channel = IChannel.new Marshal
|
41
|
+
@pool = Array.new size do
|
42
|
+
spawn
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# A graceful shutdown of the pool.
|
48
|
+
#
|
49
|
+
# All busy subprocesses finish up any code they're running & exit normally
|
50
|
+
# afterwards.
|
51
|
+
#
|
52
|
+
# @param [Fixnum] timeout
|
53
|
+
# An optional amount of seconds to wait before forcing a shutdown through
|
54
|
+
# {#shutdown!}.
|
55
|
+
#
|
56
|
+
# @see XPool::Process#spawn
|
57
|
+
#
|
58
|
+
# @return [void]
|
59
|
+
#
|
60
|
+
def shutdown(timeout=nil)
|
61
|
+
if timeout
|
62
|
+
begin
|
63
|
+
Timeout.timeout(timeout) do
|
64
|
+
@pool.each do |process|
|
65
|
+
process.shutdown
|
66
|
+
end
|
67
|
+
end
|
68
|
+
rescue Timeout::Error
|
69
|
+
XPool.log "'#{timeout}' seconds elapsed, switching to hard shutdown."
|
70
|
+
shutdown!
|
71
|
+
end
|
72
|
+
else
|
73
|
+
@pool.each(&:shutdown)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# A forceful shutdown of the pool (through SIGKILL).
|
79
|
+
#
|
80
|
+
# @return [void]
|
81
|
+
#
|
82
|
+
def shutdown!
|
83
|
+
@pool.each(&:shutdown!)
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# Resize the pool.
|
88
|
+
# All subprocesses in the pool are abruptly stopped through {#shutdown!} and
|
89
|
+
# a new pool the size of _range_ is created.
|
90
|
+
#
|
91
|
+
# @example
|
92
|
+
# pool = XPool.new 10
|
93
|
+
# pool.resiz!e 1..5
|
94
|
+
# pool.shutdown
|
95
|
+
#
|
96
|
+
# @param [Range] range
|
97
|
+
# The new size of the pool.
|
98
|
+
#
|
99
|
+
# @return [void]
|
100
|
+
#
|
101
|
+
def resize!(range)
|
102
|
+
shutdown!
|
103
|
+
@pool = range.to_a.map do
|
104
|
+
spawn
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# Dispatch a unit of work in a subprocess.
|
110
|
+
#
|
111
|
+
# @param
|
112
|
+
# (see Process#schedule)
|
113
|
+
#
|
114
|
+
# @return
|
115
|
+
# (see Process#schedule)
|
116
|
+
#
|
117
|
+
def schedule(unit, *args)
|
118
|
+
@channel.put unit: unit, args: args
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
def spawn
|
123
|
+
pid = fork do
|
124
|
+
trap :SIGUSR1 do
|
125
|
+
XPool.log "#{::Process.pid} got request to shutdown."
|
126
|
+
@shutdown_requested = true
|
127
|
+
end
|
128
|
+
loop do
|
129
|
+
begin
|
130
|
+
#
|
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
|
135
|
+
# understand right now.
|
136
|
+
#
|
137
|
+
if @channel.readable?
|
138
|
+
msg = @channel.get
|
139
|
+
msg[:unit].run *msg[:args]
|
140
|
+
end
|
141
|
+
ensure
|
142
|
+
if @shutdown_requested && !@channel.readable?
|
143
|
+
XPool.log "#{::Process.pid} is about to exit."
|
144
|
+
break
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
Process.new pid
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class XPool::Process
|
2
|
+
#
|
3
|
+
# @param [Fixnum] id
|
4
|
+
# The Process ID.
|
5
|
+
#
|
6
|
+
def initialize(id)
|
7
|
+
@id = id
|
8
|
+
end
|
9
|
+
|
10
|
+
#
|
11
|
+
# A graceful shutdown of the process.
|
12
|
+
#
|
13
|
+
# The signal 'SIGUSR1' is caught in the subprocess and exit is
|
14
|
+
# performed through Kernel#exit after the process has finished
|
15
|
+
# executing its work.
|
16
|
+
#
|
17
|
+
# @return [void]
|
18
|
+
#
|
19
|
+
def shutdown
|
20
|
+
_shutdown 'SIGUSR1'
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# A non-graceful shutdown through SIGKILL.
|
25
|
+
#
|
26
|
+
# @return [void]
|
27
|
+
#
|
28
|
+
def shutdown!
|
29
|
+
_shutdown 'SIGKILL'
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def _shutdown(sig)
|
34
|
+
begin
|
35
|
+
Process.kill sig, @id
|
36
|
+
Process.wait @id
|
37
|
+
rescue SystemCallError
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/test/setup.rb
ADDED
data/test/xpool_test.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require_relative 'setup'
|
2
|
+
class XPoolTest < Test::Unit::TestCase
|
3
|
+
class Unit
|
4
|
+
def run
|
5
|
+
sleep 1
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class XUnit
|
10
|
+
def initialize
|
11
|
+
file = Tempfile.new '__xpool_test'
|
12
|
+
@path = file.path
|
13
|
+
file.close
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
File.open @path, 'w' do |f|
|
18
|
+
f.write 'true'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def run?
|
23
|
+
return @run if defined?(@run)
|
24
|
+
@run = File.read(@path) == 'true'
|
25
|
+
FileUtils.rm_rf @path
|
26
|
+
@run
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def setup
|
31
|
+
@pool = XPool.new 5
|
32
|
+
end
|
33
|
+
|
34
|
+
def teardown
|
35
|
+
@pool.shutdown
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def test_queue
|
40
|
+
@pool.resize! 1..1
|
41
|
+
units = Array.new(5) { XUnit.new }
|
42
|
+
units.each do |unit|
|
43
|
+
@pool.schedule unit
|
44
|
+
end
|
45
|
+
@pool.shutdown
|
46
|
+
units.each do |unit|
|
47
|
+
assert unit.run?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_parallelism
|
52
|
+
5.times do
|
53
|
+
@pool.schedule Unit.new
|
54
|
+
end
|
55
|
+
assert_nothing_raised Timeout::Error do
|
56
|
+
Timeout.timeout 2 do
|
57
|
+
@pool.shutdown
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_resize!
|
63
|
+
@pool.resize! 1..1
|
64
|
+
assert_equal 1, @pool.instance_variable_get(:@pool).size
|
65
|
+
end
|
66
|
+
end
|
data/xpool.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/xpool/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Robert Gleeson"]
|
6
|
+
gem.email = ["rob@flowof.info"]
|
7
|
+
gem.description = %q{A lightweight UNIX(X) Process Pool implementation}
|
8
|
+
gem.summary = gem.description
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "xpool"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = XPool::VERSION
|
17
|
+
gem.add_runtime_dependency 'ichannel', '~> 4.1.0'
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xpool
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Robert Gleeson
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-01 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: ichannel
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 4.1.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 4.1.0
|
30
|
+
description: A lightweight UNIX(X) Process Pool implementation
|
31
|
+
email:
|
32
|
+
- rob@flowof.info
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- .gitignore
|
38
|
+
- Gemfile
|
39
|
+
- LICENSE.txt
|
40
|
+
- README.md
|
41
|
+
- Rakefile
|
42
|
+
- lib/xpool.rb
|
43
|
+
- lib/xpool/process.rb
|
44
|
+
- lib/xpool/version.rb
|
45
|
+
- test/setup.rb
|
46
|
+
- test/xpool_test.rb
|
47
|
+
- xpool.gemspec
|
48
|
+
homepage: ''
|
49
|
+
licenses: []
|
50
|
+
post_install_message:
|
51
|
+
rdoc_options: []
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ! '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
hash: 977068148446885427
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
segments:
|
70
|
+
- 0
|
71
|
+
hash: 977068148446885427
|
72
|
+
requirements: []
|
73
|
+
rubyforge_project:
|
74
|
+
rubygems_version: 1.8.23
|
75
|
+
signing_key:
|
76
|
+
specification_version: 3
|
77
|
+
summary: A lightweight UNIX(X) Process Pool implementation
|
78
|
+
test_files:
|
79
|
+
- test/setup.rb
|
80
|
+
- test/xpool_test.rb
|