Skip to content

Commit 66fa57e

Browse files
authored
fix: enhance process management to prevent zombie processes and clarify instance state handling (#544)
1 parent 4f48fdb commit 66fa57e

File tree

1 file changed

+28
-2
lines changed

1 file changed

+28
-2
lines changed

internal/core/local_runtime/instance.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ type PluginInstance struct {
3030
l *sync.Mutex
3131
listener map[string]func([]byte)
3232

33-
started bool // mark the instance as started
34-
shutdown bool // mark the instance as shutdown
33+
started bool // mark the instance as started, it will be set to true when the first heartbeat is received
34+
shutdown bool // mark the instance as shutdown, it will be set to true when stdout reader is closed
3535

3636
// app config
3737
appConfig *app.Config
@@ -114,6 +114,17 @@ func (s *PluginInstance) Stop() {
114114

115115
// StartStdout starts to read the stdout of the plugin
116116
// and parse the stdout data to trigger corresponding listeners
117+
//
118+
// # Whenever the stdout reader is closed, subprocess will be killed and reaped
119+
//
120+
// At a high-level view of the architecture design
121+
// Stdout actually take responsibility of the existing of the plugin instance
122+
//
123+
// CLOSE STDOUT = KILL and REAP subprocess
124+
// CLOSE INSTANCE = CLOSE STDOUT
125+
//
126+
// In the above scope, the subprocess was killed by daemon,
127+
// Once the subprocess exists itself, STDOUT always close, which results in `CLOSE STDOUT`
117128
func (s *PluginInstance) StartStdout() {
118129
defer func() {
119130
// notify shutdown signal
@@ -163,10 +174,19 @@ func (s *PluginInstance) StartStdout() {
163174

164175
// once reader of stdout is closed, kill subprocess
165176
if err := s.cmd.Process.Kill(); err != nil {
177+
// no need to return here, just log the error, it's perhaps the process was exited already
178+
// and the kill command fails
166179
s.WalkNotifiers(func(notifier PluginInstanceNotifier) {
167180
notifier.OnInstanceErrorLog(s, fmt.Errorf("failed to kill subprocess: %s", err.Error()))
168181
})
169182
}
183+
184+
// collect subprocess, avoid zombie processes
185+
if _, err := s.cmd.Process.Wait(); err != nil {
186+
s.WalkNotifiers(func(notifier PluginInstanceNotifier) {
187+
notifier.OnInstanceErrorLog(s, fmt.Errorf("failed to reap subprocess: %s", err.Error()))
188+
})
189+
}
170190
}
171191

172192
// handles stdout data and notify corresponding listeners
@@ -258,6 +278,11 @@ func (s *PluginInstance) Monitor() error {
258278

259279
// check status of plugin every 5 seconds
260280
for range ticker.C {
281+
if s.shutdown {
282+
// process was closed already, exit monitoring
283+
return nil
284+
}
285+
261286
// check heartbeat
262287
if time.Since(s.lastActiveAt) > MAX_HEARTBEAT_INTERVAL {
263288
s.WalkNotifiers(func(notifier PluginInstanceNotifier) {
@@ -276,6 +301,7 @@ func (s *PluginInstance) Monitor() error {
276301
s.Stop()
277302
return ErrRuntimeNotActive
278303
}
304+
279305
if time.Since(s.lastActiveAt) > MAX_HEARTBEAT_INTERVAL/2 {
280306
// notify handlers
281307
s.WalkNotifiers(func(notifier PluginInstanceNotifier) {

0 commit comments

Comments
 (0)