Skip to content

Commit 8ec289d

Browse files
committed
Improved logging when child process fails.
1 parent 9999fe1 commit 8ec289d

File tree

10 files changed

+202
-23
lines changed

10 files changed

+202
-23
lines changed

examples/exec-child/jobs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "console"
5+
require "async/container/notify"
6+
7+
# Console.logger.debug!
8+
9+
class Jobs
10+
def self.start = self.new.start
11+
12+
def start
13+
Console.info(self, "Starting jobs...")
14+
15+
if notify = Async::Container::Notify.open!
16+
Console.info(self, "Notifying container ready...")
17+
notify.ready!
18+
end
19+
20+
loop do
21+
Console.info(self, "Jobs running...")
22+
23+
sleep 10
24+
end
25+
rescue Interrupt
26+
Console.info(self, "Exiting jobs...")
27+
end
28+
end
29+
30+
Jobs.start

examples/exec-child/readme.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Exec Child Example
2+
3+
This example demonstrates how to execute a child process using the `exec` function in a container.
4+
5+
## Usage
6+
7+
Start the main controller:
8+
9+
```
10+
> bundle exec ./start
11+
0.0s info: AppController [oid=0x938] [ec=0x94c] [pid=96758] [2024-12-12 14:33:45 +1300]
12+
| Controller starting...
13+
0.65s info: Jobs [oid=0x8e8] [ec=0x8fc] [pid=96763] [2024-12-12 14:33:45 +1300]
14+
| Starting jobs...
15+
0.65s info: Jobs [oid=0x8e8] [ec=0x8fc] [pid=96763] [2024-12-12 14:33:45 +1300]
16+
| Notifying container ready...
17+
0.65s info: Jobs [oid=0x8e8] [ec=0x8fc] [pid=96763] [2024-12-12 14:33:45 +1300]
18+
| Jobs running...
19+
0.65s info: Web [oid=0x8e8] [ec=0x8fc] [pid=96760] [2024-12-12 14:33:45 +1300]
20+
| Starting web...
21+
0.65s info: Web [oid=0x8e8] [ec=0x8fc] [pid=96760] [2024-12-12 14:33:45 +1300]
22+
| Notifying container ready...
23+
0.65s info: Web [oid=0x8e8] [ec=0x8fc] [pid=96760] [2024-12-12 14:33:45 +1300]
24+
| Web running...
25+
0.09s info: AppController [oid=0x938] [ec=0x94c] [pid=96758] [2024-12-12 14:33:45 +1300]
26+
| Controller started...
27+
```
28+
29+
In another terminal: `kill -HUP 96758` to cause a blue-green restart, which causes a new container to be started with new jobs and web processes:
30+
31+
```
32+
9.57s info: Jobs [oid=0x8e8] [ec=0x8fc] [pid=96836] [2024-12-12 14:33:54 +1300]
33+
| Starting jobs...
34+
9.57s info: Web [oid=0x8e8] [ec=0x8fc] [pid=96833] [2024-12-12 14:33:54 +1300]
35+
| Starting web...
36+
9.57s info: Jobs [oid=0x8e8] [ec=0x8fc] [pid=96836] [2024-12-12 14:33:54 +1300]
37+
| Notifying container ready...
38+
9.57s info: Web [oid=0x8e8] [ec=0x8fc] [pid=96833] [2024-12-12 14:33:54 +1300]
39+
| Notifying container ready...
40+
9.57s info: Jobs [oid=0x8e8] [ec=0x8fc] [pid=96836] [2024-12-12 14:33:54 +1300]
41+
| Jobs running...
42+
9.57s info: Web [oid=0x8e8] [ec=0x8fc] [pid=96833] [2024-12-12 14:33:54 +1300]
43+
| Web running...
44+
```
45+
46+
Once the new container is running and the child processes have notified they are ready, the controller will stop the old container:
47+
48+
```
49+
9.01s info: Async::Container::Group [oid=0xa00] [ec=0x94c] [pid=96758] [2024-12-12 14:33:54 +1300]
50+
| Stopping all processes...
51+
| {
52+
| "timeout": true
53+
| }
54+
9.01s info: Async::Container::Group [oid=0xa00] [ec=0x94c] [pid=96758] [2024-12-12 14:33:54 +1300]
55+
| Sending interrupt to 2 running processes...
56+
9.57s info: Web [oid=0x8e8] [ec=0x8fc] [pid=96760] [2024-12-12 14:33:54 +1300]
57+
| Exiting web...
58+
9.57s info: Jobs [oid=0x8e8] [ec=0x8fc] [pid=96763] [2024-12-12 14:33:54 +1300]
59+
| Exiting jobs...
60+
```
61+
62+
The new container continues to run as expected:
63+
64+
```
65+
19.57s info: Web [oid=0x8e8] [ec=0x8fc] [pid=96833] [2024-12-12 14:34:04 +1300]
66+
| Web running...
67+
19.57s info: Jobs [oid=0x8e8] [ec=0x8fc] [pid=96836] [2024-12-12 14:34:04 +1300]
68+
| Jobs running...
69+
```

examples/exec-child/start

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "async/container"
5+
require "console"
6+
7+
# Console.logger.debug!
8+
9+
class AppController < Async::Container::Controller
10+
def setup(container)
11+
container.spawn(name: "Web") do |instance|
12+
# Specify ready: false here as the child process is expected to take care of the readiness notification:
13+
instance.exec("bundle", "exec", "web", ready: false)
14+
end
15+
16+
container.spawn(name: "Jobs") do |instance|
17+
instance.exec("bundle", "exec", "jobs", ready: false)
18+
end
19+
end
20+
end
21+
22+
controller = AppController.new
23+
24+
controller.run

examples/exec-child/web

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "console"
5+
require "async/container/notify"
6+
7+
# Console.logger.debug!
8+
9+
class Web
10+
def self.start = self.new.start
11+
12+
def start
13+
Console.info(self, "Starting web...")
14+
15+
if notify = Async::Container::Notify.open!
16+
Console.info(self, "Notifying container ready...")
17+
notify.ready!
18+
end
19+
20+
loop do
21+
Console.info(self, "Web running...")
22+
23+
sleep 10
24+
end
25+
rescue Interrupt
26+
Console.info(self, "Exiting web...")
27+
end
28+
end
29+
30+
Web.start

lib/async/container/controller.rb

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,10 @@ def initialize(notify: Notify.open!, container_class: Container, graceful_stop:
2626
@container = nil
2727
@container_class = container_class
2828

29-
if @notify = notify
30-
@notify.status!("Initializing...")
31-
end
32-
29+
@notify = notify
3330
@signals = {}
3431

35-
trap(SIGHUP) do
32+
self.trap(SIGHUP) do
3633
self.restart
3734
end
3835

@@ -93,7 +90,12 @@ def setup(container)
9390

9491
# Start the container unless it's already running.
9592
def start
96-
self.restart unless @container
93+
unless @container
94+
Console.info(self) {"Controller starting..."}
95+
self.restart
96+
end
97+
98+
Console.info(self) {"Controller started..."}
9799
end
98100

99101
# Stop the container if it's running.
@@ -183,6 +185,8 @@ def reload
183185

184186
# Enter the controller run loop, trapping `SIGINT` and `SIGTERM`.
185187
def run
188+
@notify&.status!("Initializing...")
189+
186190
# I thought this was the default... but it doesn't always raise an exception unless you do this explicitly.
187191
# We use `Thread.current.raise(...)` so that exceptions are filtered through `Thread.handle_interrupt` correctly.
188192
interrupt_action = Signal.trap(:INT) do

lib/async/container/generic.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,20 @@ def wait_until_ready
107107
Console.debug(self) do |buffer|
108108
buffer.puts "Waiting for ready:"
109109
@state.each do |child, state|
110-
buffer.puts "\t#{child.class}: #{state.inspect}"
110+
buffer.puts "\t#{child.inspect}: #{state}"
111111
end
112112
end
113113

114114
self.sleep
115115

116116
if self.status?(:ready)
117+
Console.logger.debug(self) do |buffer|
118+
buffer.puts "All ready:"
119+
@state.each do |child, state|
120+
buffer.puts "\t#{child.inspect}: #{state}"
121+
end
122+
end
123+
117124
return true
118125
end
119126
end

lib/async/container/group.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def wait
6565
# Interrupt all running processes.
6666
# This resumes the controlling fiber with an instance of {Interrupt}.
6767
def interrupt
68-
Console.debug(self, "Sending interrupt to #{@running.size} running processes...")
68+
Console.info(self, "Sending interrupt to #{@running.size} running processes...")
6969
@running.each_value do |fiber|
7070
fiber.resume(Interrupt)
7171
end
@@ -74,7 +74,7 @@ def interrupt
7474
# Terminate all running processes.
7575
# This resumes the controlling fiber with an instance of {Terminate}.
7676
def terminate
77-
Console.debug(self, "Sending terminate to #{@running.size} running processes...")
77+
Console.info(self, "Sending terminate to #{@running.size} running processes...")
7878
@running.each_value do |fiber|
7979
fiber.resume(Terminate)
8080
end
@@ -83,6 +83,7 @@ def terminate
8383
# Stop all child processes using {#terminate}.
8484
# @parameter timeout [Boolean | Numeric | Nil] If specified, invoke a graceful shutdown using {#interrupt} first.
8585
def stop(timeout = 1)
86+
Console.info(self, "Stopping all processes...", timeout: timeout)
8687
# Use a default timeout if not specified:
8788
timeout = 1 if timeout == true
8889

@@ -105,7 +106,7 @@ def stop(timeout = 1)
105106
end
106107

107108
# Terminate all children:
108-
self.terminate
109+
self.terminate if any?
109110

110111
# Wait for all children to exit:
111112
self.wait
@@ -137,7 +138,8 @@ def wait_for(channel)
137138
protected
138139

139140
def wait_for_children(duration = nil)
140-
Console.debug(self, "Waiting for children...", duration: duration)
141+
Console.debug(self, "Waiting for children...", duration: duration, running: @running)
142+
141143
if !@running.empty?
142144
# Maybe consider using a proper event loop here:
143145
readable, _, _ = ::IO.select(@running.keys, nil, nil, duration)

lib/async/container/notify/console.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ def initialize(logger)
2424
end
2525

2626
# Send a message to the console.
27-
def send(level: :debug, **message)
28-
@logger.send(level, self) {message}
27+
def send(level: :info, **message)
28+
@logger.public_send(level, self) {message}
2929
end
3030

3131
# Send an error message to the console.

lib/async/container/process.rb

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,12 @@ def name= value
122122

123123
# A human readable representation of the process.
124124
# @returns [String]
125-
def to_s
126-
"\#<#{self.class} #{@name}>"
125+
def inspect
126+
"\#<#{self.class} name=#{@name.inspect} status=#{@status.inspect} pid=#{@pid.inspect}>"
127127
end
128128

129+
alias to_s inspect
130+
129131
# Invoke {#terminate!} and then {#wait} for the child process to exit.
130132
def close
131133
self.terminate!
@@ -149,22 +151,28 @@ def terminate!
149151
end
150152

151153
# Wait for the child process to exit.
154+
# @asynchronous This method may block.
155+
#
152156
# @returns [::Process::Status] The process exit status.
153157
def wait
154158
if @pid && @status.nil?
155-
_, @status = ::Process.wait2(@pid, ::Process::WNOHANG)
159+
Console.debug(self, "Waiting for process to exit...", pid: @pid)
156160

157-
if @status.nil?
158-
sleep(0.01)
161+
_, @status = ::Process.wait2(@pid, ::Process::WNOHANG)
162+
163+
while @status.nil?
164+
sleep(0.1)
165+
159166
_, @status = ::Process.wait2(@pid, ::Process::WNOHANG)
160-
end
161-
162-
if @status.nil?
163-
Console.warn(self) {"Process #{@pid} is blocking, has it exited?"}
164-
_, @status = ::Process.wait2(@pid)
167+
168+
if @status.nil?
169+
Console.warn(self) {"Process #{@pid} is blocking, has it exited?"}
170+
end
165171
end
166172
end
167173

174+
Console.debug(self, "Process exited.", pid: @pid, status: @status)
175+
168176
return @status
169177
end
170178
end

releases.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Releases
2+
3+
## Unreleased
4+
5+
- Improved logging when child process fails and container startup.

0 commit comments

Comments
 (0)