Skip to content

Commit 26856b7

Browse files
committed
feat: handle graceful shutdown
Trap termination signals in the main program using context.Context. If a SIGINT or SIGTERM is received, send a SIGTERM to the sub-programs and wait for them to shutdown gracefully. If the sub-program is stuck for 5 minutes, force-kill them.
1 parent ccc5e1c commit 26856b7

File tree

3 files changed

+130
-10
lines changed

3 files changed

+130
-10
lines changed

command/cleanup.go

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func CmdCleanup(c *cli.Context) (err error) {
2626

2727
var toUndeploy []string
2828

29-
ctx := context.Background()
29+
ctx := contextWithHandler()
3030
ghCli := githubClient(ctx, c)
3131
owner, repo := githubSlug(c)
3232

@@ -37,7 +37,7 @@ func CmdCleanup(c *cli.Context) (err error) {
3737
// undeploy all closed pull requests
3838
var deployed []string
3939

40-
deployed, err = listDeployedPullRequests(listScript)
40+
deployed, err = listDeployedPullRequests(ctx, listScript)
4141
if err != nil {
4242
return err
4343
}
@@ -86,13 +86,36 @@ func CmdCleanup(c *cli.Context) (err error) {
8686
cmd.Stdout = os.Stdout
8787
cmd.Stderr = os.Stderr
8888

89-
err = cmd.Run()
89+
err = cmd.Start()
9090
if err != nil {
9191
log.Println("undeploy error: ", err)
9292

9393
lastErr = err
9494

95-
continue
95+
select {
96+
case <-ctx.Done():
97+
log.Println("undeploy cancelled: ", ctx.Err())
98+
99+
return lastErr
100+
default:
101+
continue
102+
}
103+
}
104+
105+
err = waitOrStop(ctx, cmd)
106+
if err != nil {
107+
log.Println("undeploy error: ", err)
108+
109+
lastErr = err
110+
111+
select {
112+
case <-ctx.Done():
113+
log.Println("undeploy cancelled: ", ctx.Err())
114+
115+
return lastErr
116+
default:
117+
continue
118+
}
96119
}
97120

98121
destroyGitHubDeployments(ctx, ghCli, owner, repo, pullRequestID, ignoreMissing)
@@ -112,13 +135,22 @@ func contains(item string, list []string) bool {
112135
}
113136

114137
// Get the list of deployed Pull request based on given script.
115-
func listDeployedPullRequests(listScript string) ([]string, error) {
116-
var stdout strings.Builder
138+
func listDeployedPullRequests(ctx context.Context, listScript string) ([]string, error) {
139+
var (
140+
stdout strings.Builder
141+
err error
142+
)
117143

118144
cmd := exec.Command(listScript)
119145
cmd.Stdout = &stdout
120146

121-
if err := cmd.Run(); err != nil {
147+
err = cmd.Start()
148+
if err != nil {
149+
return nil, err
150+
}
151+
152+
err = waitOrStop(ctx, cmd)
153+
if err != nil {
122154
return nil, err
123155
}
124156

command/please.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package command
22

33
import (
44
"bytes"
5-
"context"
65
"errors"
76
"fmt"
87
"io"
@@ -89,7 +88,7 @@ func CmdPlease(c *cli.Context) (err error) {
8988
environment = fmt.Sprintf("pr-%d", pr)
9089
}
9190

92-
ctx := context.Background()
91+
ctx := contextWithHandler()
9392
ghCli := githubClient(ctx, c)
9493

9594
log.Println("deploy ref", ref)
@@ -168,7 +167,7 @@ func CmdPlease(c *cli.Context) (err error) {
168167
}
169168

170169
// Wait on the deploy to finish
171-
err = cmd.Wait()
170+
err = waitOrStop(ctx, cmd)
172171
if err != nil {
173172
err2 := updateStatus(StateFailure, "")
174173
if err2 != nil {

command/utils.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ package command
33
import (
44
"context"
55
"log"
6+
"os"
7+
"os/exec"
8+
"os/signal"
69
"regexp"
10+
"syscall"
11+
"time"
712

813
"github.com/google/go-github/github"
914
secretvalue "github.com/zimbatm/go-secretvalue"
@@ -61,3 +66,87 @@ func refString(str string) *string {
6166
func refStringList(l []string) *[]string {
6267
return &l
6368
}
69+
70+
var DefaultKillDelay = 5 * time.Minute
71+
72+
// waitOrStop waits for the already-started command cmd by calling its Wait method.
73+
//
74+
// If cmd does not return before ctx is done, waitOrStop sends it the given interrupt signal.
75+
// waitOrStop waits DefaultKillDelay for Wait to return before sending os.Kill.
76+
//
77+
// This function is copied from the one added to x/playground/internal in
78+
// http://golang.org/cl/228438.
79+
func waitOrStop(ctx context.Context, cmd *exec.Cmd) error {
80+
if cmd.Process == nil {
81+
panic("waitOrStop called with a nil cmd.Process — missing Start call?")
82+
}
83+
84+
errc := make(chan error)
85+
go func() {
86+
select {
87+
case errc <- nil:
88+
return
89+
case <-ctx.Done():
90+
}
91+
92+
err := cmd.Process.Signal(os.Interrupt)
93+
if err == nil {
94+
err = ctx.Err() // Report ctx.Err() as the reason we interrupted.
95+
} else if err.Error() == "os: process already finished" {
96+
errc <- nil
97+
98+
return
99+
}
100+
101+
if DefaultKillDelay > 0 {
102+
timer := time.NewTimer(DefaultKillDelay)
103+
select {
104+
// Report ctx.Err() as the reason we interrupted the process...
105+
case errc <- ctx.Err():
106+
timer.Stop()
107+
108+
return
109+
// ...but after killDelay has elapsed, fall back to a stronger signal.
110+
case <-timer.C:
111+
}
112+
113+
// Wait still hasn't returned.
114+
// Kill the process harder to make sure that it exits.
115+
//
116+
// Ignore any error: if cmd.Process has already terminated, we still
117+
// want to send ctx.Err() (or the error from the Interrupt call)
118+
// to properly attribute the signal that may have terminated it.
119+
_ = cmd.Process.Kill()
120+
}
121+
122+
errc <- err
123+
}()
124+
125+
waitErr := cmd.Wait()
126+
127+
interruptErr := <-errc
128+
if interruptErr != nil {
129+
return interruptErr
130+
}
131+
132+
return waitErr
133+
}
134+
135+
// contextWithHandler returns a context that is canceled when the program receives a SIGINT or SIGTERM.
136+
//
137+
// !! Only call this function once per program.
138+
func contextWithHandler() context.Context {
139+
ctx, cancel := context.WithCancel(context.Background())
140+
141+
signalChan := make(chan os.Signal, 1)
142+
143+
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
144+
145+
go func() {
146+
sig := <-signalChan
147+
log.Printf("Received signal %s, stopping", sig)
148+
cancel()
149+
}()
150+
151+
return ctx
152+
}

0 commit comments

Comments
 (0)