Skip to content

Commit 5be1ef0

Browse files
Fix child lifecycle timeout handling
Assisted-By: devx/a01026a8-dd63-4b91-bae4-aec7ee8f7fa3
1 parent e557e7c commit 5be1ef0

4 files changed

Lines changed: 48 additions & 17 deletions

File tree

lib/async/container/channel.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ module Container
1010
# Provides a basic multi-thread/multi-process uni-directional communication channel.
1111
class Channel
1212
# Initialize the channel using a pipe.
13-
def initialize(timeout: 1.0)
13+
def initialize(timeout: nil)
1414
@in, @out = ::IO.pipe
15-
@in.timeout = timeout
15+
@in.timeout = timeout if timeout
1616
end
1717

1818
# The input end of the pipe.
@@ -46,8 +46,7 @@ def receive
4646
if data = @in.gets
4747
return JSON.parse(data, symbolize_names: true)
4848
end
49-
rescue => error
50-
Console.error(self, "Error during channel receive!", error)
49+
rescue JSON::ParserError
5150
return nil
5251
end
5352
end

lib/async/container/generic.rb

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def running?
9898

9999
# Whether the container is stopping.
100100
def stopping?
101-
@stopping
101+
@mutex.synchronize{@stopping}
102102
end
103103

104104
# Sleep until a group event occurs or the specified duration elapses.
@@ -139,7 +139,7 @@ def wait
139139

140140
# Interrupt all children and enter the stopping state.
141141
def interrupt
142-
@stopping = true
142+
stopping!
143143
@group.interrupt!
144144
end
145145

@@ -155,15 +155,15 @@ def wait_until_ready
155155
return false if failed? && !lifecycle_running?
156156
return true if !running? && !lifecycle_running?
157157

158-
@group.wait
158+
@group.wait(0.1)
159159
end
160160
end
161161

162162
# Stop all children.
163163
def stop(timeout = true)
164-
return if @stopping && !running?
164+
return if stopping? && !running?
165165

166-
@stopping = true
166+
stopping!
167167
@group.stop(timeout)
168168
self.wait
169169
end
@@ -229,6 +229,10 @@ def lifecycle_running?
229229
@mutex.synchronize{@threads.any?(&:alive?)}
230230
end
231231

232+
def stopping!
233+
@mutex.synchronize{@stopping = true}
234+
end
235+
232236
def start_child(name:, **options, &block)
233237
@child_type.call(name: name, **options, &block)
234238
end
@@ -265,7 +269,7 @@ def manage_child(child, name:, key:, restart:, health_check_timeout:, startup_ti
265269
unregister_child(child, status, key: key)
266270
notify_child_exit(child, status, name: name, key: key)
267271

268-
if restart && !@stopping
272+
if restart && !stopping?
269273
@statistics.restart!
270274

271275
child = start_child(name: name, **options, &block)
@@ -275,7 +279,7 @@ def manage_child(child, name:, key:, restart:, health_check_timeout:, startup_ti
275279
end
276280
end
277281
rescue => error
278-
Console.error(self, "Error during child lifecycle management!", exception: error, stopping: @stopping) if defined?(Console)
282+
Console.error(self, "Error during child lifecycle management!", exception: error, stopping: stopping?) if defined?(Console)
279283
ensure
280284
if @group.children.include?(child)
281285
begin
@@ -322,13 +326,15 @@ def monitor_child_with_timeouts(child, health_check_timeout:, startup_timeout:)
322326
end
323327

324328
def record_exit(child, status, name:, key:)
329+
stopping = stopping?
330+
325331
if status&.success?
326-
Console.debug(self, "Child exited successfully.", status: status, stopping: @stopping) if defined?(Console)
327-
elsif @stopping
328-
Console.debug(self, "Child exited while stopping.", status: status, stopping: @stopping) if defined?(Console)
332+
Console.debug(self, "Child exited successfully.", status: status, stopping: stopping) if defined?(Console)
333+
elsif stopping
334+
Console.debug(self, "Child exited while stopping.", status: status, stopping: stopping) if defined?(Console)
329335
else
330336
@statistics.failure!
331-
Console.error(self, "Child exited with error!", status: status, stopping: @stopping) if defined?(Console)
337+
Console.error(self, "Child exited with error!", status: status, stopping: stopping) if defined?(Console)
332338
end
333339
end
334340

test/async/container/channel.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@
5959
with "timeout" do
6060
let(:channel) {subject.new(timeout: 0.001)}
6161

62-
it "fails gracefully on timeout" do
63-
expect(channel.receive).to be_nil
62+
it "does not treat timeouts as channel closure" do
63+
expect do
64+
channel.receive
65+
end.to raise_exception(IO::TimeoutError)
6466
end
6567
end
6668
end

test/async/container/generic.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@
3737
expect(container).to be(:failed?)
3838
end
3939

40+
it "does not block waiting for readiness after a child fails to start" do
41+
container.spawn do
42+
raise "boom"
43+
end
44+
45+
expect(container.wait_until_ready).to be == false
46+
expect(container).not.to be(:running?)
47+
end
48+
4049
it "restarts children when requested" do
4150
count = 0
4251

@@ -54,6 +63,21 @@
5463
expect(container.statistics).to have_attributes(spawns: be == 2, restarts: be == 1, failures: be == 1)
5564
end
5665

66+
it "does not restart children while stopping" do
67+
container.spawn(restart: true) do |instance|
68+
instance.ready!
69+
sleep
70+
rescue Interrupt
71+
# Graceful shutdown.
72+
end
73+
74+
expect(container.wait_until_ready).to be == true
75+
76+
container.stop(true)
77+
78+
expect(container.statistics).to have_attributes(spawns: be == 1, restarts: be == 0)
79+
end
80+
5781
it "supports keyed children" do
5882
expect(container.spawn(key: :worker) do |instance|
5983
instance.ready!

0 commit comments

Comments
 (0)