Skip to content

Commit 1e6dc28

Browse files
authored
Merge pull request #89 from briandunn/fix/cld-aint-all-bad
Fix: don't assume SIGCLD is bad. Check child statuses.
2 parents 689dea6 + f9823dd commit 1e6dc28

File tree

5 files changed

+160
-69
lines changed

5 files changed

+160
-69
lines changed

lib/flatware/rspec/formatters/console.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def summarize(checkpoints)
3434
end
3535

3636
def summarize_remaining(remaining)
37-
progress_formatter.output.puts(colorizer.wrap(<<~MESSAGE, :detail))
37+
out.puts(colorizer.wrap(<<~MESSAGE, :detail))
3838
3939
The following specs weren't run:
4040

lib/flatware/sink.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,19 @@ def initialize(jobs:, formatter:, sink:, worker_count: 0, **)
2525
@checkpoints = []
2626
@completed_jobs = []
2727
@formatter = formatter
28+
@interrupted = false
2829
@jobs = group_jobs(jobs, worker_count).freeze
2930
@queue = @jobs.dup
3031
@sink = sink
3132
@workers = Set.new(worker_count.times.to_a)
3233
end
3334

3435
def start
35-
@signal = Signal.listen(&method(:summarize_remaining))
36+
Signal.listen(formatter, &method(:on_interrupt))
3637
formatter.jobs jobs
3738
DRb.start_service(sink, self, verbose: Flatware.verbose?)
3839
DRb.thread.join
39-
!failures?
40+
!(failures? || interrupted?)
4041
end
4142

4243
def ready(worker)
@@ -73,8 +74,13 @@ def respond_to_missing?(name, include_all)
7374

7475
private
7576

77+
def on_interrupt
78+
@interrupted = true
79+
summarize_remaining
80+
end
81+
7682
def interrupted?
77-
@signal&.interrupted?
83+
@interrupted
7884
end
7985

8086
def check_finished!

lib/flatware/sink/signal.rb

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,77 @@
11
module Flatware
22
module Sink
33
class Signal
4-
def initialize(&on_interrupt)
4+
Message = Struct.new(:message)
5+
6+
attr_reader :formatter
7+
8+
def initialize(formatter, &on_interrupt)
9+
@formatter = formatter
510
Thread.main[:signals] = Queue.new
611

712
@on_interrupt = on_interrupt
813
end
914

10-
def interrupted?
11-
!signals.empty?
12-
end
13-
1415
def listen
1516
Thread.new(&method(:handle_signals))
1617

1718
::Signal.trap('INT') { signals << :int }
18-
::Signal.trap('CLD') { signals << :cld }
19+
::Signal.trap('CLD') do
20+
signals << :cld if child_failed?
21+
end
1922

2023
self
2124
end
2225

23-
def self.listen(&block)
24-
new(&block).listen
26+
def self.listen(formatter, &block)
27+
new(formatter, &block).listen
2528
end
2629

2730
private
2831

32+
def child_status
33+
_worker_pid, status = begin
34+
Process.wait2(-1, Process::WNOHANG)
35+
rescue Errno::ECHILD
36+
[]
37+
end
38+
status
39+
end
40+
41+
def child_statuses
42+
statuses = []
43+
loop do
44+
status = child_status
45+
return statuses unless status
46+
47+
statuses << status
48+
end
49+
end
50+
51+
def child_failed?
52+
child_statuses.any? { |status| !status.success? }
53+
end
54+
2955
def handle_signals
30-
puts signal_message(signals.pop)
31-
Process.waitall
32-
@on_interrupt.call
33-
puts 'done.'
56+
signal_message(signals.pop) do
57+
Process.waitall
58+
@on_interrupt.call
59+
end
60+
3461
abort
3562
end
3663

3764
def signal_message(signal)
38-
format(<<~MESSAGE, { cld: 'A worker died', int: 'Interrupted' }.fetch(signal))
65+
formatter.message(Message.new(format(<<~MESSAGE, { cld: 'A worker died', int: 'Interrupted' }.fetch(signal))))
3966
4067
%s!
4168
42-
Cleaning up. Please wait...
69+
Waiting for workers to finish their current jobs...
4370
MESSAGE
71+
72+
yield
73+
74+
formatter.message(Message.new('done.'))
4475
end
4576

4677
def signals

spec/flatware/sink/signal_spec.rb

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
require 'spec_helper'
2+
3+
describe Flatware::Sink::Signal do
4+
let(:formatter_queue) { Queue.new }
5+
6+
let(:formatter) do
7+
queue = formatter_queue
8+
9+
Class.new do
10+
define_method(:message, &queue.method(:push))
11+
end.new
12+
end
13+
14+
let(:signal_blocks) { {} }
15+
16+
let(:on_interrupt) do
17+
-> {}.tap do |block|
18+
allow(block).to receive(:call)
19+
end
20+
end
21+
22+
before do
23+
allow(Process).to receive(:waitall)
24+
25+
allow(Signal).to receive(:trap) do |signal, &block|
26+
signal_blocks[signal] = block
27+
end
28+
29+
@subject = described_class.listen(formatter, &on_interrupt).tap do |instance|
30+
allow(instance).to receive(:abort)
31+
end
32+
end
33+
34+
attr_reader :subject
35+
36+
def send_signal(signal)
37+
signal_blocks.fetch(signal).call
38+
end
39+
40+
shared_examples_for 'a signal initiated shutdown' do |expected_message|
41+
before do
42+
@messages = 2.times.map do
43+
Timeout.timeout(1, StandardError, 'formatter did not receive within 1 sec') do
44+
formatter_queue.pop.message
45+
end
46+
end
47+
end
48+
49+
attr_reader :messages
50+
51+
it 'aborts' do
52+
expect(subject).to have_received(:abort)
53+
end
54+
55+
it 'tells the formatter to emit the signal message' do
56+
expect(messages).to match([include(expected_message), 'done.'])
57+
end
58+
59+
it 'calls on_interrupt' do
60+
expect(on_interrupt).to have_received(:call)
61+
end
62+
63+
it 'waits for workers' do
64+
expect(Process).to have_received(:waitall)
65+
end
66+
end
67+
68+
describe 'on SIGINT' do
69+
before do
70+
send_signal('INT')
71+
end
72+
73+
it_should_behave_like 'a signal initiated shutdown', 'Interrupted'
74+
end
75+
76+
describe 'on SIGCLD' do
77+
context 'when a child failed' do
78+
before do
79+
allow(Process).to receive(:wait2).and_return(
80+
[nil, double(success?: true)],
81+
[nil, double(success?: false)],
82+
nil
83+
)
84+
85+
send_signal('CLD')
86+
end
87+
88+
it_should_behave_like 'a signal initiated shutdown', 'A worker died'
89+
end
90+
91+
context 'when a child has not failed' do
92+
before do
93+
allow(Process).to receive(:wait2).and_return nil
94+
95+
send_signal('CLD')
96+
end
97+
98+
it 'does nothing' do
99+
expect(on_interrupt).to_not have_received(:call)
100+
expect(subject).to_not have_received(:abort)
101+
expect(formatter_queue).to be_empty
102+
end
103+
end
104+
end
105+
end

spec/flatware/sink_spec.rb

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -31,57 +31,6 @@
3131
}
3232
end
3333

34-
def connect
35-
Timeout.timeout(2) do
36-
sleep 0.1
37-
DRbObject.new_with_uri(sink_endpoint)
38-
rescue DRb::DRbConnError
39-
retry
40-
end
41-
end
42-
43-
def fork_server(&block)
44-
IO.popen('-') do |f|
45-
if f
46-
connect.ready(1)
47-
block.call(f)
48-
Process.waitall
49-
else
50-
described_class.start_server(**defaults, jobs: [job])
51-
end
52-
end
53-
end
54-
55-
context 'when I have work to do' do
56-
let(:job) { Flatware::Job.new('int.feature') }
57-
58-
context 'but a worker dies' do
59-
it 'explains and exits non-zero' do
60-
fork_server do |server|
61-
Process.kill 'CLD', server.pid
62-
63-
expect(server.read).to match(/A worker died/).and(match(/int\.feature/))
64-
65-
Process.wait(server.pid)
66-
expect(Process.last_status).to_not be_success
67-
end
68-
end
69-
end
70-
71-
context 'but am interupted' do
72-
it 'explains and exits non-zero' do
73-
fork_server do |server|
74-
Process.kill 'INT', server.pid
75-
76-
expect(server.read).to match(/Interrupted/).and(match(/int\.feature/))
77-
78-
Process.wait(server.pid)
79-
expect(Process.last_status).to_not be_success
80-
end
81-
end
82-
end
83-
end
84-
8534
context 'there is no work' do
8635
it 'sumarizes' do
8736
server = described_class::Server.new jobs: [], **defaults

0 commit comments

Comments
 (0)