Skip to content

Commit 5ab6183

Browse files
authored
Gracefully Shutdown Foreground Server on Interrupt (#2863)
The solution here avoids the competing signal handlers by: Removing the os.Exit(0) from the default handler. When the default handler receives a signal, cancel the context that is plumbed to the run command. Remove the run signal handler and instead rely on context cancellation. This solution is not without risk: Commands that don’t check for context cancellation and relied on the os.Exit(0) may now continue running until they hit code that does respect the context cancellation. Based on my understanding of the code, thv run --foreground is one of the few commands that can be long running, so I think this risk is relatively small. Beyond the scope of this pull request, I think we should prefer to rely on context cancellation rather than using signal.Notify deep in the implementation of commands. This “competing signal handlers” problem likely exists elsewhere: I count 10 uses of signal.Notify. --------- Signed-off-by: Jeremy Drouillard <jeremy@stacklok.com>
1 parent c201f38 commit 5ab6183

File tree

2 files changed

+12
-12
lines changed

2 files changed

+12
-12
lines changed

cmd/thv/main.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package main
33

44
import (
5+
"context"
56
"os"
67
"os/signal"
78
"syscall"
@@ -23,7 +24,7 @@ func main() {
2324
logger.Initialize()
2425

2526
// Setup signal handling for graceful cleanup
26-
setupSignalHandler()
27+
ctx := setupSignalHandler()
2728

2829
// Clean up stale lock files on startup
2930
cleanupStaleLockFiles()
@@ -47,8 +48,10 @@ func main() {
4748
migration.CheckAndPerformDefaultGroupMigration()
4849
}
4950

51+
cmd := app.NewRootCmd(!app.IsCompletionCommand(os.Args) && !runtime.IsKubernetesRuntime())
52+
5053
// Skip update check for completion command or if we are running in kubernetes
51-
if err := app.NewRootCmd(!app.IsCompletionCommand(os.Args) && !runtime.IsKubernetesRuntime()).Execute(); err != nil {
54+
if err := cmd.ExecuteContext(ctx); err != nil {
5255
// Clean up any remaining lock files on error exit
5356
lockfile.CleanupAllLocks()
5457
os.Exit(1)
@@ -59,16 +62,19 @@ func main() {
5962
}
6063

6164
// setupSignalHandler configures signal handling to ensure lock files are cleaned up
62-
func setupSignalHandler() {
65+
func setupSignalHandler() context.Context {
6366
sigCh := make(chan os.Signal, 1)
6467
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
6568

69+
ctx, cancel := context.WithCancel(context.Background())
6670
go func() {
6771
<-sigCh
6872
logger.Debugf("Received signal, cleaning up lock files...")
6973
lockfile.CleanupAllLocks()
70-
os.Exit(0)
74+
cancel()
7175
}()
76+
77+
return ctx
7278
}
7379

7480
// cleanupStaleLockFiles removes stale lock files from known directories on startup

pkg/runner/runner.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ import (
88
"fmt"
99
"net/http"
1010
"os"
11-
"os/signal"
1211
"strings"
13-
"syscall"
1412
"time"
1513

1614
"golang.org/x/oauth2"
@@ -354,10 +352,6 @@ func (r *Runner) Run(ctx context.Context) error {
354352
logger.Info("Press Ctrl+C to stop or wait for container to exit")
355353
}
356354

357-
// Set up signal handling
358-
sigCh := make(chan os.Signal, 1)
359-
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
360-
361355
// Create a done channel to signal when the server has been stopped
362356
doneCh := make(chan struct{})
363357

@@ -399,8 +393,8 @@ func (r *Runner) Run(ctx context.Context) error {
399393

400394
// Wait for either a signal or the done channel to be closed
401395
select {
402-
case sig := <-sigCh:
403-
stopMCPServer(fmt.Sprintf("Received signal %s", sig))
396+
case <-ctx.Done():
397+
stopMCPServer("Context cancelled")
404398
case <-doneCh:
405399
// The transport has already been stopped (likely by the container exit)
406400
// Clean up the PID file and state

0 commit comments

Comments
 (0)