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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 733102f569568fbb2f2953e74298840e6cddd625
4
- data.tar.gz: 0753643bc032abc303a72f212dab29f7691338ec
3
+ metadata.gz: 335d81b6d4f243e4d8516d63f27444fc786f1b43
4
+ data.tar.gz: a4918eb5ca7855e60056abef9441735ee63e474e
5
5
  SHA512:
6
- metadata.gz: 8b34b053dba7b457a0d4561c5fff71f14a8fa7e96314c0c9d209c539f874255da769ecd89b1fd96e4e8d1011c4dce4c4ef9b30ddbdd67f9ce8147e3396b93193
7
- data.tar.gz: dd1101a603f7f83a28af4ebfd74734daadeecadeeef069987ced8947d5a73b5b527fda0a8f5040bd47409ecd39972b5a8d2e6b502f8f8813f942887fb160dff3
6
+ metadata.gz: d1fa0022f0fa6d41f715ba08677cb4a8ad6863b9d2da73ccee86d42612c1693a56c92bf61abe7a427d51a46d13eae10242a5d5bd250ba18b6e46a35efed1854d
7
+ data.tar.gz: b3a59f59fdf5d17bfcc62ee2772f9a15c835dce742e6e65daf1ab28478a45fd2d19dd409ea4582a9f3b951e18f942948e3e27a1ba1e5ce9007b39f91eb18d15d
@@ -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.2"
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-07-15"
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.require_paths = ["lib"]
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
@@ -1,5 +1,6 @@
1
1
  module Chantier
2
- VERSION = '0.0.2'
2
+ VERSION = '0.0.3'
3
3
  require_relative 'process_pool'
4
+ require_relative 'process_pool_with_kill'
4
5
  require_relative 'thread_pool'
5
6
  end
@@ -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
@@ -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.2
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-07-15 00:00:00.000000000 Z
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: ' Process your jobs in parallel with a simple table of processes or threads '
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.0.3
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