Skip to content

Commit d7f0998

Browse files
authored
adjust shutdown process on the httpserver to remove fsm noise (#45)
1 parent 15873b6 commit d7f0998

File tree

2 files changed

+211
-63
lines changed

2 files changed

+211
-63
lines changed

runnables/httpserver/runner.go

Lines changed: 61 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ type HttpServer interface {
3232
Shutdown(ctx context.Context) error
3333
}
3434

35-
// Runner implements a configurable HTTP server that supports graceful shutdown,
36-
// dynamic reconfiguration, and state monitoring. It meets the Runnable, Reloadable,
37-
// and Stateable interfaces from the supervisor package.
35+
// Runner implements an HTTP server with graceful shutdown, dynamic reconfiguration,
36+
// and state monitoring. It implements the Runnable, Reloadable, and Stateable
37+
// interfaces from the supervisor package.
3838
type Runner struct {
3939
name string
4040
config atomic.Pointer[Config]
@@ -52,7 +52,7 @@ type Runner struct {
5252
logger *slog.Logger
5353
}
5454

55-
// NewRunner initializes a new HTTPServer runner instance.
55+
// NewRunner creates a new HTTP server runner instance with the provided options.
5656
func NewRunner(opts ...Option) (*Runner, error) {
5757
// Set default logger
5858
logger := slog.Default().WithGroup("httpserver.Runner")
@@ -115,7 +115,8 @@ func (r *Runner) String() string {
115115
return fmt.Sprintf("HTTPServer{%s}", strings.Join(args, ", "))
116116
}
117117

118-
// Run starts the HTTP server and listens for incoming requests
118+
// Run starts the HTTP server and handles its lifecycle. It transitions through
119+
// FSM states and returns when the server is stopped or encounters an error.
119120
func (r *Runner) Run(ctx context.Context) error {
120121
runCtx, runCancel := context.WithCancel(ctx)
121122
defer runCancel()
@@ -152,62 +153,28 @@ func (r *Runner) Run(ctx context.Context) error {
152153
return fmt.Errorf("%w: %w", ErrHttpServer, err)
153154
}
154155

155-
// Try to transition to Stopping state
156-
if !r.fsm.TransitionBool(finitestate.StatusStopping) {
157-
// If already in Stopping state, this is okay and we can continue
158-
if r.fsm.GetState() == finitestate.StatusStopping {
159-
r.logger.Debug("Already in Stopping state, continuing shutdown")
160-
} else {
161-
// Otherwise, this is a real failure
162-
r.setStateError()
163-
return fmt.Errorf("%w: transition to Stopping state", ErrStateTransition)
164-
}
165-
}
166-
167-
r.mutex.Lock()
168-
err = r.stopServer(runCtx)
169-
r.mutex.Unlock()
170-
171-
if err != nil {
172-
r.setStateError()
173-
// Return the error directly so it can be checked with errors.Is
174-
return err
175-
}
176-
177-
err = r.fsm.Transition(finitestate.StatusStopped)
178-
if err != nil {
179-
r.setStateError()
180-
return err
181-
}
182-
183-
r.logger.Debug("HTTP server shut down gracefully")
184-
return nil
156+
return r.shutdown(runCtx)
185157
}
186158

187-
// Stop will cancel the parent context, which will close the HTTP server
159+
// Stop signals the HTTP server to shut down by canceling its context.
188160
func (r *Runner) Stop() {
189-
// Only transition to Stopping if we're currently Running
190-
err := r.fsm.TransitionIfCurrentState(finitestate.StatusRunning, finitestate.StatusStopping)
191-
if err != nil {
192-
// This error is expected if we're already stopping, so only log at debug level
193-
r.logger.Debug("Note: Not transitioning to Stopping state", "error", err)
194-
}
161+
r.logger.Debug("Stopping HTTP server")
195162
r.cancel()
196163
}
197164

198-
// serverReadinessProbe checks if the HTTP server is listening and accepting connections
199-
// by attempting to establish a TCP connection to the server's address
165+
// serverReadinessProbe verifies the HTTP server is accepting connections by
166+
// repeatedly attempting TCP connections until success or timeout.
200167
func (r *Runner) serverReadinessProbe(ctx context.Context, addr string) error {
201-
// Create a dialer with timeout
168+
// Configure TCP dialer with connection timeout
202169
dialer := &net.Dialer{
203170
Timeout: 100 * time.Millisecond,
204171
}
205172

206-
// Set up a timeout context for the entire probe operation
173+
// Set timeout for the readiness probe operation
207174
probeCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
208175
defer cancel()
209176

210-
// Retry loop - attempt to connect until success or timeout
177+
// Retry connection attempts until success or timeout
211178
ticker := time.NewTicker(100 * time.Millisecond)
212179
defer ticker.Stop()
213180

@@ -219,17 +186,17 @@ func (r *Runner) serverReadinessProbe(ctx context.Context, addr string) error {
219186
// Attempt to establish a TCP connection
220187
conn, err := dialer.DialContext(ctx, "tcp", addr)
221188
if err == nil {
222-
// Connection successful, server is accepting connections
189+
// Server is ready and accepting connections
223190
if err := conn.Close(); err != nil {
224-
// Check if it's a "closed network connection" error
191+
// Ignore expected connection close errors
225192
if !errors.Is(err, net.ErrClosed) {
226193
r.logger.Warn("Error closing connection", "error", err)
227194
}
228195
}
229196
return nil
230197
}
231198

232-
// Connection failed, log and retry
199+
// Connection failed, continue retrying
233200
r.logger.Debug("Server not ready yet, retrying", "error", err)
234201
}
235202
}
@@ -241,7 +208,7 @@ func (r *Runner) boot() error {
241208
return ErrRetrieveConfig
242209
}
243210

244-
// Create a new Config with the same settings but use the Runner's context
211+
// Create server config using Runner's context for request handling
245212
serverCfg, err := NewConfig(
246213
originalCfg.ListenAddr,
247214
originalCfg.Routes,
@@ -254,7 +221,7 @@ func (r *Runner) boot() error {
254221

255222
listenAddr := serverCfg.ListenAddr
256223

257-
// Create the server, and reset the serverCloseOnce with a mutex
224+
// Initialize server instance and reset shutdown guard
258225
r.serverMutex.Lock()
259226
r.server = serverCfg.createServer()
260227
r.serverCloseOnce = sync.Once{}
@@ -267,7 +234,7 @@ func (r *Runner) boot() error {
267234
"idleTimeout", serverCfg.IdleTimeout,
268235
"drainTimeout", serverCfg.DrainTimeout)
269236

270-
// Start the server in a goroutine
237+
// Start HTTP server in background goroutine
271238
go func() {
272239
r.serverMutex.RLock()
273240
server := r.server
@@ -284,16 +251,16 @@ func (r *Runner) boot() error {
284251
r.logger.Debug("HTTP server stopped", "listenOn", listenAddr)
285252
}()
286253

287-
// Wait for the server to be ready or fail
254+
// Verify server is ready to accept connections
288255
if err := r.serverReadinessProbe(r.ctx, listenAddr); err != nil {
289-
// If probe fails, attempt to stop the server since it may be partially started
256+
// Clean up partially started server on readiness failure
290257
if err := r.stopServer(r.ctx); err != nil {
291258
r.logger.Warn("Error stopping server", "error", err)
292259
}
293260
return fmt.Errorf("%w: %w", ErrServerBoot, err)
294261
}
295262

296-
// Get the actual listening address for auto-assigned ports
263+
// Retrieve actual listening address for port 0 assignments
297264
actualAddr := listenAddr
298265
r.serverMutex.RLock()
299266
if tcpAddr, ok := r.server.(interface{ Addr() net.Addr }); ok && tcpAddr.Addr() != nil {
@@ -307,13 +274,13 @@ func (r *Runner) boot() error {
307274
return nil
308275
}
309276

310-
// setConfig atomically updates the current configuration
277+
// setConfig atomically stores the new configuration.
311278
func (r *Runner) setConfig(config *Config) {
312279
r.config.Store(config)
313280
r.logger.Debug("Config updated", "config", config)
314281
}
315282

316-
// getConfig returns the current configuration, loading it via the callback if necessary
283+
// getConfig returns the current configuration, loading it via callback if not set.
317284
func (r *Runner) getConfig() *Config {
318285
config := r.config.Load()
319286
if config != nil {
@@ -336,6 +303,8 @@ func (r *Runner) getConfig() *Config {
336303
return newConfig
337304
}
338305

306+
// stopServer performs graceful HTTP server shutdown with timeout handling.
307+
// It uses sync.Once to ensure shutdown occurs only once per server instance.
339308
func (r *Runner) stopServer(ctx context.Context) error {
340309
var shutdownErr error
341310
r.serverCloseOnce.Do(func() {
@@ -361,7 +330,7 @@ func (r *Runner) stopServer(ctx context.Context) error {
361330

362331
localErr := r.server.Shutdown(shutdownCtx)
363332

364-
// Check if the context deadline was exceeded, regardless of the error from Shutdown
333+
// Detect timeout regardless of Shutdown() return value
365334
select {
366335
case <-shutdownCtx.Done():
367336
if errors.Is(shutdownCtx.Err(), context.DeadlineExceeded) {
@@ -370,20 +339,50 @@ func (r *Runner) stopServer(ctx context.Context) error {
370339
return
371340
}
372341
default:
373-
// Context not done, normal shutdown
342+
// Shutdown completed within timeout
374343
}
375344

376-
// Handle any other error from shutdown
345+
// Handle other shutdown errors
377346
if localErr != nil {
378347
shutdownErr = fmt.Errorf("%w: %w", ErrGracefulShutdown, localErr)
379348
return
380349
}
381350
})
382351

383-
// if stopServer is called, always reset the server reference
352+
// Reset server reference after shutdown attempt
384353
r.serverMutex.Lock()
385354
r.server = nil
386355
r.serverMutex.Unlock()
387356

388357
return shutdownErr
389358
}
359+
360+
// shutdown coordinates HTTP server shutdown with FSM state management.
361+
// It transitions to Stopping state, calls stopServer, then transitions to Stopped.
362+
func (r *Runner) shutdown(ctx context.Context) error {
363+
logger := r.logger.WithGroup("shutdown")
364+
logger.Debug("Shutting down HTTP server")
365+
366+
// Begin shutdown by transitioning to Stopping state
367+
if err := r.fsm.Transition(finitestate.StatusStopping); err != nil {
368+
logger.Error("Failed to transition to stopping state", "error", err)
369+
// Continue shutdown even if state transition fails
370+
}
371+
372+
r.mutex.Lock()
373+
err := r.stopServer(ctx)
374+
r.mutex.Unlock()
375+
376+
if err != nil {
377+
r.setStateError()
378+
return err
379+
}
380+
381+
if err := r.fsm.Transition(finitestate.StatusStopped); err != nil {
382+
r.setStateError()
383+
return err
384+
}
385+
386+
logger.Debug("HTTP server shutdown complete")
387+
return nil
388+
}

0 commit comments

Comments
 (0)