github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/kv/kvserver/replica_proposal_buf_test.go (about) 1 // Copyright 2019 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package kvserver 12 13 import ( 14 "math/rand" 15 "sync" 16 "testing" 17 "time" 18 19 "github.com/cockroachdb/cockroach/pkg/kv/kvserver/kvserverpb" 20 "github.com/cockroachdb/cockroach/pkg/roachpb" 21 "github.com/cockroachdb/cockroach/pkg/util/leaktest" 22 "github.com/cockroachdb/cockroach/pkg/util/syncutil" 23 "github.com/cockroachdb/errors" 24 "github.com/stretchr/testify/assert" 25 "github.com/stretchr/testify/require" 26 "go.etcd.io/etcd/raft" 27 "golang.org/x/sync/errgroup" 28 ) 29 30 // testProposer is a testing implementation of proposer. 31 type testProposer struct { 32 syncutil.RWMutex 33 ds destroyStatus 34 lai uint64 35 enqueued int 36 registered int 37 } 38 39 func (t *testProposer) locker() sync.Locker { 40 return &t.RWMutex 41 } 42 43 func (t *testProposer) rlocker() sync.Locker { 44 return t.RWMutex.RLocker() 45 } 46 47 func (t *testProposer) replicaID() roachpb.ReplicaID { 48 return 1 49 } 50 51 func (t *testProposer) destroyed() destroyStatus { 52 return t.ds 53 } 54 55 func (t *testProposer) leaseAppliedIndex() uint64 { 56 return t.lai 57 } 58 59 func (t *testProposer) enqueueUpdateCheck() { 60 t.enqueued++ 61 } 62 63 func (t *testProposer) withGroupLocked(fn func(*raft.RawNode) error) error { 64 // Pass nil for the RawNode, which FlushLockedWithRaftGroup supports. 65 return fn(nil) 66 } 67 68 func (t *testProposer) registerProposalLocked(p *ProposalData) { 69 t.registered++ 70 } 71 72 func newPropData(leaseReq bool) (*ProposalData, []byte) { 73 var ba roachpb.BatchRequest 74 if leaseReq { 75 ba.Add(&roachpb.RequestLeaseRequest{}) 76 } else { 77 ba.Add(&roachpb.PutRequest{}) 78 } 79 return &ProposalData{ 80 command: &kvserverpb.RaftCommand{}, 81 Request: &ba, 82 }, make([]byte, 0, kvserverpb.MaxRaftCommandFooterSize()) 83 } 84 85 // TestProposalBuffer tests the basic behavior of the Raft proposal buffer. 86 func TestProposalBuffer(t *testing.T) { 87 defer leaktest.AfterTest(t)() 88 89 var p testProposer 90 var b propBuf 91 b.Init(&p) 92 93 // Insert propBufArrayMinSize proposals. The buffer should not be flushed. 94 num := propBufArrayMinSize 95 for i := 0; i < num; i++ { 96 leaseReq := i == 3 97 pd, data := newPropData(leaseReq) 98 mlai, err := b.Insert(pd, data) 99 require.Nil(t, err) 100 if leaseReq { 101 expMlai := uint64(i) 102 require.Equal(t, uint64(0), mlai) 103 require.Equal(t, expMlai, pd.command.MaxLeaseIndex) 104 require.Equal(t, expMlai, b.LastAssignedLeaseIndexRLocked()) 105 } else { 106 expMlai := uint64(i + 1) 107 require.Equal(t, expMlai, mlai) 108 require.Equal(t, expMlai, pd.command.MaxLeaseIndex) 109 require.Equal(t, expMlai, b.LastAssignedLeaseIndexRLocked()) 110 } 111 require.Equal(t, i+1, b.Len()) 112 require.Equal(t, 1, p.enqueued) 113 require.Equal(t, 0, p.registered) 114 } 115 116 // Insert another proposal. This causes the buffer to flush. Doing so 117 // results in a lease applied index being skipped, which is harmless. 118 // Remember that the lease request above did not receive a lease index. 119 pd, data := newPropData(false) 120 mlai, err := b.Insert(pd, data) 121 require.Nil(t, err) 122 expMlai := uint64(num + 1) 123 require.Equal(t, expMlai, mlai) 124 require.Equal(t, expMlai, pd.command.MaxLeaseIndex) 125 require.Equal(t, expMlai, b.LastAssignedLeaseIndexRLocked()) 126 require.Equal(t, 1, b.Len()) 127 require.Equal(t, 2, p.enqueued) 128 require.Equal(t, num, p.registered) 129 require.Equal(t, uint64(num), b.liBase) 130 require.Equal(t, 2*propBufArrayMinSize, b.arr.len()) 131 132 // Increase the proposer's applied lease index and flush. The buffer's 133 // lease index offset should jump up. 134 p.lai = 10 135 require.Nil(t, b.flushLocked()) 136 require.Equal(t, 0, b.Len()) 137 require.Equal(t, 2, p.enqueued) 138 require.Equal(t, num+1, p.registered) 139 require.Equal(t, p.lai, b.liBase) 140 141 // Insert one more proposal. The lease applied index should adjust to 142 // the increase accordingly. 143 mlai, err = b.Insert(pd, data) 144 require.Nil(t, err) 145 expMlai = p.lai + 1 146 require.Equal(t, expMlai, mlai) 147 require.Equal(t, expMlai, pd.command.MaxLeaseIndex) 148 require.Equal(t, expMlai, b.LastAssignedLeaseIndexRLocked()) 149 require.Equal(t, 1, b.Len()) 150 require.Equal(t, 3, p.enqueued) 151 require.Equal(t, num+1, p.registered) 152 153 // Flush the buffer repeatedly until its array shrinks. We've already 154 // flushed once above, so start iterating at 1. 155 for i := 1; i < propBufArrayShrinkDelay; i++ { 156 require.Equal(t, 2*propBufArrayMinSize, b.arr.len()) 157 require.Nil(t, b.flushLocked()) 158 } 159 require.Equal(t, propBufArrayMinSize, b.arr.len()) 160 } 161 162 // TestProposalBufferConcurrentWithDestroy tests the concurrency properties of 163 // the Raft proposal buffer. 164 func TestProposalBufferConcurrentWithDestroy(t *testing.T) { 165 defer leaktest.AfterTest(t)() 166 167 var p testProposer 168 var b propBuf 169 b.Init(&p) 170 171 mlais := make(map[uint64]struct{}) 172 dsErr := errors.New("destroyed") 173 174 // Run 20 concurrent producers. 175 var g errgroup.Group 176 const concurrency = 20 177 for i := 0; i < concurrency; i++ { 178 g.Go(func() error { 179 for { 180 pd, data := newPropData(false) 181 mlai, err := b.Insert(pd, data) 182 if err != nil { 183 if errors.Is(err, dsErr) { 184 return nil 185 } 186 return errors.Wrap(err, "Insert") 187 } 188 p.Lock() 189 if _, ok := mlais[mlai]; ok { 190 p.Unlock() 191 return errors.New("max lease index collision") 192 } 193 mlais[mlai] = struct{}{} 194 p.Unlock() 195 } 196 }) 197 } 198 199 // Run a concurrent consumer. 200 g.Go(func() error { 201 for { 202 if stop, err := func() (bool, error) { 203 p.Lock() 204 defer p.Unlock() 205 if !p.ds.IsAlive() { 206 return true, nil 207 } 208 if err := b.flushLocked(); err != nil { 209 return true, errors.Wrap(err, "flushLocked") 210 } 211 return false, nil 212 }(); stop { 213 return err 214 } 215 } 216 }) 217 218 // Wait for a random duration before destroying. 219 time.Sleep(time.Duration(rand.Intn(1000)) * time.Microsecond) 220 221 // Destroy the proposer. All producers and consumers should notice. 222 p.Lock() 223 p.ds.Set(dsErr, destroyReasonRemoved) 224 p.Unlock() 225 226 require.Nil(t, g.Wait()) 227 t.Logf("%d successful proposals before destroy", len(mlais)) 228 } 229 230 // TestProposalBufferRegistersAllOnProposalError tests that all proposals in the 231 // proposal buffer are registered with the proposer when the buffer is flushed, 232 // even if an error is seen when proposing a batch of entries. 233 func TestProposalBufferRegistersAllOnProposalError(t *testing.T) { 234 defer leaktest.AfterTest(t)() 235 236 var p testProposer 237 var b propBuf 238 b.Init(&p) 239 240 num := propBufArrayMinSize 241 for i := 0; i < num; i++ { 242 pd, data := newPropData(false) 243 _, err := b.Insert(pd, data) 244 require.Nil(t, err) 245 } 246 require.Equal(t, num, b.Len()) 247 248 propNum := 0 249 propErr := errors.New("failed proposal") 250 b.testing.submitProposalFilter = func(*ProposalData) (drop bool, err error) { 251 propNum++ 252 require.Equal(t, propNum, p.registered) 253 if propNum == 2 { 254 return false, propErr 255 } 256 return false, nil 257 } 258 err := b.flushLocked() 259 require.Equal(t, propErr, err) 260 require.Equal(t, num, p.registered) 261 } 262 263 // TestProposalBufferRegistrationWithInsertionErrors tests that if during 264 // proposal insertion we reserve array indexes but are unable to actually insert 265 // them due to errors, we simply ignore said indexes when flushing proposals. 266 func TestProposalBufferRegistrationWithInsertionErrors(t *testing.T) { 267 defer leaktest.AfterTest(t)() 268 269 var p testProposer 270 var b propBuf 271 b.Init(&p) 272 273 num := propBufArrayMinSize / 2 274 for i := 0; i < num; i++ { 275 pd, data := newPropData(i%2 == 0) 276 _, err := b.Insert(pd, data) 277 require.Nil(t, err) 278 } 279 280 var insertErr = errors.New("failed insertion") 281 b.testing.leaseIndexFilter = func(*ProposalData) (indexOverride uint64, err error) { 282 return 0, insertErr 283 } 284 285 for i := 0; i < num; i++ { 286 pd, data := newPropData(i%2 == 0) 287 _, err := b.Insert(pd, data) 288 require.Equal(t, insertErr, err) 289 } 290 require.Equal(t, 2*num, b.Len()) 291 292 require.Nil(t, b.flushLocked()) 293 294 require.Equal(t, 0, b.Len()) 295 require.Equal(t, num, p.registered) 296 } 297 298 // TestPropBufCnt tests the basic behavior of the counter maintained by the 299 // proposal buffer. 300 func TestPropBufCnt(t *testing.T) { 301 defer leaktest.AfterTest(t)() 302 303 var count propBufCnt 304 const numReqs = 10 305 306 reqLeaseInc := makePropBufCntReq(true) 307 reqLeaseNoInc := makePropBufCntReq(false) 308 309 for i := 0; i < numReqs; i++ { 310 count.update(reqLeaseInc) 311 } 312 313 res := count.read() 314 assert.Equal(t, numReqs, res.arrayLen()) 315 assert.Equal(t, numReqs-1, res.arrayIndex()) 316 assert.Equal(t, uint64(numReqs), res.leaseIndexOffset()) 317 318 for i := 0; i < numReqs; i++ { 319 count.update(reqLeaseNoInc) 320 } 321 322 res = count.read() 323 assert.Equal(t, 2*numReqs, res.arrayLen()) 324 assert.Equal(t, (2*numReqs)-1, res.arrayIndex()) 325 assert.Equal(t, uint64(numReqs), res.leaseIndexOffset()) 326 327 count.clear() 328 res = count.read() 329 assert.Equal(t, 0, res.arrayLen()) 330 assert.Equal(t, -1, res.arrayIndex()) 331 assert.Equal(t, uint64(0), res.leaseIndexOffset()) 332 }