Skip to content

Commit 38947d2

Browse files
authored
fix: Gitter properly shutdown (#4618)
Actually recreate the gitter, and gracefully shutdown, as well as send the shutdown signal to subprocesses to avoid locking.
1 parent 1f6c5ac commit 38947d2

2 files changed

Lines changed: 93 additions & 20 deletions

File tree

deployment/clouddeploy/gke-workers/base/gitter.yaml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ metadata:
55
spec:
66
replicas: 1
77
strategy:
8-
type: RollingUpdate
9-
rollingUpdate:
10-
maxUnavailable: 1
11-
maxSurge: 1
8+
type: Recreate
129
selector:
1310
matchLabels:
1411
name: gitter
@@ -17,6 +14,7 @@ spec:
1714
labels:
1815
name: gitter
1916
spec:
17+
terminationGracePeriodSeconds: 60
2018
tolerations:
2119
- key: workloadType
2220
operator: Equal

go/cmd/gitter/gitter.go

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import (
1515
"net/http"
1616
"os"
1717
"os/exec"
18+
"os/signal"
1819
"path"
1920
"regexp"
2021
"strings"
22+
"syscall"
2123
"time"
2224

2325
"github.com/google/osv.dev/go/logger"
@@ -36,6 +38,39 @@ var (
3638
fetchTimeout time.Duration
3739
)
3840

41+
const shutdownTimeout = 10 * time.Second
42+
43+
// runCmd executes a command with context cancellation handled by sending SIGINT.
44+
// It logs cancellation errors separately as requested.
45+
func runCmd(ctx context.Context, dir string, env []string, name string, args ...string) error {
46+
cmd := exec.CommandContext(ctx, name, args...)
47+
if dir != "" {
48+
cmd.Dir = dir
49+
}
50+
if len(env) > 0 {
51+
cmd.Env = append(os.Environ(), env...)
52+
}
53+
// Use SIGINT instead of SIGKILL for graceful shutdown of subprocesses
54+
cmd.Cancel = func() error {
55+
return cmd.Process.Signal(syscall.SIGINT)
56+
}
57+
// Ensure it eventually dies if it ignores SIGINT
58+
cmd.WaitDelay = shutdownTimeout / 2
59+
60+
out, err := cmd.CombinedOutput()
61+
if err != nil {
62+
if ctx.Err() != nil {
63+
// Log separately if cancelled
64+
logger.Warn("Command cancelled", slog.String("cmd", name), slog.Any("err", ctx.Err()))
65+
return fmt.Errorf("command %s cancelled: %w", name, ctx.Err())
66+
}
67+
68+
return fmt.Errorf("command %s failed: %w, output: %s", name, err, out)
69+
}
70+
71+
return nil
72+
}
73+
3974
func isLocalRequest(r *http.Request) bool {
4075
host, _, err := net.SplitHostPort(r.RemoteAddr)
4176
if err != nil {
@@ -82,32 +117,31 @@ func fetchBlob(ctx context.Context, url string) ([]byte, error) {
82117
logger.Info("Fetching git blob", slog.String("url", url), slog.Duration("sinceAccessTime", time.Since(accessTime)))
83118
if _, err := os.Stat(path.Join(repoPath, ".git")); os.IsNotExist(err) {
84119
// Clone
85-
cmd := exec.Command("git", "clone", "--", url, repoPath)
86-
cmd.Env = append(cmd.Env, "GIT_TERMINAL_PROMPT=0")
87-
if out, err := cmd.CombinedOutput(); err != nil {
88-
return nil, fmt.Errorf("git clone failed: %w, output: %s", err, out)
120+
err := runCmd(ctx, "", []string{"GIT_TERMINAL_PROMPT=0"}, "git", "clone", "--", url, repoPath)
121+
if err != nil {
122+
return nil, fmt.Errorf("git clone failed: %w", err)
89123
}
90124
} else {
91125
// Fetch/Pull - implementing simple git pull for now, might need reset --hard if we want exact mirrors
92126
// For a generic "get latest", pull is usually sufficient if we treat it as read-only.
93127
// Ideally safely: git fetch origin && git reset --hard origin/HEAD
94-
cmd := exec.Command("git", "-C", repoPath, "fetch", "origin")
95-
if out, err := cmd.CombinedOutput(); err != nil {
96-
return nil, fmt.Errorf("git fetch failed: %w, output: %s", err, out)
128+
err := runCmd(ctx, repoPath, nil, "git", "fetch", "origin")
129+
if err != nil {
130+
return nil, fmt.Errorf("git fetch failed: %w", err)
97131
}
98-
cmd = exec.Command("git", "-C", repoPath, "reset", "--hard", "origin/HEAD")
99-
if out, err := cmd.CombinedOutput(); err != nil {
100-
return nil, fmt.Errorf("git reset failed: %w, output: %s", err, out)
132+
err = runCmd(ctx, repoPath, nil, "git", "reset", "--hard", "origin/HEAD")
133+
if err != nil {
134+
return nil, fmt.Errorf("git reset failed: %w", err)
101135
}
102136
}
103137

104138
logger.Info("Archiving git blob", slog.String("url", url))
105139
// Archive
106140
// tar --zstd -cf <archivePath> -C "<gitStorePath>/<repoDirName>" .
107141
// using -C to archive the relative path so it unzips nicely
108-
cmd := exec.Command("tar", "--zstd", "-cf", archivePath, "-C", path.Join(gitStorePath, repoDirName), ".")
109-
if out, err := cmd.CombinedOutput(); err != nil {
110-
return nil, fmt.Errorf("tar zstd failed: %w, output: %s", err, out)
142+
err := runCmd(ctx, "", nil, "tar", "--zstd", "-cf", archivePath, "-C", path.Join(gitStorePath, repoDirName), ".")
143+
if err != nil {
144+
return nil, fmt.Errorf("tar zstd failed: %w", err)
111145
}
112146

113147
updateLastFetch(url)
@@ -147,18 +181,57 @@ func main() {
147181

148182
loadMap()
149183

184+
// Create a context that listens for the interrupt signal from the OS.
185+
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
186+
defer stop()
187+
150188
http.HandleFunc(getGitEndpoint, gitHandler)
151189

152190
logger.Info("Gitter starting and listening", slog.Int("port", *port))
153191

192+
// --- Server Shutdown Protocol ---
193+
// This is what happens when a kubernetes send a SIGTERM signal:
194+
// 1. Kubernetes sends SIGTERM to the process
195+
// 2. The process receives the signal and prints "Shutting down gracefully..."
196+
// 3. The process calls server.Shutdown(ctx) to close incoming connections, and wait for timeout.
197+
// 4. The context within each request will be automatically cancelled (does not wait for timeout).
198+
// 5. Any subprocesses will be sent SIGINT, with a timeout / 2 duration before SIGKILL.
199+
// 6. The server waits for the timeout to finish processing all requests.
200+
// 7. We save the lastFetch map to disk.
201+
// 8. The process exits
202+
154203
server := &http.Server{
155204
Addr: fmt.Sprintf(":%d", *port),
156205
ReadHeaderTimeout: 3 * time.Second,
206+
BaseContext: func(_ net.Listener) context.Context {
207+
// Return the context tied to the termination signal.
208+
return ctx
209+
},
157210
}
158-
if err := server.ListenAndServe(); err != nil {
159-
logger.Error("Gitter failed to start", slog.Int("port", *port), slog.Any("error", err))
160-
os.Exit(1)
211+
212+
go func() {
213+
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
214+
logger.Error("Gitter failed to start", slog.Int("port", *port), slog.Any("error", err))
215+
}
216+
}()
217+
218+
// Listen for the interrupt signal.
219+
<-ctx.Done()
220+
221+
// Restore default behavior on the interrupt signal and notify user of shutdown.
222+
stop()
223+
logger.Info("Shutting down gracefully, press Ctrl+C again to force")
224+
225+
// The context is used to inform the server it has 5 seconds to finish
226+
// the request it is currently handling
227+
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
228+
defer cancel()
229+
if err := server.Shutdown(ctx); err != nil {
230+
logger.Error("Server forced to shutdown", slog.Any("error", err))
161231
}
232+
233+
saveMap()
234+
logger.Info("Server exiting")
162235
}
163236

164237
func gitHandler(w http.ResponseWriter, r *http.Request) {
@@ -203,4 +276,6 @@ func gitHandler(w http.ResponseWriter, r *http.Request) {
203276

204277
return
205278
}
279+
280+
logger.Info("Request completed successfully", slog.String("url", url))
206281
}

0 commit comments

Comments
 (0)