@@ -32,9 +32,9 @@ type HttpServer interface {
32
32
Shutdown (ctx context.Context ) error
33
33
}
34
34
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.
38
38
type Runner struct {
39
39
name string
40
40
config atomic.Pointer [Config ]
@@ -52,7 +52,7 @@ type Runner struct {
52
52
logger * slog.Logger
53
53
}
54
54
55
- // NewRunner initializes a new HTTPServer runner instance.
55
+ // NewRunner creates a new HTTP server runner instance with the provided options .
56
56
func NewRunner (opts ... Option ) (* Runner , error ) {
57
57
// Set default logger
58
58
logger := slog .Default ().WithGroup ("httpserver.Runner" )
@@ -115,7 +115,8 @@ func (r *Runner) String() string {
115
115
return fmt .Sprintf ("HTTPServer{%s}" , strings .Join (args , ", " ))
116
116
}
117
117
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.
119
120
func (r * Runner ) Run (ctx context.Context ) error {
120
121
runCtx , runCancel := context .WithCancel (ctx )
121
122
defer runCancel ()
@@ -152,62 +153,28 @@ func (r *Runner) Run(ctx context.Context) error {
152
153
return fmt .Errorf ("%w: %w" , ErrHttpServer , err )
153
154
}
154
155
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 )
185
157
}
186
158
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.
188
160
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" )
195
162
r .cancel ()
196
163
}
197
164
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.
200
167
func (r * Runner ) serverReadinessProbe (ctx context.Context , addr string ) error {
201
- // Create a dialer with timeout
168
+ // Configure TCP dialer with connection timeout
202
169
dialer := & net.Dialer {
203
170
Timeout : 100 * time .Millisecond ,
204
171
}
205
172
206
- // Set up a timeout context for the entire probe operation
173
+ // Set timeout for the readiness probe operation
207
174
probeCtx , cancel := context .WithTimeout (ctx , 5 * time .Second )
208
175
defer cancel ()
209
176
210
- // Retry loop - attempt to connect until success or timeout
177
+ // Retry connection attempts until success or timeout
211
178
ticker := time .NewTicker (100 * time .Millisecond )
212
179
defer ticker .Stop ()
213
180
@@ -219,17 +186,17 @@ func (r *Runner) serverReadinessProbe(ctx context.Context, addr string) error {
219
186
// Attempt to establish a TCP connection
220
187
conn , err := dialer .DialContext (ctx , "tcp" , addr )
221
188
if err == nil {
222
- // Connection successful, server is accepting connections
189
+ // Server is ready and accepting connections
223
190
if err := conn .Close (); err != nil {
224
- // Check if it's a "closed network connection" error
191
+ // Ignore expected connection close errors
225
192
if ! errors .Is (err , net .ErrClosed ) {
226
193
r .logger .Warn ("Error closing connection" , "error" , err )
227
194
}
228
195
}
229
196
return nil
230
197
}
231
198
232
- // Connection failed, log and retry
199
+ // Connection failed, continue retrying
233
200
r .logger .Debug ("Server not ready yet, retrying" , "error" , err )
234
201
}
235
202
}
@@ -241,7 +208,7 @@ func (r *Runner) boot() error {
241
208
return ErrRetrieveConfig
242
209
}
243
210
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
245
212
serverCfg , err := NewConfig (
246
213
originalCfg .ListenAddr ,
247
214
originalCfg .Routes ,
@@ -254,7 +221,7 @@ func (r *Runner) boot() error {
254
221
255
222
listenAddr := serverCfg .ListenAddr
256
223
257
- // Create the server, and reset the serverCloseOnce with a mutex
224
+ // Initialize server instance and reset shutdown guard
258
225
r .serverMutex .Lock ()
259
226
r .server = serverCfg .createServer ()
260
227
r .serverCloseOnce = sync.Once {}
@@ -267,7 +234,7 @@ func (r *Runner) boot() error {
267
234
"idleTimeout" , serverCfg .IdleTimeout ,
268
235
"drainTimeout" , serverCfg .DrainTimeout )
269
236
270
- // Start the server in a goroutine
237
+ // Start HTTP server in background goroutine
271
238
go func () {
272
239
r .serverMutex .RLock ()
273
240
server := r .server
@@ -284,16 +251,16 @@ func (r *Runner) boot() error {
284
251
r .logger .Debug ("HTTP server stopped" , "listenOn" , listenAddr )
285
252
}()
286
253
287
- // Wait for the server to be ready or fail
254
+ // Verify server is ready to accept connections
288
255
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
290
257
if err := r .stopServer (r .ctx ); err != nil {
291
258
r .logger .Warn ("Error stopping server" , "error" , err )
292
259
}
293
260
return fmt .Errorf ("%w: %w" , ErrServerBoot , err )
294
261
}
295
262
296
- // Get the actual listening address for auto-assigned ports
263
+ // Retrieve actual listening address for port 0 assignments
297
264
actualAddr := listenAddr
298
265
r .serverMutex .RLock ()
299
266
if tcpAddr , ok := r .server .(interface { Addr () net.Addr }); ok && tcpAddr .Addr () != nil {
@@ -307,13 +274,13 @@ func (r *Runner) boot() error {
307
274
return nil
308
275
}
309
276
310
- // setConfig atomically updates the current configuration
277
+ // setConfig atomically stores the new configuration.
311
278
func (r * Runner ) setConfig (config * Config ) {
312
279
r .config .Store (config )
313
280
r .logger .Debug ("Config updated" , "config" , config )
314
281
}
315
282
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.
317
284
func (r * Runner ) getConfig () * Config {
318
285
config := r .config .Load ()
319
286
if config != nil {
@@ -336,6 +303,8 @@ func (r *Runner) getConfig() *Config {
336
303
return newConfig
337
304
}
338
305
306
+ // stopServer performs graceful HTTP server shutdown with timeout handling.
307
+ // It uses sync.Once to ensure shutdown occurs only once per server instance.
339
308
func (r * Runner ) stopServer (ctx context.Context ) error {
340
309
var shutdownErr error
341
310
r .serverCloseOnce .Do (func () {
@@ -361,7 +330,7 @@ func (r *Runner) stopServer(ctx context.Context) error {
361
330
362
331
localErr := r .server .Shutdown (shutdownCtx )
363
332
364
- // Check if the context deadline was exceeded, regardless of the error from Shutdown
333
+ // Detect timeout regardless of Shutdown() return value
365
334
select {
366
335
case <- shutdownCtx .Done ():
367
336
if errors .Is (shutdownCtx .Err (), context .DeadlineExceeded ) {
@@ -370,20 +339,50 @@ func (r *Runner) stopServer(ctx context.Context) error {
370
339
return
371
340
}
372
341
default :
373
- // Context not done, normal shutdown
342
+ // Shutdown completed within timeout
374
343
}
375
344
376
- // Handle any other error from shutdown
345
+ // Handle other shutdown errors
377
346
if localErr != nil {
378
347
shutdownErr = fmt .Errorf ("%w: %w" , ErrGracefulShutdown , localErr )
379
348
return
380
349
}
381
350
})
382
351
383
- // if stopServer is called, always reset the server reference
352
+ // Reset server reference after shutdown attempt
384
353
r .serverMutex .Lock ()
385
354
r .server = nil
386
355
r .serverMutex .Unlock ()
387
356
388
357
return shutdownErr
389
358
}
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