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  }