chantier 0.0.2 → 0.0.3
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 +4 -4
- data/chantier.gemspec +7 -4
- data/lib/chantier.rb +2 -1
- data/lib/process_pool.rb +2 -5
- data/lib/process_pool_with_kill.rb +52 -0
- data/spec/process_pool_spec.rb +4 -1
- data/spec/process_pool_with_kill_spec.rb +125 -0
- metadata +20 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 335d81b6d4f243e4d8516d63f27444fc786f1b43
|
4
|
+
data.tar.gz: a4918eb5ca7855e60056abef9441735ee63e474e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d1fa0022f0fa6d41f715ba08677cb4a8ad6863b9d2da73ccee86d42612c1693a56c92bf61abe7a427d51a46d13eae10242a5d5bd250ba18b6e46a35efed1854d
|
7
|
+
data.tar.gz: b3a59f59fdf5d17bfcc62ee2772f9a15c835dce742e6e65daf1ab28478a45fd2d19dd409ea4582a9f3b951e18f942948e3e27a1ba1e5ce9007b39f91eb18d15d
|
data/chantier.gemspec
CHANGED
@@ -2,14 +2,16 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
+
# stub: chantier 0.0.3 ruby lib
|
5
6
|
|
6
7
|
Gem::Specification.new do |s|
|
7
8
|
s.name = "chantier"
|
8
|
-
s.version = "0.0.
|
9
|
+
s.version = "0.0.3"
|
9
10
|
|
10
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
|
+
s.require_paths = ["lib"]
|
11
13
|
s.authors = ["Julik Tarkhanov"]
|
12
|
-
s.date = "2014-
|
14
|
+
s.date = "2014-08-02"
|
13
15
|
s.description = " Process your jobs in parallel with a simple table of processes or threads "
|
14
16
|
s.email = "me@julik.nl"
|
15
17
|
s.extra_rdoc_files = [
|
@@ -26,15 +28,16 @@ Gem::Specification.new do |s|
|
|
26
28
|
"chantier.gemspec",
|
27
29
|
"lib/chantier.rb",
|
28
30
|
"lib/process_pool.rb",
|
31
|
+
"lib/process_pool_with_kill.rb",
|
29
32
|
"lib/thread_pool.rb",
|
30
33
|
"spec/process_pool_spec.rb",
|
34
|
+
"spec/process_pool_with_kill_spec.rb",
|
31
35
|
"spec/spec_helper.rb",
|
32
36
|
"spec/thread_pool_spec.rb"
|
33
37
|
]
|
34
38
|
s.homepage = "http://github.com/julik/chantier"
|
35
39
|
s.licenses = ["MIT"]
|
36
|
-
s.
|
37
|
-
s.rubygems_version = "2.0.3"
|
40
|
+
s.rubygems_version = "2.2.2"
|
38
41
|
s.summary = "Dead-simple worker table based multiprocessing/multithreading"
|
39
42
|
|
40
43
|
if s.respond_to? :specification_version then
|
data/lib/chantier.rb
CHANGED
data/lib/process_pool.rb
CHANGED
@@ -26,11 +26,6 @@
|
|
26
26
|
#
|
27
27
|
# Can be rewritten using Threads if operation on JVM/Rubinius will be feasible.
|
28
28
|
class Chantier::ProcessPool
|
29
|
-
# Kill the spawned processes after at most X seconds
|
30
|
-
# KILL_AFTER_SECONDS = 60 * 2
|
31
|
-
|
32
|
-
# http://linuxman.wikispaces.com/killing+me+softly
|
33
|
-
# TERMINATION_SIGNALS = %w( TERM HUP INT QUIT PIPE KILL )
|
34
29
|
|
35
30
|
# The manager uses loops in a few places. By doing a little sleep()
|
36
31
|
# in those loops we can yield process control back to the OS which brings
|
@@ -97,6 +92,8 @@ class Chantier::ProcessPool
|
|
97
92
|
@semaphore.synchronize { @pids[destination_slot_idx] = nil }
|
98
93
|
end
|
99
94
|
|
95
|
+
return task_pid
|
96
|
+
|
100
97
|
# Dispatch the killer thread which kicks in after KILL_AFTER_SECONDS.
|
101
98
|
# Note that we do not manage the @pids table here because once the process
|
102
99
|
# gets terminated it will bounce back to the standard wait() above.
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# Allows you to spin off a pool of subprocesses that is not larger than X, and
|
2
|
+
# maintains a pool of those proceses (same as ProcessPool). Will also forcibly quit
|
3
|
+
# those processes after a certain period to ensure they do not hang
|
4
|
+
#
|
5
|
+
# manager = ProcessPoolWithKill.new(slots = 4, kill_after = 5) # seconds
|
6
|
+
# jobs_hose.each_job do | job |
|
7
|
+
# # this call will block until a slot becomes available
|
8
|
+
# manager.fork_task do # this block runs in a subprocess
|
9
|
+
# Churner.new(job).churn
|
10
|
+
# end
|
11
|
+
# manager.still_running? # => most likely "true"
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# manager.block_until_complete! #=> Will block until all the subprocesses have terminated
|
15
|
+
class Chantier::ProcessPoolWithKill < Chantier::ProcessPool
|
16
|
+
|
17
|
+
# http://linuxman.wikispaces.com/killing+me+softly
|
18
|
+
TERMINATION_SIGNALS = %w( TERM HUP INT QUIT PIPE KILL )
|
19
|
+
|
20
|
+
DEFAULT_KILL_TIMEOUT = 60
|
21
|
+
def initialize(num_procs, kill_after_seconds = DEFAULT_KILL_TIMEOUT)
|
22
|
+
@kill_after_seconds = kill_after_seconds.to_f
|
23
|
+
super(num_procs)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Run the given block in a forked subprocess. This method will block
|
27
|
+
# the thread it is called from until a slot in the process table
|
28
|
+
# becomes free. Once that happens, the given block will be forked off
|
29
|
+
# and the method will return.
|
30
|
+
def fork_task(&blk)
|
31
|
+
task_pid = super
|
32
|
+
Thread.abort_on_exception = true
|
33
|
+
# Dispatch the killer thread which kicks in after KILL_AFTER_SECONDS.
|
34
|
+
# Note that we do not manage the @pids table here because once the process
|
35
|
+
# gets terminated it will bounce back to the standard wait() above.
|
36
|
+
Thread.new do
|
37
|
+
sleep @kill_after_seconds
|
38
|
+
TERMINATION_SIGNALS.each do | sig |
|
39
|
+
puts "Dispatching #{sig}"
|
40
|
+
begin
|
41
|
+
Process.kill(sig, task_pid)
|
42
|
+
sleep 1 # Give it some time to react
|
43
|
+
rescue Errno::ESRCH
|
44
|
+
# It has already quit, nothing to do
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
return task_pid
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
data/spec/process_pool_spec.rb
CHANGED
@@ -55,10 +55,13 @@ describe Chantier::ProcessPool do
|
|
55
55
|
|
56
56
|
it 'processes 1 file' do
|
57
57
|
filename = @files[0]
|
58
|
-
manager.fork_task do
|
58
|
+
pid = manager.fork_task do
|
59
59
|
sleep(0.05 + (rand / 10))
|
60
60
|
File.open(filename, "wb"){|f| f.write("Worker completed") }
|
61
61
|
end
|
62
|
+
|
63
|
+
expect(pid).to be_kind_of(Fixnum)
|
64
|
+
|
62
65
|
manager.block_until_complete!
|
63
66
|
|
64
67
|
expect(File.read(filename)).to eq('Worker completed')
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe Chantier::ProcessPoolWithKill do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@files = (0...20).map do
|
7
|
+
SecureRandom.hex(12).tap { |filename| FileUtils.touch(filename) }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
after(:each) do
|
12
|
+
@files.map(&File.method(:unlink))
|
13
|
+
end
|
14
|
+
|
15
|
+
context '#map_fork' do
|
16
|
+
let(:manager) { described_class.new(5) }
|
17
|
+
|
18
|
+
it 'processes multiple files' do
|
19
|
+
|
20
|
+
data_chunks = (0..10).map{|e| Digest::SHA1.hexdigest(e.to_s) }
|
21
|
+
|
22
|
+
expect(manager).not_to be_still_running
|
23
|
+
|
24
|
+
manager.map_fork(@files) do | filename |
|
25
|
+
sleep(0.05 + (rand / 10))
|
26
|
+
File.open(filename, "wb"){|f| f.write("Worker completed for #{filename}") }
|
27
|
+
end
|
28
|
+
|
29
|
+
expect(manager).not_to be_still_running
|
30
|
+
|
31
|
+
@files.each do | filename |
|
32
|
+
expect(File.read(filename)).to eq("Worker completed for #{filename}")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'with 0 concurrent slots' do
|
38
|
+
it 'raises an exception' do
|
39
|
+
expect {
|
40
|
+
described_class.new(0)
|
41
|
+
}.to raise_error(RuntimeError, 'Need at least 1 slot, given 0')
|
42
|
+
|
43
|
+
expect {
|
44
|
+
described_class.new(-1)
|
45
|
+
}.to raise_error(RuntimeError, 'Need at least 1 slot, given -1')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'with 1 slot' do
|
50
|
+
let(:manager) { described_class.new(1) }
|
51
|
+
|
52
|
+
it 'processes 1 file' do
|
53
|
+
filename = @files[0]
|
54
|
+
pid = manager.fork_task do
|
55
|
+
sleep(0.05 + (rand / 10))
|
56
|
+
File.open(filename, "wb"){|f| f.write("Worker completed") }
|
57
|
+
end
|
58
|
+
|
59
|
+
expect(pid).to be_kind_of(Fixnum)
|
60
|
+
|
61
|
+
manager.block_until_complete!
|
62
|
+
|
63
|
+
expect(File.read(filename)).to eq('Worker completed')
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'processes multiple files' do
|
67
|
+
expect(manager).not_to be_still_running
|
68
|
+
|
69
|
+
@files.each do | filename |
|
70
|
+
manager.fork_task do
|
71
|
+
sleep(0.05 + (rand / 10))
|
72
|
+
File.open(filename, "wb"){|f| f.write("Worker completed for #{filename}") }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
expect(manager).to be_still_running
|
77
|
+
|
78
|
+
manager.block_until_complete!
|
79
|
+
|
80
|
+
@files.each do | filename |
|
81
|
+
expect(File.read(filename)).to eq("Worker completed for #{filename}")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'forcibly quites a process that is hung for too long' do
|
86
|
+
manager_with_short_timeout = described_class.new(1, timeout=0.4)
|
87
|
+
|
88
|
+
filename = SecureRandom.hex(22)
|
89
|
+
manager_with_short_timeout.fork_task do
|
90
|
+
10.times do
|
91
|
+
sleep 1 # WAY longer than the timeout
|
92
|
+
end
|
93
|
+
File.open(filename, "wb") {|f| f.write("Should never happen")}
|
94
|
+
end
|
95
|
+
|
96
|
+
manager_with_short_timeout.block_until_complete!
|
97
|
+
expect(File.exist?(filename)).to eq(false)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context 'with 5 slots' do
|
102
|
+
let(:manager) { described_class.new(5) }
|
103
|
+
|
104
|
+
it 'processes multiple files' do
|
105
|
+
|
106
|
+
expect(manager).not_to be_still_running
|
107
|
+
|
108
|
+
@files.each do | filename |
|
109
|
+
manager.fork_task do
|
110
|
+
sleep(0.05 + (rand / 10))
|
111
|
+
File.open(filename, "wb"){|f| f.write("Worker completed for #{filename}") }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
expect(manager).to be_still_running
|
116
|
+
|
117
|
+
manager.block_until_complete!
|
118
|
+
|
119
|
+
@files.each do | filename |
|
120
|
+
expect(File.read(filename)).to eq("Worker completed for #{filename}")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
metadata
CHANGED
@@ -1,86 +1,86 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: chantier
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-08-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - ~>
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '2.9'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - ~>
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.9'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rdoc
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - ~>
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '3.12'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - ~>
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '3.12'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: bundler
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - ~>
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '1.0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - ~>
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '1.0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: jeweler
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - ~>
|
59
|
+
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: 2.0.1
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - ~>
|
66
|
+
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 2.0.1
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: simplecov
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- -
|
73
|
+
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
75
|
version: '0'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- -
|
80
|
+
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
-
description:
|
83
|
+
description: " Process your jobs in parallel with a simple table of processes or threads "
|
84
84
|
email: me@julik.nl
|
85
85
|
executables: []
|
86
86
|
extensions: []
|
@@ -88,8 +88,8 @@ extra_rdoc_files:
|
|
88
88
|
- LICENSE.txt
|
89
89
|
- README.rdoc
|
90
90
|
files:
|
91
|
-
- .document
|
92
|
-
- .rspec
|
91
|
+
- ".document"
|
92
|
+
- ".rspec"
|
93
93
|
- Gemfile
|
94
94
|
- LICENSE.txt
|
95
95
|
- README.rdoc
|
@@ -97,8 +97,10 @@ files:
|
|
97
97
|
- chantier.gemspec
|
98
98
|
- lib/chantier.rb
|
99
99
|
- lib/process_pool.rb
|
100
|
+
- lib/process_pool_with_kill.rb
|
100
101
|
- lib/thread_pool.rb
|
101
102
|
- spec/process_pool_spec.rb
|
103
|
+
- spec/process_pool_with_kill_spec.rb
|
102
104
|
- spec/spec_helper.rb
|
103
105
|
- spec/thread_pool_spec.rb
|
104
106
|
homepage: http://github.com/julik/chantier
|
@@ -111,17 +113,17 @@ require_paths:
|
|
111
113
|
- lib
|
112
114
|
required_ruby_version: !ruby/object:Gem::Requirement
|
113
115
|
requirements:
|
114
|
-
- -
|
116
|
+
- - ">="
|
115
117
|
- !ruby/object:Gem::Version
|
116
118
|
version: '0'
|
117
119
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
120
|
requirements:
|
119
|
-
- -
|
121
|
+
- - ">="
|
120
122
|
- !ruby/object:Gem::Version
|
121
123
|
version: '0'
|
122
124
|
requirements: []
|
123
125
|
rubyforge_project:
|
124
|
-
rubygems_version: 2.
|
126
|
+
rubygems_version: 2.2.2
|
125
127
|
signing_key:
|
126
128
|
specification_version: 4
|
127
129
|
summary: Dead-simple worker table based multiprocessing/multithreading
|