@@ -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"
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+
3974func 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
164237func 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