Skip to content

Commit 058b18f

Browse files
committed
feat: remove anti-pattern for graceful shutdown
1 parent 1d7aaed commit 058b18f

5 files changed

Lines changed: 78 additions & 56 deletions

File tree

cmd/server/server.go

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,22 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er
181181
fmt.Println(srv.GetOpenAPI())
182182
return nil
183183
}
184-
handleSignals(ctx, logger, srv, process, pidFile)
184+
185+
// Create a context for graceful shutdown
186+
gracefulCtx, gracefulCancel := context.WithCancel(ctx)
187+
defer gracefulCancel()
188+
189+
// Setup signal handlers (they will call gracefulCancel)
190+
handleSignals(gracefulCtx, gracefulCancel, logger, srv)
191+
192+
// Setup PID file cleanup
193+
if pidFile != "" {
194+
defer cleanupPIDFile(pidFile, logger)
195+
}
196+
185197
logger.Info("Starting server on port", "port", port)
198+
199+
// Monitor process exit
186200
processExitCh := make(chan error, 1)
187201
go func() {
188202
defer close(processExitCh)
@@ -193,18 +207,52 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er
193207
processExitCh <- xerrors.Errorf("failed to wait for process: %w", err)
194208
}
195209
}
196-
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
197-
defer cancel()
198-
if err := srv.Stop(shutdownCtx); err != nil {
199-
logger.Error("Failed to stop server after process exit", "error", err)
210+
211+
select {
212+
case <-gracefulCtx.Done():
213+
default:
214+
gracefulCancel()
215+
}
216+
}()
217+
218+
// Start the server
219+
serverErrCh := make(chan error, 1)
220+
go func() {
221+
defer close(serverErrCh)
222+
if err := srv.Start(); err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, http.ErrServerClosed) {
223+
serverErrCh <- err
200224
}
201225
}()
202-
if err := srv.Start(); err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, http.ErrServerClosed) {
203-
return xerrors.Errorf("failed to start server: %w", err)
226+
227+
select {
228+
case err := <-serverErrCh:
229+
if err != nil {
230+
return xerrors.Errorf("failed to start server: %w", err)
231+
}
232+
case <-gracefulCtx.Done():
204233
}
234+
235+
if err := srv.SaveState("shutdown"); err != nil {
236+
logger.Error("Failed to save state during shutdown", "error", err)
237+
}
238+
239+
// Stop the HTTP server
240+
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
241+
defer cancel()
242+
if err := srv.Stop(shutdownCtx); err != nil {
243+
logger.Error("Failed to stop HTTP server", "error", err)
244+
}
245+
246+
// Close the process
247+
if err := process.Close(logger, 5*time.Second); err != nil {
248+
logger.Error("Failed to close process cleanly", "error", err)
249+
}
250+
205251
select {
206252
case err := <-processExitCh:
207-
return xerrors.Errorf("agent exited with error: %w", err)
253+
if err != nil {
254+
return xerrors.Errorf("agent exited with error: %w", err)
255+
}
208256
default:
209257
}
210258
return nil

cmd/server/signals.go

Lines changed: 0 additions & 41 deletions
This file was deleted.

cmd/server/signals_unix.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,20 @@ import (
1010
"syscall"
1111

1212
"github.com/coder/agentapi/lib/httpapi"
13-
"github.com/coder/agentapi/lib/termexec"
1413
)
1514

1615
// handleSignals sets up signal handlers for:
17-
// - SIGTERM, SIGINT, SIGHUP: save conversation state, stop server, then close the process
16+
// - SIGTERM, SIGINT, SIGHUP: trigger graceful shutdown by canceling the context
1817
// - SIGUSR1: save conversation state without exiting
19-
func handleSignals(ctx context.Context, logger *slog.Logger, srv *httpapi.Server, process *termexec.Process, pidFile string) {
18+
func handleSignals(ctx context.Context, cancel context.CancelFunc, logger *slog.Logger, srv *httpapi.Server) {
2019
// Handle shutdown signals (SIGTERM, SIGINT, SIGHUP)
2120
shutdownCh := make(chan os.Signal, 1)
2221
signal.Notify(shutdownCh, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGINT)
2322
go func() {
2423
defer signal.Stop(shutdownCh)
2524
sig := <-shutdownCh
26-
performGracefulShutdown(sig, logger, srv, process, pidFile)
25+
logger.Info("Received shutdown signal", "signal", sig)
26+
cancel()
2727
}()
2828

2929
// Handle SIGUSR1 for save without exit

cmd/server/signals_windows.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@ import (
1010
"syscall"
1111

1212
"github.com/coder/agentapi/lib/httpapi"
13-
"github.com/coder/agentapi/lib/termexec"
1413
)
1514

1615
// handleSignals sets up signal handlers for Windows.
1716
// Only handles SIGTERM and SIGINT (SIGHUP and SIGUSR1 don't exist on Windows).
18-
func handleSignals(ctx context.Context, logger *slog.Logger, srv *httpapi.Server, process *termexec.Process, pidFile string) {
17+
func handleSignals(ctx context.Context, cancel context.CancelFunc, logger *slog.Logger, srv *httpapi.Server) {
1918
// Handle shutdown signals (SIGTERM, SIGINT only on Windows)
2019
shutdownCh := make(chan os.Signal, 1)
2120
signal.Notify(shutdownCh, os.Interrupt, syscall.SIGTERM)
2221
go func() {
2322
defer signal.Stop(shutdownCh)
2423
sig := <-shutdownCh
25-
performGracefulShutdown(sig, logger, srv, process, pidFile)
24+
logger.Info("Received shutdown signal", "signal", sig)
25+
cancel()
2626
}()
2727
}

lib/httpapi/server.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ type Server struct {
4949
chatBasePath string
5050
tempDir string
5151
clock quartz.Clock
52+
shutdownCtx context.Context
53+
shutdown context.CancelFunc
5254
}
5355

5456
func (s *Server) NormalizeSchema(schema any) any {
@@ -275,6 +277,8 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) {
275277
}
276278
logger.Info("Created temporary directory for uploads", "tempDir", tempDir)
277279

280+
ctx, cancel := context.WithCancel(context.Background())
281+
278282
s := &Server{
279283
router: router,
280284
api: api,
@@ -287,6 +291,8 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) {
287291
chatBasePath: strings.TrimSuffix(config.ChatBasePath, "/"),
288292
tempDir: tempDir,
289293
clock: config.Clock,
294+
shutdownCtx: ctx,
295+
shutdown: cancel,
290296
}
291297

292298
// Register API routes
@@ -514,6 +520,7 @@ func (s *Server) uploadFiles(ctx context.Context, input *struct {
514520
func (s *Server) subscribeEvents(ctx context.Context, input *struct{}, send sse.Sender) {
515521
subscriberId, ch, stateEvents := s.emitter.Subscribe()
516522
defer s.emitter.Unsubscribe(subscriberId)
523+
517524
s.logger.Info("New subscriber", "subscriberId", subscriberId)
518525
for _, event := range stateEvents {
519526
if event.Type == EventTypeScreenUpdate {
@@ -539,6 +546,9 @@ func (s *Server) subscribeEvents(ctx context.Context, input *struct{}, send sse.
539546
s.logger.Error("Failed to send event", "subscriberId", subscriberId, "error", err)
540547
return
541548
}
549+
case <-s.shutdownCtx.Done():
550+
s.logger.Info("Server stop initiated, unsubscribing.", "subscriberId", subscriberId)
551+
return
542552
case <-ctx.Done():
543553
s.logger.Info("Context done", "subscriberId", subscriberId)
544554
return
@@ -573,6 +583,9 @@ func (s *Server) subscribeScreen(ctx context.Context, input *struct{}, send sse.
573583
s.logger.Error("Failed to send screen event", "subscriberId", subscriberId, "error", err)
574584
return
575585
}
586+
case <-s.shutdownCtx.Done():
587+
s.logger.Info("Server stop initiated, unsubscribing.", "subscriberId", subscriberId)
588+
return
576589
case <-ctx.Done():
577590
s.logger.Info("Screen context done", "subscriberId", subscriberId)
578591
return
@@ -595,6 +608,8 @@ func (s *Server) Start() error {
595608
func (s *Server) Stop(ctx context.Context) error {
596609
var err error
597610
s.stopOnce.Do(func() {
611+
s.shutdown()
612+
598613
// Clean up temporary directory
599614
s.cleanupTempDir()
600615

0 commit comments

Comments
 (0)