From acca4fa93e43826648ee1c5e100ca6b52057dd31 Mon Sep 17 00:00:00 2001 From: Benjamin Wang Date: Tue, 6 Dec 2022 18:09:44 +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 --- etcdserver/util.go | 6 ++- etcdserver/util_test.go | 104 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/etcdserver/util.go b/etcdserver/util.go index bd80202a6..d44946f0d 100644 --- a/etcdserver/util.go +++ b/etcdserver/util.go @@ -138,7 +138,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/etcdserver/util_test.go b/etcdserver/util_test.go index 4adf6556d..c322da2bf 100644 --- a/etcdserver/util_test.go +++ b/etcdserver/util_test.go @@ -20,10 +20,12 @@ import ( "time" "go.uber.org/zap" + "go.uber.org/zap/zaptest" "go.etcd.io/etcd/etcdserver/api/membership" "go.etcd.io/etcd/etcdserver/api/rafthttp" "go.etcd.io/etcd/etcdserver/api/snap" + pb "go.etcd.io/etcd/etcdserver/etcdserverpb" "go.etcd.io/etcd/pkg/types" "go.etcd.io/etcd/raft/raftpb" ) @@ -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) + }) + } +}