@@ -110,8 +110,21 @@ func NewServer(parse ParseFn, options ...OptionFn) (*Server, error) {
110110
111111// Server contains options for listening to an address.
112112type Server struct {
113- closing atomic.Bool
114- wg sync.WaitGroup
113+ closing atomic.Bool
114+ // Used to make reading closing and calling wg.Add be a single atomic operation.
115+ closingMu sync.RWMutex
116+ // This WaitGroup tracks the number of connections that are actively
117+ // serving a request. Idle connections are not counted. This is used during
118+ // Shutdown and Close to wait for outstanding requests to finish.
119+ //
120+ // Additionally it also waits for the Serve loop to stop listening for new
121+ // connections.
122+ wg sync.WaitGroup
123+ // Tracks total number of connection-serving goroutines, so tests can wait
124+ // for them to exit before the test's `t` becomes invalid.
125+ //
126+ // Additionally it also waits for the Go routine
127+ connWg sync.WaitGroup
115128 logger * slog.Logger
116129 Auth AuthStrategy
117130 BackendKeyData BackendKeyDataFunc
@@ -149,21 +162,19 @@ func (srv *Server) ListenAndServe(address string) error {
149162// preconfigured configurations. The given listener will be closed once the
150163// server is gracefully closed.
151164func (srv * Server ) Serve (listener net.Listener ) error {
152- // Early check to avoid logging and work if shutdown already started
165+ srv . closingMu . RLock ()
153166 if srv .closing .Load () {
167+ // Don't start serving if we're already shutting down or shut down.
168+ srv .closingMu .RUnlock ()
154169 return nil
155170 }
171+ srv .wg .Add (1 )
172+ srv .closingMu .RUnlock ()
156173
157- srv .logger .Info ("serving incoming connections" , slog .String ("addr" , listener .Addr ().String ()))
158-
159- // Double-check: if shutdown started between the check and Add, we must undo
160- if srv .closing .Load () {
161- return nil
162- }
174+ defer srv .wg .Done ()
163175
164- srv .wg .Add (1 )
165- go func () {
166- defer srv .wg .Done ()
176+ srv .logger .Info ("serving incoming connections" , slog .String ("addr" , listener .Addr ().String ()))
177+ srv .wg .Go (func () {
167178 <- srv .closer
168179
169180 srv .logger .Info ("closing server" )
@@ -172,7 +183,7 @@ func (srv *Server) Serve(listener net.Listener) error {
172183 if err != nil {
173184 srv .logger .Error ("unexpected error while attempting to close the net listener" , "err" , err )
174185 }
175- }( )
186+ })
176187
177188 for {
178189 conn , err := listener .Accept ()
@@ -190,7 +201,7 @@ func (srv *Server) Serve(listener net.Listener) error {
190201 continue
191202 }
192203
193- go func () {
204+ srv . connWg . Go ( func () {
194205 ctx := context .Background ()
195206 err = srv .serve (ctx , conn )
196207 if err != nil {
@@ -200,7 +211,7 @@ func (srv *Server) Serve(listener net.Listener) error {
200211 srv .logger .Error ("an unexpected error got returned while serving a client connection" , "err" , err )
201212 }
202213 }
203- }( )
214+ })
204215 }
205216}
206217
@@ -292,11 +303,14 @@ func (srv *Server) serve(ctx context.Context, conn net.Conn) error {
292303
293304// Close gracefully closes the underlaying Postgres server.
294305func (srv * Server ) Close () error {
295- if srv .closing .Load () {
306+ srv .closingMu .Lock ()
307+ if ! srv .closing .CompareAndSwap (false , true ) {
308+ srv .closingMu .Unlock ()
309+ srv .wg .Wait ()
296310 return nil
297311 }
312+ srv .closingMu .Unlock ()
298313
299- srv .closing .Store (true )
300314 close (srv .closer )
301315 srv .wg .Wait ()
302316 return nil
@@ -308,11 +322,14 @@ func (srv *Server) Close() error {
308322// If the context has no deadline, the server's ShutdownTimeout is used.
309323func (srv * Server ) Shutdown (ctx context.Context ) error {
310324 // Check if already shutting down or shut down
311- if srv .closing .Load () {
325+ srv .closingMu .Lock ()
326+ if ! srv .closing .CompareAndSwap (false , true ) {
312327 // If already closing, just wait for existing shutdown to complete
328+ srv .closingMu .Unlock ()
313329 srv .wg .Wait ()
314330 return nil
315331 }
332+ srv .closingMu .Unlock ()
316333
317334 // Use the shorter of context deadline or server timeout
318335 var shutdownCtx context.Context
@@ -329,13 +346,6 @@ func (srv *Server) Shutdown(ctx context.Context) error {
329346 }
330347 defer cancel ()
331348
332- // Atomically check and set closing state
333- if ! srv .closing .CompareAndSwap (false , true ) {
334- // Another goroutine beat us to it, just wait for shutdown to complete
335- srv .wg .Wait ()
336- return nil
337- }
338-
339349 srv .logger .Info ("starting graceful shutdown" )
340350
341351 // Close the closer channel (we're the first/only one to get here)
@@ -358,6 +368,13 @@ func (srv *Server) Shutdown(ctx context.Context) error {
358368 }
359369}
360370
371+ // Wait blocks until all connection-serving goroutines have finished. This is
372+ // intended to be called at the end of a test, so no go-routines are lingering.
373+ // This can block indefinitely if not all clients have disconnected.
374+ func (srv * Server ) Wait () {
375+ srv .connWg .Wait ()
376+ }
377+
361378// isNormalConnectionClosure checks if an error represents a normal client connection closure
362379// that shouldn't be logged as an error.
363380func (srv * Server ) isNormalConnectionClosure (err error ) bool {
0 commit comments