Skip to content

Commit f388dff

Browse files
committed
cmd/server: remove data race between Listen and Shutdown
Shutdown is now idempotent: it does nothing if the server was never started or was already stopped. Listen is guarded by sync.Once so the HTTP server can be started at most once.
1 parent e2de360 commit f388dff

File tree

2 files changed

+52
-25
lines changed

2 files changed

+52
-25
lines changed

api.go

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package toxiproxy
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"net/http"
89
"os"
910
"strings"
11+
"sync"
1012
"time"
1113

1214
"github.com/gorilla/mux"
@@ -34,7 +36,9 @@ type ApiServer struct {
3436
Collection *ProxyCollection
3537
Metrics *metricsContainer
3638
Logger *zerolog.Logger
37-
http *http.Server
39+
40+
initOnce sync.Once // guards shutdown
41+
shutdown func(ctx context.Context) error
3842
}
3943

4044
const (
@@ -50,42 +54,45 @@ func NewServer(m *metricsContainer, logger zerolog.Logger) *ApiServer {
5054
}
5155
}
5256

53-
func (server *ApiServer) Listen(addr string) error {
54-
server.Logger.
55-
Info().
56-
Str("address", addr).
57-
Msg("Starting Toxiproxy HTTP server")
57+
var errServerStarted = errors.New("server already started")
5858

59-
server.http = &http.Server{
60-
Addr: addr,
61-
Handler: server.Routes(),
62-
WriteTimeout: wait_timeout,
63-
ReadTimeout: read_timeout,
64-
IdleTimeout: 60 * time.Second,
59+
func (server *ApiServer) Listen(addr string) error {
60+
var s *http.Server
61+
server.initOnce.Do(func() {
62+
server.Logger.
63+
Info().
64+
Str("address", addr).
65+
Msg("Starting Toxiproxy HTTP server")
66+
67+
s = &http.Server{
68+
Addr: addr,
69+
Handler: server.Routes(),
70+
WriteTimeout: wait_timeout,
71+
ReadTimeout: read_timeout,
72+
IdleTimeout: 60 * time.Second,
73+
}
74+
})
75+
if s == nil {
76+
return errServerStarted
6577
}
6678

67-
err := server.http.ListenAndServe()
68-
if err == http.ErrServerClosed {
69-
err = nil
79+
server.shutdown = s.Shutdown
80+
err := s.ListenAndServe()
81+
if errors.Is(err, http.ErrServerClosed) {
82+
return nil
7083
}
71-
7284
return err
7385
}
7486

7587
func (server *ApiServer) Shutdown() error {
76-
if server.http == nil {
77-
return nil
88+
server.initOnce.Do(func() {})
89+
if server.shutdown == nil {
90+
return nil // in desire state
7891
}
7992

8093
ctx, cancel := context.WithTimeout(context.Background(), wait_timeout)
8194
defer cancel()
82-
83-
err := server.http.Shutdown(ctx)
84-
if err != nil {
85-
return err
86-
}
87-
88-
return nil
95+
return server.shutdown(ctx)
8996
}
9097

9198
func (server *ApiServer) Routes() *mux.Router {

api_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io"
77
"net/http"
88
"os"
9+
"strings"
910
"testing"
1011
"time"
1112

@@ -1124,6 +1125,25 @@ func TestInvalidStream(t *testing.T) {
11241125
})
11251126
}
11261127

1128+
// Issue https://github.com/Shopify/toxiproxy/issues/667
1129+
func TestApiServerShutdownListen(t *testing.T) {
1130+
s := &toxiproxy.ApiServer{Logger: &zerolog.Logger{}}
1131+
defer s.Shutdown()
1132+
1133+
got := s.Shutdown()
1134+
if got != nil {
1135+
t.Fatal("error shutting down unstarted server:", got)
1136+
}
1137+
1138+
got = s.Listen(":0")
1139+
// Avoid inventing an exported sentinal error just for this test, which is heavy handed
1140+
// given this error is specific only to starting the server twice, via the Listen method,
1141+
// and is unlikely to change, we can just do a substring match.
1142+
if !strings.Contains(got.Error(), "server already started") {
1143+
t.Fatal("error starting server:", got)
1144+
}
1145+
}
1146+
11271147
func AssertToxicExists(
11281148
t *testing.T,
11291149
toxics tclient.Toxics,

0 commit comments

Comments
 (0)