Skip to content

Commit 09c025b

Browse files
authored
Enable -race in tests and fix race conditions (#141)
1 parent 14d10e2 commit 09c025b

5 files changed

Lines changed: 58 additions & 29 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ lint: | $(GOLANGCI_LINT) ; $(info $(M) running golint…) @ ## Run the project l
3939

4040
.PHONY: test
4141
test: ## Run all tests
42-
$Q $(GO) test ./... -timeout 20s
42+
$Q $(GO) test ./... -timeout 20s -race
4343

4444
.PHONY: generate
4545
generate: | $(TOOLS) ; $(info $(M) running go generate…) @ ## Run go generate

command.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,15 @@ func (srv *Session) consumeSingleCommand(ctx context.Context, reader *buffer.Rea
131131
return err
132132
}
133133

134+
// We hold closingMu for reading while checking closing + adding to the
135+
// wait group, so that Close cannot finish wg.Wait before we are tracked.
136+
srv.closingMu.RLock()
134137
if srv.closing.Load() {
138+
srv.closingMu.RUnlock()
135139
return nil
136140
}
137-
138-
// NOTE: we increase the wait group by one in order to make sure that idle
139-
// connections are not blocking a close.
140141
srv.wg.Add(1)
142+
srv.closingMu.RUnlock()
141143
srv.logger.Debug("<- incoming command", slog.Int("length", length), slog.String("type", t.String()))
142144
err = srv.handleCommand(ctx, conn, t, reader, writer)
143145
srv.wg.Done()

pkg/mock/client.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,9 @@ func (client *Client) Close(t *testing.T) {
139139
if err != nil {
140140
t.Fatal(err)
141141
}
142+
143+
err = client.conn.Close()
144+
if err != nil {
145+
t.Fatal(err)
146+
}
142147
}

wire.go

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,21 @@ func NewServer(parse ParseFn, options ...OptionFn) (*Server, error) {
110110

111111
// Server contains options for listening to an address.
112112
type 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.
151164
func (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.
294305
func (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.
309323
func (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.
363380
func (srv *Server) isNormalConnectionClosure(err error) bool {

wire_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func TListenAndServe(t *testing.T, server *Server) *net.TCPAddr {
3737
if err != nil {
3838
t.Fatal(err)
3939
}
40+
server.Wait()
4041
})
4142

4243
go server.Serve(listener) //nolint:errcheck
@@ -1237,6 +1238,10 @@ func TListenAndServeWithoutCleanup(t *testing.T, server *Server) *net.TCPAddr {
12371238
t.Fatal(err)
12381239
}
12391240

1241+
t.Cleanup(func() {
1242+
server.Wait()
1243+
})
1244+
12401245
go server.Serve(listener) //nolint:errcheck
12411246
return listener.Addr().(*net.TCPAddr)
12421247
}

0 commit comments

Comments
 (0)