From c1a89973f041a02aa3cb1c8ebd43db0a433dba2b Mon Sep 17 00:00:00 2001 From: Benjamin Wang Date: Tue, 6 Dec 2022 18:04:29 +0800 Subject: [PATCH] etcdserver: fix nil pointer panic for readonly txn Backporting https://github.com/etcd-io/etcd/pull/14895 Signed-off-by: Benjamin Wang --- server/etcdserver/util.go | 6 +- server/etcdserver/util_test.go | 104 +++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/server/etcdserver/util.go b/server/etcdserver/util.go index 6ad5f0f4c..b048b8641 100644 --- a/server/etcdserver/util.go +++ b/server/etcdserver/util.go @@ -140,7 +140,11 @@ func warnOfExpensiveReadOnlyTxnRequest(lg *zap.Logger, warningApplyDuration time for _, r := range txnResponse.Responses { switch op := r.Response.(type) { case *pb.ResponseOp_ResponseRange: - resps = append(resps, fmt.Sprintf("range_response_count:%d", len(op.ResponseRange.Kvs))) + if op.ResponseRange != nil { + resps = append(resps, fmt.Sprintf("range_response_count:%d", len(op.ResponseRange.Kvs))) + } else { + resps = append(resps, "range_response:nil") + } default: // only range responses should be in a read only txn request } diff --git a/server/etcdserver/util_test.go b/server/etcdserver/util_test.go index dd57c7e1d..97673427b 100644 --- a/server/etcdserver/util_test.go +++ b/server/etcdserver/util_test.go @@ -20,7 +20,9 @@ import ( "time" "go.uber.org/zap" + "go.uber.org/zap/zaptest" + pb "go.etcd.io/etcd/api/v3/etcdserverpb" "go.etcd.io/etcd/client/pkg/v3/types" "go.etcd.io/etcd/raft/v3/raftpb" "go.etcd.io/etcd/server/v3/etcdserver/api/membership" @@ -110,3 +112,105 @@ type testStringerFunc func() string func (s testStringerFunc) String() string { return s() } + +// TestWarnOfExpensiveReadOnlyTxnRequest verifies WarnOfExpensiveReadOnlyTxnRequest +// never panic no matter what data the txnResponse contains. +func TestWarnOfExpensiveReadOnlyTxnRequest(t *testing.T) { + testCases := []struct { + name string + txnResp *pb.TxnResponse + }{ + { + name: "all readonly responses", + txnResp: &pb.TxnResponse{ + Responses: []*pb.ResponseOp{ + { + Response: &pb.ResponseOp_ResponseRange{ + ResponseRange: &pb.RangeResponse{}, + }, + }, + { + Response: &pb.ResponseOp_ResponseRange{ + ResponseRange: &pb.RangeResponse{}, + }, + }, + }, + }, + }, + { + name: "all readonly responses with partial nil responses", + txnResp: &pb.TxnResponse{ + Responses: []*pb.ResponseOp{ + { + Response: &pb.ResponseOp_ResponseRange{ + ResponseRange: &pb.RangeResponse{}, + }, + }, + { + Response: &pb.ResponseOp_ResponseRange{ + ResponseRange: nil, + }, + }, + }, + }, + }, + { + name: "all readonly responses with all nil responses", + txnResp: &pb.TxnResponse{ + Responses: []*pb.ResponseOp{ + { + Response: &pb.ResponseOp_ResponseRange{ + ResponseRange: nil, + }, + }, + { + Response: &pb.ResponseOp_ResponseRange{ + ResponseRange: nil, + }, + }, + }, + }, + }, + { + name: "partial non readonly responses", + txnResp: &pb.TxnResponse{ + Responses: []*pb.ResponseOp{ + { + Response: &pb.ResponseOp_ResponseRange{ + ResponseRange: nil, + }, + }, + { + Response: &pb.ResponseOp_ResponsePut{}, + }, + { + Response: &pb.ResponseOp_ResponseDeleteRange{}, + }, + }, + }, + }, + { + name: "all non readonly responses", + txnResp: &pb.TxnResponse{ + Responses: []*pb.ResponseOp{ + { + Response: &pb.ResponseOp_ResponsePut{}, + }, + { + Response: &pb.ResponseOp_ResponseDeleteRange{}, + }, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + lg := zaptest.NewLogger(t) + start := time.Now().Add(-1 * time.Second) + // WarnOfExpensiveReadOnlyTxnRequest shouldn't panic. + warnOfExpensiveReadOnlyTxnRequest(lg, 0, start, &pb.TxnRequest{}, tc.txnResp, nil) + }) + } +}