Skip to content

Commit c6ec071

Browse files
authored
feat: add handling for SIGCHLD to cleanup child processes (#2043)
1 parent 1d91fa5 commit c6ec071

File tree

3 files changed

+87
-26
lines changed

3 files changed

+87
-26
lines changed

cmd/proxy/main.go

+25-25
Original file line numberDiff line numberDiff line change
@@ -63,22 +63,6 @@ func main() {
6363
Handler: handler,
6464
ReadHeaderTimeout: 2 * time.Second,
6565
}
66-
idleConnsClosed := make(chan struct{})
67-
68-
go func() {
69-
sigint := make(chan os.Signal, 1)
70-
signal.Notify(sigint, shutdown.GetSignals()...)
71-
s := <-sigint
72-
logger.WithField("signal", s).Infof("Received signal, shutting down server")
73-
74-
// We received an interrupt signal, shut down.
75-
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(conf.ShutdownTimeout))
76-
defer cancel()
77-
if err := srv.Shutdown(ctx); err != nil {
78-
logger.WithError(err).Fatal("Could not shut down server")
79-
}
80-
close(idleConnsClosed)
81-
}()
8266

8367
if conf.EnablePprof {
8468
go func() {
@@ -111,15 +95,31 @@ func main() {
11195
}
11296
}
11397

114-
if conf.TLSCertFile != "" && conf.TLSKeyFile != "" {
115-
err = srv.ServeTLS(ln, conf.TLSCertFile, conf.TLSKeyFile)
116-
} else {
117-
err = srv.Serve(ln)
118-
}
98+
signalCtx, signalStop := signal.NotifyContext(context.Background(), shutdown.GetSignals()...)
99+
reaper := shutdown.ChildProcReaper(signalCtx, logger.Logger)
119100

120-
if !errors.Is(err, http.ErrServerClosed) {
121-
logger.WithError(err).Fatal("Could not start server")
122-
}
101+
go func() {
102+
defer signalStop()
103+
if conf.TLSCertFile != "" && conf.TLSKeyFile != "" {
104+
err = srv.ServeTLS(ln, conf.TLSCertFile, conf.TLSKeyFile)
105+
} else {
106+
err = srv.Serve(ln)
107+
}
123108

124-
<-idleConnsClosed
109+
if !errors.Is(err, http.ErrServerClosed) {
110+
logger.WithError(err).Fatal("Could not start server")
111+
}
112+
}()
113+
114+
// Wait for shutdown signal, then cleanup before exit.
115+
<-signalCtx.Done()
116+
logger.Infof("Shutting down server")
117+
118+
// We received an interrupt signal, shut down.
119+
shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(conf.ShutdownTimeout))
120+
defer cancel()
121+
if err := srv.Shutdown(shutdownCtx); err != nil {
122+
logger.WithError(err).Fatal("Could not shut down server")
123+
}
124+
<-reaper.Done()
125125
}

internal/shutdown/signals.go

+44
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@
33
package shutdown
44

55
import (
6+
"context"
7+
"errors"
68
"os"
9+
"os/signal"
710
"syscall"
11+
12+
"github.com/sirupsen/logrus"
813
)
914

1015
// GetSignals returns the appropriate signals to catch for a clean shutdown, dependent on the OS.
@@ -13,3 +18,42 @@ import (
1318
func GetSignals() []os.Signal {
1419
return []os.Signal{os.Interrupt, syscall.SIGTERM}
1520
}
21+
22+
// ChildProcReaper spawns a goroutine to listen for SIGCHLD signals to cleanup
23+
// zombie child processes. The returned context will be canceled once all child
24+
// processes have been cleaned up, and should be waited on before exiting.
25+
//
26+
// This only applies to Unix platforms, and returns an already canceled context
27+
// on Windows.
28+
func ChildProcReaper(ctx context.Context, logger logrus.FieldLogger) context.Context {
29+
sigChld := make(chan os.Signal, 1)
30+
signal.Notify(sigChld, syscall.SIGCHLD)
31+
done, cancel := context.WithCancel(context.WithoutCancel(ctx))
32+
go func() {
33+
defer cancel()
34+
for {
35+
select {
36+
case <-ctx.Done():
37+
reap(logger)
38+
return
39+
case <-sigChld:
40+
reap(logger)
41+
}
42+
}
43+
}()
44+
return done
45+
}
46+
47+
func reap(logger logrus.FieldLogger) {
48+
for {
49+
var wstatus syscall.WaitStatus
50+
pid, err := syscall.Wait4(-1, &wstatus, syscall.WNOHANG, nil)
51+
if err != nil && !errors.Is(err, syscall.ECHILD) {
52+
logger.Errorf("failed to reap child process: %v", err)
53+
continue
54+
} else if pid <= 0 {
55+
return
56+
}
57+
logger.Infof("reaped child process %v, exit status: %v", pid, wstatus.ExitStatus())
58+
}
59+
}

internal/shutdown/signals_notunix.go

+18-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,26 @@
22

33
package shutdown
44

5-
import "os"
5+
import (
6+
"context"
7+
"os"
8+
9+
"github.com/sirupsen/logrus"
10+
)
611

712
// GetSignals returns the appropriate signals to catch for a clean shutdown, dependent on the OS.
813
func GetSignals() []os.Signal {
914
return []os.Signal{os.Interrupt}
1015
}
16+
17+
// ChildProcReaper spawns a goroutine to listen for SIGCHLD signals to cleanup
18+
// zombie child processes. The returned context will be canceled once all child
19+
// processes have been cleaned up, and should be waited on before exiting.
20+
//
21+
// This only applies to Unix platforms, and returns an already canceled context
22+
// on Windows.
23+
func ChildProcReaper(ctx context.Context, logger logrus.FieldLogger) context.Context {
24+
ctx, cancel := context.WithCancel(ctx)
25+
defer cancel()
26+
return ctx
27+
}

0 commit comments

Comments
 (0)