sisyphus 0.2.4 → 0.2.5

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
- SHA1:
3
- metadata.gz: cba21106d39eba3304360aef31ce2a159d7a0213
4
- data.tar.gz: e04b2f8ed6b63981e40c8f5bd99286ee1251f24e
2
+ SHA256:
3
+ metadata.gz: 5b0d7aeac8761a528b7d084dcf254c284d541f1aabf3c728e35e5f25233fa8fe
4
+ data.tar.gz: 2358bceac26513eeab3f712138bbcd98cfda0fa9e3f3d38a1cdbbabacb762487
5
5
  SHA512:
6
- metadata.gz: 68ec3f435ab8161d7510d849f4131b014b12c84660db5347c5fdc7c0717b1f3259efb979616d2f259f043d02861eab4103a5d971c533867e237055e2e3452c8e
7
- data.tar.gz: a0e650e2792a69f15ca6b0b6e3c5ad1b3403533d5c5992ce01d70524fe43b545a294dbfb5e35a62fd132da09b51e74d6460f762c048f3f3783801b0f2495278a
6
+ metadata.gz: 66317b7e08c7c1de9125e1300811e808641fc76dc29230bcdff93ec66e1a1a1f8841b4565cdbd54709517501f283b81dbcb5e1ba4ceaeefe5f5ccfa7566437bd
7
+ data.tar.gz: cc2fb6929498e3c6ab44eddbd6aa7439cfef3357fdff42e11998f25941c2c44a522b6a7375800fa35068ddd2964bf2869e0f81b01cdafc08ed2546088299f327
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -1,56 +1,23 @@
1
1
  module Sisyphus
2
2
  class ForkingExecutionStrategy
3
3
 
4
- attr_reader :logger
5
-
6
- def initialize(logger)
7
- @logger = logger
8
- end
9
-
10
- def execute(job, error_handler = ->{})
4
+ def execute(job, error_handler = ->(process_name, error) {})
11
5
  if @child_pid = fork
12
- error_handler.call unless success?
6
+ ChildProcess.new(@child_pid).success?
13
7
  else
14
- perform job
15
- end
16
- end
17
-
18
- class ChildProcess
19
- attr_reader :pid
20
-
21
- def initialize(pid)
22
- @pid = pid
23
- end
24
-
25
- def success?
26
- status.success?
27
- end
28
-
29
- private
30
-
31
- def status
32
- _, status = ::Process.waitpid2 pid
33
- status
8
+ perform job, error_handler
34
9
  end
35
10
  end
36
11
 
37
12
  private
38
13
 
39
- def success?
40
- child_process.success?
41
- end
42
-
43
- def child_process
44
- ChildProcess.new(@child_pid)
45
- end
46
-
47
- def perform(job)
14
+ def perform(job, error_handler)
48
15
  self.process_name = "Child of worker #{::Process.ppid}"
49
16
  begin
50
17
  job.perform
51
18
  exit! 0
52
19
  rescue ::Exception => e
53
- logger.warn(process_name) { e }
20
+ error_handler.call process_name, e
54
21
  exit! 1
55
22
  end
56
23
  end
@@ -63,5 +30,23 @@ module Sisyphus
63
30
  $0 = name
64
31
  end
65
32
 
33
+ class ChildProcess
34
+ attr_reader :pid
35
+
36
+ def initialize(pid)
37
+ @pid = pid
38
+ end
39
+
40
+ def success?
41
+ status.success?
42
+ end
43
+
44
+ def status
45
+ _, status = ::Process.waitpid2 pid
46
+ status
47
+ end
48
+
49
+ end
50
+
66
51
  end
67
52
  end
@@ -1,7 +1,8 @@
1
1
  require 'timeout'
2
- require_relative './forking_execution_strategy'
3
2
  require_relative './worker'
3
+ require_relative './forking_execution_strategy'
4
4
  require_relative './null_logger'
5
+ require_relative './worker_pool'
5
6
 
6
7
  module Sisyphus
7
8
  class Master
@@ -9,13 +10,15 @@ module Sisyphus
9
10
 
10
11
  HANDLED_SIGNALS = [:INT, :TTIN, :TTOU]
11
12
 
12
- attr_reader :logger, :job, :number_of_workers
13
+ attr_reader :logger, :job, :number_of_workers, :execution_strategy
13
14
 
14
15
  def initialize(job, options = {})
15
16
  self.number_of_workers = options.fetch :workers, 0
16
17
  @logger = options.fetch(:logger) { NullLogger.new }
17
- @execution_strategy = options.fetch(:execution_strategy) { ForkingExecutionStrategy }
18
- @workers = []
18
+ @execution_strategy = options.fetch(:execution_strategy) { ForkingExecutionStrategy.new }
19
+
20
+ @worker_pool = options.fetch(:worker_pool) { WorkerPool.new self }
21
+
19
22
  @job = job
20
23
 
21
24
  self_reader, self_writer = IO.pipe
@@ -27,33 +30,15 @@ module Sisyphus
27
30
  def start
28
31
  trap_signals
29
32
  number_of_workers.times do
30
- spawn_worker
33
+ @worker_pool.spawn_worker
31
34
  sleep rand(1000).fdiv(1000)
32
35
  end
33
36
  puts "Sisyphus::Master started with PID: #{Process.pid}"
34
37
  watch_for_output
35
38
  end
36
39
 
37
- def spawn_worker
38
- reader, writer = IO.pipe
39
- if wpid = fork
40
- writer.close
41
- workers << { pid: wpid, reader: reader }
42
- else
43
- reader.close
44
- self.process_name = "Worker #{Process.pid}"
45
- worker = create_worker(writer)
46
- start_worker worker
47
- end
48
- end
49
-
50
- def start_worker(worker)
51
- worker.setup
52
- worker.start
53
- rescue Exception => e
54
- worker.error_handler.call
55
- logger.warn(process_name) { e }
56
- exit! 0
40
+ def create_worker
41
+ Worker.new(job, execution_strategy, logger)
57
42
  end
58
43
 
59
44
  def stop_worker(wpid)
@@ -70,7 +55,7 @@ module Sisyphus
70
55
  Timeout.timeout(30) do
71
56
  watch_for_shutdown while worker_count > 0
72
57
  end
73
- rescue e
58
+ rescue Timeout::Error => e
74
59
  p "Timeout reached:", e
75
60
  end
76
61
  end
@@ -81,20 +66,15 @@ module Sisyphus
81
66
 
82
67
  private
83
68
 
84
- attr_reader :workers
85
69
  attr_writer :number_of_workers
86
70
 
87
- def create_worker(writer)
88
- Worker.new(job, writer, execution_strategy)
89
- end
90
-
91
- def execution_strategy
92
- @execution_strategy.new logger
71
+ def workers
72
+ @worker_pool.workers
93
73
  end
94
74
 
95
75
  def watch_for_shutdown
96
76
  wpid, _ = Process.wait2
97
- worker = @workers.find { |w| w.fetch(:pid) == wpid }
77
+ worker = workers.find { |w| w.fetch(:pid) == wpid }
98
78
  worker.fetch(:reader).close
99
79
  workers.delete worker
100
80
  wpid
@@ -112,7 +92,7 @@ module Sisyphus
112
92
  end
113
93
 
114
94
  def process_signal_queue
115
- handle_signal(Thread.main[:signal_queue].shift) until Thread.main[:signal_queue].empty?
95
+ handle_signal(signal_queue.shift) until signal_queue.empty?
116
96
  end
117
97
 
118
98
  def process_pipes(pipes)
@@ -131,7 +111,7 @@ module Sisyphus
131
111
  end
132
112
 
133
113
  def respawn_worker(wpid)
134
- spawn_worker
114
+ @worker_pool.spawn_worker
135
115
  stop_worker wpid
136
116
  watch_for_shutdown
137
117
  end
@@ -161,7 +141,7 @@ module Sisyphus
161
141
  end
162
142
 
163
143
  def queue_signal(signal)
164
- Thread.main[:signal_queue] << signal
144
+ signal_queue << signal
165
145
  @selfpipe[:writer].write_nonblock('.')
166
146
  rescue Errno::EAGAIN
167
147
  # Ignore
@@ -191,7 +171,7 @@ module Sisyphus
191
171
 
192
172
  def handle_ttin
193
173
  self.number_of_workers += 1
194
- spawn_worker
174
+ @worker_pool.spawn_worker
195
175
  end
196
176
 
197
177
  def handle_ttou
@@ -209,12 +189,8 @@ module Sisyphus
209
189
  @stopping
210
190
  end
211
191
 
212
- def process_name=(name)
213
- $0 = name
214
- end
215
-
216
- def process_name
217
- $0
192
+ def signal_queue
193
+ Thread.main[:signal_queue]
218
194
  end
219
195
  end
220
196
  end
@@ -1,17 +1,10 @@
1
1
  module Sisyphus
2
2
  class SimpleExecutionStrategy
3
3
 
4
- attr_reader :logger
5
-
6
- def initialize(logger)
7
- @logger = logger
8
- end
9
-
10
- def execute(job, error_handler = ->{})
4
+ def execute(job, error_handler = ->(name, error) {})
11
5
  job.perform
12
6
  rescue Exception => e
13
- logger.warn(process_name) { e }
14
- error_handler.call
7
+ error_handler.call process_name, e
15
8
  end
16
9
 
17
10
  private
@@ -2,25 +2,34 @@ module Sisyphus
2
2
  class Worker
3
3
  UNCAUGHT_ERROR = '.'
4
4
 
5
- attr_reader :execution_strategy, :job, :output
5
+ attr_reader :logger, :execution_strategy, :job, :output, :to_master
6
6
 
7
- def initialize(job, output, execution_strategy)
7
+ def initialize(job, execution_strategy, logger)
8
8
  @job = job
9
- @output = output
9
+ @to_master, @output = IO.pipe
10
10
  @execution_strategy = execution_strategy
11
+ @logger = logger
12
+ @set_up = false
11
13
  end
12
14
 
13
- def setup
14
- job.setup if job.respond_to? :setup
15
+ def start
16
+ setup
17
+ run
15
18
  end
16
19
 
17
- def start
20
+ def setup
18
21
  trap_signals
22
+ job.setup if job.respond_to? :setup
23
+ setup_done
24
+ rescue Exception => e
25
+ error_handler.call "Setup", e
26
+ end
19
27
 
28
+ def run
20
29
  loop do
21
30
  break if stopped?
22
31
  perform_job
23
- end
32
+ end if set_up?
24
33
 
25
34
  exit! 0
26
35
  end
@@ -30,27 +39,45 @@ module Sisyphus
30
39
  end
31
40
 
32
41
  def error_handler
33
- -> {
42
+ -> (name, error) {
43
+ return if stopped?
34
44
  begin
35
- output.write UNCAUGHT_ERROR unless stopped?
45
+ logger.warn(name) { error }
46
+ output.write UNCAUGHT_ERROR
36
47
  rescue Errno::EAGAIN, Errno::EINTR
37
48
  # Ignore
38
49
  end
39
50
  }
40
51
  end
41
52
 
53
+ def stop
54
+ @stopped = true
55
+ end
56
+
57
+ def atfork_parent
58
+ output.close
59
+ end
60
+
61
+ def atfork_child
62
+ to_master.close
63
+ end
64
+
42
65
  private
43
66
 
67
+ def set_up?
68
+ @set_up
69
+ end
70
+
71
+ def setup_done
72
+ @set_up = true
73
+ end
74
+
44
75
  def trap_signals
45
76
  Signal.trap('INT') do
46
77
  stop
47
78
  end
48
79
  end
49
80
 
50
- def stop
51
- @stopped = true
52
- end
53
-
54
81
  def stopped?
55
82
  @stopped
56
83
  end
@@ -0,0 +1,36 @@
1
+ require_relative './worker'
2
+
3
+ module Sisyphus
4
+ class WorkerPool
5
+
6
+ attr_reader :workers, :worker_factory
7
+
8
+ def initialize(worker_factory)
9
+ @worker_factory = worker_factory
10
+ @workers = []
11
+ end
12
+
13
+ def spawn_worker
14
+ worker = create_worker
15
+ if wpid = fork
16
+ worker.atfork_parent
17
+ workers << { pid: wpid, reader: worker.to_master }
18
+ else
19
+ worker.atfork_child
20
+ self.process_name = "Worker #{Process.pid}"
21
+ worker.start
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def create_worker
28
+ worker_factory.create_worker
29
+ end
30
+
31
+ def process_name=(name)
32
+ $0 = name
33
+ end
34
+
35
+ end
36
+ end
data/lib/sisyphus.rb CHANGED
@@ -1,8 +1,6 @@
1
1
  require "version"
2
- require 'sisyphus/forking_execution_strategy'
3
2
  require 'sisyphus/simple_execution_strategy'
4
3
  require 'sisyphus/job'
5
- require 'sisyphus/worker'
6
4
  require 'sisyphus/master'
7
5
 
8
6
  module Sisyphus
data/lib/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Sisyphus
2
- VERSION = "0.2.4"
2
+ VERSION = "0.2.5"
3
3
  end
data/sisyphus.gemspec CHANGED
@@ -18,6 +18,8 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ spec.required_ruby_version = '>= 1.9.3'
22
+
21
23
  spec.add_development_dependency "bundler", "~> 1.3"
22
24
  spec.add_development_dependency "rake", "~> 10.0"
23
25
  spec.add_development_dependency "rspec", "~> 2.14"
@@ -1,50 +1,37 @@
1
+ require 'spec_helper'
1
2
  require_relative '../../lib/sisyphus/forking_execution_strategy'
2
3
 
3
4
  module Sisyphus
4
5
  describe ForkingExecutionStrategy do
5
6
 
6
- let(:logger) { double :logger }
7
7
  let(:job) { double :job }
8
- let(:error_handler) { double :error_handler }
9
- let(:strategy) { ForkingExecutionStrategy.new logger }
8
+ let(:error_handler) { ->(name, raised_error) {} }
9
+ let(:strategy) { ForkingExecutionStrategy.new }
10
10
  let(:child_pid) { 1 }
11
+ let(:status) { double :process_status }
11
12
 
12
13
  it 'forks on execution' do
13
- allow(strategy).to receive(:success?) { true }
14
+ allow(Process).to receive(:waitpid2).with(child_pid) { [child_pid, status] }
15
+ allow(status).to receive(:success?) { true }
14
16
  expect(strategy).to receive(:fork) { child_pid }
15
17
  strategy.execute job
16
18
  end
17
19
 
18
20
  context 'in the parent process' do
19
21
 
20
- let(:child_process) { double :child_process }
21
-
22
22
  before :each do
23
23
  allow(strategy).to receive(:fork) { child_pid }
24
+ allow(status).to receive(:success?) { true }
24
25
  end
25
26
 
26
- it 'calls error_handler if execution is unsuccessful' do
27
- allow(strategy).to receive(:success?) { false }
28
- expect(error_handler).to receive(:call)
29
- strategy.execute job, error_handler
30
- end
31
-
32
- it 'does not call error_handler if execution is successful' do
33
- allow(strategy).to receive(:success?) { true }
34
- expect(error_handler).not_to receive(:call)
35
- strategy.execute job, error_handler
36
- end
37
-
38
- it 'instantiates a child_process' do
39
- allow(child_process).to receive(:success?) { true }
40
- expect(ForkingExecutionStrategy::ChildProcess).to receive(:new).with(child_pid) { child_process }
41
- strategy.execute job, error_handler
27
+ it 'waits for the execution to finish' do
28
+ expect(Process).to receive(:waitpid2).with(child_pid) { [child_pid, status] }
29
+ strategy.execute(job, error_handler)
42
30
  end
43
31
 
44
- it 'checks if the child process is successful' do
45
- expect(child_process).to receive(:success?) { true }
46
- allow(strategy).to receive(:child_process) { child_process }
47
- strategy.execute job, error_handler
32
+ it 'gets the status of the child process' do
33
+ allow(Process).to receive(:waitpid2) { [child_pid, status] }
34
+ expect(strategy.execute job, error_handler).to eq(true)
48
35
  end
49
36
 
50
37
  end
@@ -75,47 +62,36 @@ module Sisyphus
75
62
  strategy.execute job
76
63
  end
77
64
 
78
- it 'logs the exception if job is performed and it fails' do
79
- process_name = "foobarbaz"
80
- exception_message = "foo"
81
- allow(strategy).to receive(:process_name) { process_name }
82
- allow(job).to receive(:perform) { raise Exception, exception_message }
83
- expect(logger).to receive(:warn).with(process_name)
84
- strategy.execute job
85
- end
86
-
87
- it 'exits with a 1 status if job is performed and it fails' do
88
- process_name = "foobarbaz"
89
- allow(strategy).to receive(:process_name) { process_name }
90
- allow(job).to receive(:perform) { raise "foo" }
91
- allow(logger).to receive(:warn)
92
- expect(strategy).to receive(:exit!).with(1)
93
- strategy.execute job
65
+ it 'does not call error_handler if execution is successful' do
66
+ allow(strategy).to receive(:exit!).with(0)
67
+ strategy.execute job, ->(n, e) { fail "Should not be called" }
94
68
  end
95
69
 
96
- end
70
+ context 'when the job#perform fails' do
97
71
 
98
- describe ForkingExecutionStrategy::ChildProcess do
72
+ let(:process_name) { "foobarbaz" }
73
+ let(:exception) { Exception.new("foo") }
99
74
 
100
- let(:pid) { 1 }
101
- let(:status) { double :status }
102
- let(:child_process) { ForkingExecutionStrategy::ChildProcess.new pid }
75
+ before :each do
76
+ allow(strategy).to receive(:process_name) { process_name }
77
+ allow(job).to receive(:perform).and_raise(exception)
78
+ end
103
79
 
104
- it 'waits for the process to finish' do
105
- expect(Process).to receive(:waitpid2).with pid do
106
- allow(status).to receive(:success?) { true }
107
- [pid, status]
80
+ it 'exits with a 1 status if job is performed and it fails' do
81
+ expect(strategy).to receive(:exit!).with(1)
82
+ strategy.execute job, error_handler
108
83
  end
109
- child_process.success?
110
- end
111
84
 
112
- it 'asks status about its success' do
113
- allow(Process).to receive(:waitpid2).with pid do
114
- expect(status).to receive(:success?) { true }
115
- [pid, status]
85
+ it 'calls error_handler if execution is unsuccessful' do
86
+ allow(strategy).to receive(:exit!)
87
+ strategy.execute job, ->(name, raised_error) {
88
+ expect(name).to eq(process_name)
89
+ expect(raised_error).to eq(exception)
90
+ }
116
91
  end
117
- child_process.success?
92
+
118
93
  end
94
+
119
95
  end
120
96
 
121
97
  end
@@ -1,8 +1,11 @@
1
+ require 'spec_helper'
1
2
  require_relative '../../lib/sisyphus/master'
2
3
 
3
4
  module Sisyphus
4
5
  describe Master do
5
- subject(:master) { Master.new job }
6
+ subject(:master) { Master.new job, worker_pool: worker_pool }
7
+
8
+ let(:worker_pool) { double :worker_pool }
6
9
 
7
10
  before(:each) {
8
11
  allow(master).to receive(:puts)
@@ -12,186 +15,98 @@ module Sisyphus
12
15
  let(:job) { double(:job) }
13
16
  let(:pipes) { [double(:reader_pipe), double(:writer_pipe)] }
14
17
 
15
- describe 'when receiving the spawn_worker message' do
16
- it 'forks' do
17
- expect(master).to receive(:fork) { 666 }
18
- master.spawn_worker
19
- end
20
-
21
- describe 'in the worker process' do
22
- let(:worker) { double :worker }
23
-
24
- before :each do
25
- allow(master).to receive(:fork) { nil }
26
- allow(IO).to receive(:pipe) { pipes }
27
- allow(pipes.first).to receive(:close)
28
- allow(Process).to receive(:pid) { 666 }
29
- allow(master).to receive(:exit!)
30
- allow(Worker).to receive(:new) { worker }
31
- allow(worker).to receive(:setup)
32
- allow(worker).to receive(:start)
33
- end
34
-
35
- it 'should setup the worker' do
36
- expect(worker).to receive(:setup)
37
- master.spawn_worker
38
- end
18
+ it 'creates a worker' do
19
+ execution_strategy = double :execution_strategy
20
+ master = Master.new job
21
+ allow(master).to receive(:execution_strategy) { execution_strategy }
22
+ worker = master.create_worker
23
+ expect(worker.job).to eq(job)
24
+ expect(worker.execution_strategy).to eq(execution_strategy)
25
+ end
39
26
 
40
- it 'should rename the process' do
41
- expect(master).to receive(:process_name=).with("Worker #{666}")
42
- master.spawn_worker
43
- end
27
+ describe 'when it has running workers' do
44
28
 
45
- it 'starts a worker after forking' do
46
- expect(worker).to receive(:start)
47
- master.spawn_worker
48
- end
29
+ let(:workers) { double :workers }
49
30
 
50
- it 'gives the writer pipe to the worker' do
51
- execution_strategy = double :execution_strategy
52
- allow(master).to receive(:execution_strategy) { execution_strategy }
53
- expect(Worker).to receive(:new).with(job, pipes.last, execution_strategy) { worker }
54
- master.spawn_worker
55
- end
31
+ it 'stops all workers when receiving stop_all' do
32
+ allow(worker_pool).to receive(:workers) { workers }
33
+ allow(workers).to receive(:each).and_yield({ pid: 666 }).and_yield({ pid: 667 })
34
+ allow(workers).to receive(:length).and_return(2, 1, 0)
56
35
 
57
- it 'closes the reader pipe' do
58
- expect(pipes.first).to receive(:close)
59
- master.spawn_worker
60
- end
36
+ expect(master).to receive(:stop_worker).with(666).ordered
37
+ expect(master).to receive(:stop_worker).with(667).ordered
61
38
 
62
- describe 'when an exception is raised' do
63
- let(:logger) { double(:logger) }
64
-
65
- it 'should log the exception' do
66
- allow(worker).to receive(:error_handler) { ->{} }
67
- allow(master).to receive(:logger) { logger }
68
- allow(worker).to receive(:setup) { raise :raised_by_spec }
69
- expect(logger).to receive(:warn)
70
- master.spawn_worker
71
- end
72
-
73
- it 'should write to the writer pipe' do
74
- allow(master).to receive(:logger) { logger }
75
- allow(worker).to receive(:setup) { raise :raised_by_spec }
76
- allow(logger).to receive(:warn)
77
- expect(worker).to receive(:error_handler) { ->{} }
78
- master.spawn_worker
79
- end
80
- end
39
+ master.stop_all
81
40
  end
82
41
 
83
- describe 'in the master process' do
42
+ describe 'and it receives stop_worker message' do
84
43
  before :each do
85
- allow(master).to receive(:fork) { 666 }
86
- allow(IO).to receive(:pipe) { pipes }
87
- allow(pipes.last).to receive(:close)
88
- end
89
-
90
- it 'increases worker_count' do
91
- master.spawn_worker
92
- expect(master.worker_count).to eq(1)
93
- end
94
-
95
- it 'should open a pipe' do
96
- expect(IO).to receive(:pipe) { pipes }
97
- master.spawn_worker
98
- end
99
-
100
- it 'should close the writer pipe' do
101
- expect(pipes.last).to receive(:close)
102
- master.spawn_worker
44
+ allow(worker_pool).to receive(:workers) { workers }
45
+ allow(workers).to receive(:find) { |&block| block.call({ pid: 666 }) }
103
46
  end
104
- end
105
- end
106
-
107
- describe 'when it has running workers' do
108
- before :each do
109
- pipes.each { |p| allow(p).to receive(:close) }
110
- allow(IO).to receive(:pipe) { pipes }
111
- allow(master).to receive(:fork) { 666 }
112
- master.spawn_worker
113
- allow(Process).to receive(:kill).with('INT', 666)
114
- allow(Process).to receive(:waitpid2).with(666)
115
- end
116
47
 
117
- describe 'and it receives stop_worker message' do
118
48
  it 'kills a child with the INT signal' do
119
49
  expect(Process).to receive(:kill).with('INT', 666)
120
50
  master.stop_worker(666)
121
51
  end
122
- end
123
-
124
- it 'stops all workers when receiving stop_all' do
125
- allow(Process).to receive(:kill).with('INT', 666)
126
- allow(Process).to receive(:wait2) { 666 }
127
-
128
- expect(master).to receive(:stop_worker).with(666).exactly(master.worker_count).times.and_call_original
129
52
 
130
- master.stop_all
131
- end
132
- end
133
-
134
- describe 'when there are no running workers' do
135
- describe 'and it receives stop_worker' do
136
- it 'raises an error' do
137
- expect { master.stop_worker(666) }.not_to raise_error
53
+ it 'kills nothing if no worker corresponds to the pid' do
54
+ expect(Process).not_to receive(:kill).with('INT', 667)
55
+ master.stop_worker(667)
138
56
  end
139
57
  end
140
58
 
141
- describe 'and it receives stop_all' do
142
- it 'does nothing' do
143
- expect(master).not_to receive(:stop_worker)
144
- master.stop_all
145
- end
146
- end
147
59
  end
148
60
 
149
61
  it 'starts the specified number of workers when started' do
150
- master = Master.new nil, workers: 3
62
+ master = Master.new nil, workers: 3, worker_pool: worker_pool
151
63
  allow(master).to receive(:puts)
152
64
  allow(master).to receive(:watch_for_output)
153
- expect(master).to receive(:spawn_worker).exactly(3).times
65
+ allow(master).to receive(:sleep)
66
+ expect(worker_pool).to receive(:spawn_worker).exactly(3).times
154
67
  master.start
155
68
  end
156
69
 
157
70
  describe 'when number of workers is zero' do
158
- let(:master) { Master.new nil, workers: 0 }
71
+ let(:master) { Master.new nil, workers: 0, worker_pool: worker_pool }
159
72
 
160
73
  before(:each) { allow(master).to receive(:puts) }
161
74
 
162
75
  it 'should not start workers' do
76
+ allow(master).to receive(:trap_signals)
163
77
  allow(master).to receive(:watch_for_output)
164
- expect(master).not_to receive(:spawn_worker)
78
+ expect(worker_pool).not_to receive(:spawn_worker)
165
79
  master.start
166
80
  end
167
81
  end
168
82
 
169
83
  it 'attaches a signal handler when started' do
84
+ allow(worker_pool).to receive(:spawn_worker)
85
+ allow(master).to receive(:watch_for_output)
86
+
170
87
  expect(Signal).to receive(:trap).with(:TTIN)
171
88
  expect(Signal).to receive(:trap).with(:INT)
172
89
  expect(Signal).to receive(:trap).with(:TTOU)
173
- allow(master).to receive(:spawn_worker)
174
- allow(master).to receive(:watch_for_output)
90
+
175
91
  master.start
176
92
  end
177
93
 
178
94
  it 'should watch for output' do
179
- allow(master).to receive(:spawn_worker)
95
+ allow(worker_pool).to receive(:spawn_worker)
96
+ allow(master).to receive(:trap_signals)
180
97
  expect(master).to receive(:watch_for_output)
181
98
  master.start
182
99
  end
183
100
 
184
101
  it 'can resolve a wpid from a reader pipe' do
185
- allow(IO).to receive(:pipe) { pipes }
186
- pipes.each { |p| allow(p).to receive(:close) }
187
- allow(pipes.first).to receive(:fileno) { 213 }
188
- allow(master).to receive(:fork) { 666 }
189
- master.spawn_worker
190
-
191
- expect(master.send(:worker_pid, pipes.first)).to eq(666)
102
+ pipe = double :pipe
103
+ allow(pipe).to receive(:fileno) { 123 }
104
+ allow(worker_pool).to receive(:workers) { [{ pid: 666, reader: pipe }] }
105
+ expect(master.send(:worker_pid, pipe)).to eq(666)
192
106
  end
193
107
 
194
108
  it 'raises if it can\'t resolve a wpid from a reader pipe' do
109
+ allow(worker_pool).to receive(:workers) { [] }
195
110
  expect { master.send(:worker_pid, pipes.first) }.to raise_error("Unknown worker pipe")
196
111
  end
197
112
  end
@@ -1,32 +1,28 @@
1
+ require 'spec_helper'
1
2
  require_relative '../../lib/sisyphus/simple_execution_strategy'
2
3
 
3
4
  module Sisyphus
4
5
  describe SimpleExecutionStrategy do
5
6
 
6
- let(:logger) { double :logger }
7
7
  let(:job) { double :job }
8
- let(:strategy) { SimpleExecutionStrategy.new(logger) }
8
+ let(:strategy) { SimpleExecutionStrategy.new }
9
9
 
10
10
  it 'should perform the job when executed' do
11
11
  expect(job).to receive(:perform)
12
12
  strategy.execute job
13
13
  end
14
14
 
15
- it 'should log exceptions if the job fails' do
15
+ it 'should call the error_handler if the job fails' do
16
16
  error_message = "This is a horrible failure.. The Universe is probably ending!"
17
+ error = Exception.new(error_message)
17
18
  process_name = "uber awesome process name"
18
- allow(job).to receive(:perform) { fail Exception, error_message }
19
+ allow(job).to receive(:perform) { fail error }
19
20
  allow(strategy).to receive(:process_name) { process_name }
20
- expect(logger).to receive(:warn).with(process_name)
21
- strategy.execute job
21
+ strategy.execute job, ->(name, raised_error) {
22
+ expect(name).to eq(process_name)
23
+ expect(raised_error).to eq(error)
24
+ }
22
25
  end
23
26
 
24
- it 'should call the error_handler if the job fails' do
25
- allow(job).to receive(:perform) { fail "foo" }
26
- allow(logger).to receive(:warn)
27
- error_handler = double :error_handler
28
- expect(error_handler).to receive(:call)
29
- strategy.execute job, error_handler
30
- end
31
27
  end
32
28
  end
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+ require_relative '../../lib/sisyphus/master'
3
+ require_relative '../../lib/sisyphus/worker_pool'
4
+
5
+ module Sisyphus
6
+ describe WorkerPool do
7
+
8
+ subject(:worker_pool) { WorkerPool.new worker_factory }
9
+
10
+ describe 'when receiving the spawn_worker message' do
11
+
12
+ let(:job) { double :job }
13
+ let(:worker_factory) { Master.new(job) }
14
+ let(:pipes) { [double(:output), double(:input)] }
15
+
16
+ it 'retrieves a worker from the worker_factory' do
17
+ allow(worker_pool).to receive(:fork) { 3267 }
18
+ expect(worker_factory).to receive(:create_worker).and_call_original
19
+ worker_pool.spawn_worker
20
+ end
21
+
22
+ it 'forks' do
23
+ expect(worker_pool).to receive(:fork) { 666 }
24
+ worker_pool.spawn_worker
25
+ end
26
+
27
+ describe 'in the worker process' do
28
+
29
+ let(:worker) { double :worker }
30
+
31
+ before :each do
32
+ allow(worker_pool).to receive(:fork) { nil }
33
+ allow(worker_pool).to receive(:process_name=)
34
+ allow(worker).to receive(:start)
35
+ allow(worker).to receive(:atfork_child)
36
+ end
37
+
38
+ it 'runs Worker#atfork_child' do
39
+ allow(worker_factory).to receive(:create_worker) { worker }
40
+ expect(worker).to receive(:atfork_child).with(no_args)
41
+ worker_pool.spawn_worker
42
+ end
43
+
44
+ it 'renames the process' do
45
+ allow(worker_factory).to receive(:create_worker) { worker }
46
+ expect(worker_pool).to receive(:process_name=).with("Worker #{Process.pid}")
47
+ worker_pool.spawn_worker
48
+ end
49
+
50
+ it 'starts the worker' do
51
+ allow(worker_factory).to receive(:create_worker) { worker }
52
+ allow(worker).to receive(:atfork_child)
53
+ expect(worker).to receive(:start)
54
+ worker_pool.spawn_worker
55
+ end
56
+
57
+ end
58
+
59
+ describe 'in the master process' do
60
+
61
+ before :each do
62
+ allow(worker_pool).to receive(:fork) { Process.pid }
63
+ allow(pipes.last).to receive(:close)
64
+ allow(IO).to receive(:pipe) { pipes }
65
+ end
66
+
67
+ it 'closes the input pipe' do
68
+ worker = double :worker
69
+ allow(worker).to receive(:to_master)
70
+ allow(worker_factory).to receive(:create_worker) { worker }
71
+ expect(worker).to receive(:atfork_parent).with(no_args)
72
+ worker_pool.spawn_worker
73
+ end
74
+
75
+ it 'adds the worker pid and output pipe to the list of workers' do
76
+ expect(worker_pool.workers).to receive(:<<).with({ pid: Process.pid, reader: pipes.first })
77
+ worker_pool.spawn_worker
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -1,23 +1,38 @@
1
+ require 'spec_helper'
1
2
  require_relative '../../lib/sisyphus/worker'
2
3
 
3
4
  module Sisyphus
4
5
  describe Worker do
5
6
  let(:job) { double :job }
6
- let(:output) { double :pipe }
7
7
  let(:execution_strategy) { double :execution_strategy }
8
- let(:worker) { Worker.new job, output, execution_strategy }
8
+ let(:logger) { double :logger }
9
9
 
10
- it 'traps signals when started' do
11
- worker.stub :exit!
12
- worker.instance_variable_set(:@stopped, true)
13
- worker.should_receive :trap_signals
10
+ subject(:worker) { Worker.new job, execution_strategy, logger }
11
+
12
+ it 'sets up itself and executes run when started' do
13
+ expect(worker).to receive(:setup).ordered
14
+ expect(worker).to receive(:run).ordered
14
15
  worker.start
15
16
  end
16
17
 
17
- it 'exits when it has been stopped' do
18
- worker.instance_variable_set(:@stopped, true)
19
- worker.should_receive :exit!
20
- worker.start
18
+ it 'traps INT signals before setting up the job' do
19
+ allow(job).to receive(:respond_to?).with(:setup) { true }
20
+ expect(Signal).to receive(:trap).with('INT').ordered
21
+ expect(job).to receive(:setup).ordered
22
+ worker.setup
23
+ end
24
+
25
+ it 'does not perform the job when it has been stopped' do
26
+ worker.stop
27
+ allow(worker).to receive(:exit!)
28
+ expect(worker).not_to receive(:perform_job)
29
+ worker.run
30
+ end
31
+
32
+ it 'will not run if it has not been set up' do
33
+ expect(worker).not_to receive(:perform_job)
34
+ allow(worker).to receive(:exit!)
35
+ worker.run
21
36
  end
22
37
 
23
38
  it 'uses execution_strategy to perform the job' do
@@ -31,37 +46,82 @@ module Sisyphus
31
46
  worker.perform_job
32
47
  end
33
48
 
49
+ it 'only closes the output in atfork_parent' do
50
+ expect(worker.output).to receive(:close)
51
+ expect(worker.to_master).not_to receive(:close)
52
+ worker.atfork_parent
53
+ end
54
+
55
+ it 'only closes the to_master in atfork_child' do
56
+ expect(worker.to_master).to receive(:close)
57
+ expect(worker.output).not_to receive(:close)
58
+ worker.atfork_child
59
+ end
60
+
34
61
  context 'the error_handler' do
35
62
 
36
63
  it 'writes the UNCAUGHT_ERROR to output' do
37
- expect(output).to receive(:write).with Worker::UNCAUGHT_ERROR
38
- worker.error_handler.call
64
+ allow(logger).to receive(:warn)
65
+ expect(worker.output).to receive(:write).with Worker::UNCAUGHT_ERROR
66
+ worker.error_handler.call(:name, :error)
67
+ end
68
+
69
+ it 'logs the error being thrown' do
70
+ the_name = :the_name
71
+ the_exception = :the_exception
72
+ expect(worker.logger).to receive(:warn) do |name, &block|
73
+ expect(name).to eq(the_name)
74
+ expect(block.call).to eq(the_exception)
75
+ end
76
+ worker.error_handler.call(the_name, the_exception)
39
77
  end
40
78
 
41
79
  it 'does not write UNCAUGHT_ERROR to output if the worker is stopped' do
42
80
  allow(worker).to receive(:stopped?) { true }
43
- expect(output).to_not receive(:write)
44
- worker.error_handler.call
45
- expect(output).to_not receive(:write)
46
- worker.error_handler.call
81
+ expect(worker.output).to_not receive(:write)
82
+ worker.error_handler.call(:name, :error)
83
+ expect(worker.output).to_not receive(:write)
84
+ worker.error_handler.call(:name, :error)
47
85
  end
48
86
 
49
87
  end
50
88
 
51
89
  context 'when job does not respond to :setup' do
52
90
  it 'does not call job.setup' do
53
- job.stub(:respond_to?).with(:setup) { false }
54
- job.should_not_receive :setup
91
+ allow(job).to receive(:respond_to?).with(:setup) { false }
92
+ expect(job).not_to receive(:setup)
55
93
  worker.setup
56
94
  end
57
95
  end
58
96
 
59
97
  context 'when job responds to :setup' do
98
+
99
+ before :each do
100
+ allow(job).to receive(:respond_to?).with(:setup) { true }
101
+ end
102
+
60
103
  it 'sets up the job' do
61
- job.stub(:respond_to?).with(:setup) { true }
62
- job.should_receive :setup
104
+ expect(job).to receive(:setup)
63
105
  worker.setup
64
106
  end
107
+
108
+ context 'and job#setup raises an exception' do
109
+
110
+ before :each do
111
+ allow(job).to receive(:setup).and_raise(Exception)
112
+ allow(logger).to receive(:warn)
113
+ end
114
+
115
+ it 'handles the exception' do
116
+ expect { worker.setup }.not_to raise_error(Exception)
117
+ end
118
+
119
+ it 'calls the error_handler in the rescue block' do
120
+ expect(worker.output).to receive(:write).with(Worker::UNCAUGHT_ERROR)
121
+ worker.setup
122
+ end
123
+
124
+ end
65
125
  end
66
126
 
67
127
  end
@@ -0,0 +1,17 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+ config.filter_run :focus
11
+
12
+ # Run specs in random order to surface order dependencies. If you find an
13
+ # order dependency and want to debug it, you can fix the order by providing
14
+ # the seed, which is printed after each run.
15
+ # --seed 1234
16
+ config.order = 'random'
17
+ end
metadata CHANGED
@@ -1,55 +1,54 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sisyphus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rasmus Bang Grouleff
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2014-03-31 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bundler
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - ~>
16
+ - - "~>"
18
17
  - !ruby/object:Gem::Version
19
18
  version: '1.3'
20
19
  type: :development
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
- - - ~>
23
+ - - "~>"
25
24
  - !ruby/object:Gem::Version
26
25
  version: '1.3'
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: rake
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
- - - ~>
30
+ - - "~>"
32
31
  - !ruby/object:Gem::Version
33
32
  version: '10.0'
34
33
  type: :development
35
34
  prerelease: false
36
35
  version_requirements: !ruby/object:Gem::Requirement
37
36
  requirements:
38
- - - ~>
37
+ - - "~>"
39
38
  - !ruby/object:Gem::Version
40
39
  version: '10.0'
41
40
  - !ruby/object:Gem::Dependency
42
41
  name: rspec
43
42
  requirement: !ruby/object:Gem::Requirement
44
43
  requirements:
45
- - - ~>
44
+ - - "~>"
46
45
  - !ruby/object:Gem::Version
47
46
  version: '2.14'
48
47
  type: :development
49
48
  prerelease: false
50
49
  version_requirements: !ruby/object:Gem::Requirement
51
50
  requirements:
52
- - - ~>
51
+ - - "~>"
53
52
  - !ruby/object:Gem::Version
54
53
  version: '2.14'
55
54
  description: A tiny library for spawning worker processes
@@ -59,7 +58,8 @@ executables: []
59
58
  extensions: []
60
59
  extra_rdoc_files: []
61
60
  files:
62
- - .gitignore
61
+ - ".gitignore"
62
+ - ".rspec"
63
63
  - Gemfile
64
64
  - Gemfile.lock
65
65
  - LICENSE.txt
@@ -73,38 +73,40 @@ files:
73
73
  - lib/sisyphus/simple_execution_strategy.rb
74
74
  - lib/sisyphus/sleep.rb
75
75
  - lib/sisyphus/worker.rb
76
+ - lib/sisyphus/worker_pool.rb
76
77
  - lib/version.rb
77
78
  - sisyphus.gemspec
78
79
  - spec/sisyphus/forking_execution_strategy_spec.rb
79
80
  - spec/sisyphus/master_spec.rb
80
81
  - spec/sisyphus/simple_execution_strategy_spec.rb
82
+ - spec/sisyphus/worker_pool_spec.rb
81
83
  - spec/sisyphus/worker_spec.rb
84
+ - spec/spec_helper.rb
82
85
  homepage: https://github.com/rbgrouleff/sisyphus
83
86
  licenses:
84
87
  - Apache License 2.0
85
88
  metadata: {}
86
- post_install_message:
87
89
  rdoc_options: []
88
90
  require_paths:
89
91
  - lib
90
92
  required_ruby_version: !ruby/object:Gem::Requirement
91
93
  requirements:
92
- - - '>='
94
+ - - ">="
93
95
  - !ruby/object:Gem::Version
94
- version: '0'
96
+ version: 1.9.3
95
97
  required_rubygems_version: !ruby/object:Gem::Requirement
96
98
  requirements:
97
- - - '>='
99
+ - - ">="
98
100
  - !ruby/object:Gem::Version
99
101
  version: '0'
100
102
  requirements: []
101
- rubyforge_project:
102
- rubygems_version: 2.1.5
103
- signing_key:
103
+ rubygems_version: 3.7.2
104
104
  specification_version: 4
105
105
  summary: A tiny library for spawning worker processes
106
106
  test_files:
107
107
  - spec/sisyphus/forking_execution_strategy_spec.rb
108
108
  - spec/sisyphus/master_spec.rb
109
109
  - spec/sisyphus/simple_execution_strategy_spec.rb
110
+ - spec/sisyphus/worker_pool_spec.rb
110
111
  - spec/sisyphus/worker_spec.rb
112
+ - spec/spec_helper.rb