Skip to content

Commit ebb7e79

Browse files
etcdserver: add linearizable_read check to readyz.
Signed-off-by: Siyuan Zhang <[email protected]>
1 parent 293fc21 commit ebb7e79

File tree

2 files changed

+72
-25
lines changed

2 files changed

+72
-25
lines changed

server/etcdserver/api/etcdhttp/health.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@ import (
2323
"encoding/json"
2424
"fmt"
2525
"net/http"
26-
"time"
27-
2826
"path"
2927
"strings"
28+
"time"
3029

3130
"go.uber.org/zap"
3231

@@ -276,14 +275,19 @@ type CheckRegistry struct {
276275

277276
func installLivezEndpoints(lg *zap.Logger, mux *http.ServeMux, server ServerHealth) {
278277
reg := CheckRegistry{checkType: checkTypeLivez, checks: make(map[string]HealthCheck)}
279-
reg.Register("serializable_read", serializableReadCheck(server))
278+
reg.Register("serializable_read", readCheck(server, true /* serializable */))
280279
reg.InstallHttpEndpoints(lg, mux)
281280
}
282281

283282
func installReadyzEndpoints(lg *zap.Logger, mux *http.ServeMux, server ServerHealth) {
284283
reg := CheckRegistry{checkType: checkTypeReadyz, checks: make(map[string]HealthCheck)}
285284
reg.Register("data_corruption", activeAlarmCheck(server, pb.AlarmType_CORRUPT))
286-
reg.Register("serializable_read", serializableReadCheck(server))
285+
// serializable_read checks if local read is ok.
286+
// linearizable_read checks if there is consensus in the cluster.
287+
// Having both serializable_read and linearizable_read helps isolate the cause of problems if there is a read failure.
288+
reg.Register("serializable_read", readCheck(server, true))
289+
// linearizable_read check would be replaced by read_index check in 3.6
290+
reg.Register("linearizable_read", readCheck(server, false))
287291
reg.InstallHttpEndpoints(lg, mux)
288292
}
289293

@@ -447,13 +451,10 @@ func activeAlarmCheck(srv ServerHealth, at pb.AlarmType) func(context.Context) e
447451
}
448452
}
449453

450-
func serializableReadCheck(srv ServerHealth) func(ctx context.Context) error {
454+
func readCheck(srv ServerHealth, serializable bool) func(ctx context.Context) error {
451455
return func(ctx context.Context) error {
452456
ctx = srv.AuthStore().WithRoot(ctx)
453-
_, err := srv.Range(ctx, &pb.RangeRequest{KeysOnly: true, Limit: 1, Serializable: true})
454-
if err != nil {
455-
return fmt.Errorf("range error: %w", err)
456-
}
457-
return nil
457+
_, err := srv.Range(ctx, &pb.RangeRequest{KeysOnly: true, Limit: 1, Serializable: serializable})
458+
return err
458459
}
459460
}

server/etcdserver/api/etcdhttp/health_test.go

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,17 @@ import (
2323

2424
type fakeHealthServer struct {
2525
fakeServer
26-
apiError error
27-
missingLeader bool
28-
authStore auth.AuthStore
26+
serializableReadError error
27+
linearizableReadError error
28+
missingLeader bool
29+
authStore auth.AuthStore
2930
}
3031

31-
func (s *fakeHealthServer) Range(_ context.Context, _ *pb.RangeRequest) (*pb.RangeResponse, error) {
32-
return nil, s.apiError
32+
func (s *fakeHealthServer) Range(_ context.Context, req *pb.RangeRequest) (*pb.RangeResponse, error) {
33+
if req.Serializable {
34+
return nil, s.serializableReadError
35+
}
36+
return nil, s.linearizableReadError
3337
}
3438

3539
func (s *fakeHealthServer) Config() config.ServerConfig {
@@ -132,10 +136,11 @@ func TestHealthHandler(t *testing.T) {
132136
be, _ := betesting.NewDefaultTmpBackend(t)
133137
defer betesting.Close(t, be)
134138
HandleHealth(zaptest.NewLogger(t), mux, &fakeHealthServer{
135-
fakeServer: fakeServer{alarms: tt.alarms},
136-
apiError: tt.apiError,
137-
missingLeader: tt.missingLeader,
138-
authStore: auth.NewAuthStore(lg, be, nil, 0),
139+
fakeServer: fakeServer{alarms: tt.alarms},
140+
serializableReadError: tt.apiError,
141+
linearizableReadError: tt.apiError,
142+
missingLeader: tt.missingLeader,
143+
authStore: auth.NewAuthStore(lg, be, nil, 0),
139144
})
140145
ts := httptest.NewServer(mux)
141146
defer ts.Close()
@@ -171,8 +176,8 @@ func TestHttpSubPath(t *testing.T) {
171176
mux := http.NewServeMux()
172177
logger := zaptest.NewLogger(t)
173178
s := &fakeHealthServer{
174-
apiError: tt.apiError,
175-
authStore: auth.NewAuthStore(logger, be, nil, 0),
179+
serializableReadError: tt.apiError,
180+
authStore: auth.NewAuthStore(logger, be, nil, 0),
176181
}
177182
HandleHealth(logger, mux, s)
178183
ts := httptest.NewServer(mux)
@@ -255,23 +260,23 @@ func TestSerializableReadCheck(t *testing.T) {
255260
healthCheckURL: "/livez",
256261
apiError: fmt.Errorf("Unexpected error"),
257262
expectStatusCode: http.StatusServiceUnavailable,
258-
inResult: []string{"[-]serializable_read failed: range error: Unexpected error"},
263+
inResult: []string{"[-]serializable_read failed: Unexpected error"},
259264
},
260265
{
261266
name: "Not ready if range api is not available",
262267
healthCheckURL: "/readyz",
263268
apiError: fmt.Errorf("Unexpected error"),
264269
expectStatusCode: http.StatusServiceUnavailable,
265-
inResult: []string{"[-]serializable_read failed: range error: Unexpected error"},
270+
inResult: []string{"[-]serializable_read failed: Unexpected error"},
266271
},
267272
}
268273
for _, tt := range tests {
269274
t.Run(tt.name, func(t *testing.T) {
270275
mux := http.NewServeMux()
271276
logger := zaptest.NewLogger(t)
272277
s := &fakeHealthServer{
273-
apiError: tt.apiError,
274-
authStore: auth.NewAuthStore(logger, be, nil, 0),
278+
serializableReadError: tt.apiError,
279+
authStore: auth.NewAuthStore(logger, be, nil, 0),
275280
}
276281
HandleHealth(logger, mux, s)
277282
ts := httptest.NewServer(mux)
@@ -282,6 +287,47 @@ func TestSerializableReadCheck(t *testing.T) {
282287
}
283288
}
284289

290+
func TestLinearizableReadCheck(t *testing.T) {
291+
be, _ := betesting.NewDefaultTmpBackend(t)
292+
defer betesting.Close(t, be)
293+
tests := []healthTestCase{
294+
{
295+
name: "Alive normal",
296+
healthCheckURL: "/livez?verbose",
297+
expectStatusCode: http.StatusOK,
298+
inResult: []string{"[+]serializable_read ok"},
299+
},
300+
{
301+
name: "Alive if lineariable range api is not available",
302+
healthCheckURL: "/livez",
303+
apiError: fmt.Errorf("Unexpected error"),
304+
expectStatusCode: http.StatusOK,
305+
},
306+
{
307+
name: "Not ready if range api is not available",
308+
healthCheckURL: "/readyz",
309+
apiError: fmt.Errorf("Unexpected error"),
310+
expectStatusCode: http.StatusServiceUnavailable,
311+
inResult: []string{"[+]serializable_read ok", "[-]linearizable_read failed: Unexpected error"},
312+
},
313+
}
314+
for _, tt := range tests {
315+
t.Run(tt.name, func(t *testing.T) {
316+
mux := http.NewServeMux()
317+
logger := zaptest.NewLogger(t)
318+
s := &fakeHealthServer{
319+
linearizableReadError: tt.apiError,
320+
authStore: auth.NewAuthStore(logger, be, nil, 0),
321+
}
322+
HandleHealth(logger, mux, s)
323+
ts := httptest.NewServer(mux)
324+
defer ts.Close()
325+
checkHttpResponse(t, ts, tt.healthCheckURL, tt.expectStatusCode, tt.inResult, tt.notInResult)
326+
checkMetrics(t, tt.healthCheckURL, "linearizable_read", tt.expectStatusCode)
327+
})
328+
}
329+
}
330+
285331
func checkHttpResponse(t *testing.T, ts *httptest.Server, url string, expectStatusCode int, inResult []string, notInResult []string) {
286332
res, err := ts.Client().Do(&http.Request{Method: http.MethodGet, URL: testutil.MustNewURL(t, ts.URL+url)})
287333

0 commit comments

Comments
 (0)