fix(common): update kvstorev2

with: #22409822 of #22357426

Signed-off-by: xiejian <xiejian3@oppo.com>
This commit is contained in:
xiejian 2024-07-16 10:08:44 +08:00 committed by slasher
parent b7cade30e6
commit 75bc515d0b
64 changed files with 4014 additions and 14972 deletions

View File

@ -0,0 +1,321 @@
// Copyright 2023 The Cuber Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
package kvstore
import (
"context"
"errors"
)
const (
defaultCF = "default"
RocksdbLsmKVType = LsmKVType("rocksdb")
FIFOStyle = CompactionStyle("fifo")
LevelStyle = CompactionStyle("level")
UniversalStyle = CompactionStyle("universal")
defaultReadConcurrency = 10
defaultReadQueueLen = 10
defaultWriteConcurrency = 4
defaultWriteQueueLen = 10
)
var (
ErrNotFound = errors.New("key not found")
ErrKVTypeNotFound = errors.New("kv type not found")
)
type (
CF string
LsmKVType string
CompactionStyle string
Store interface {
NewSnapshot() Snapshot
CreateColumn(col CF) error
GetAllColumns() []CF
CheckColumns(col CF) bool
Get(ctx context.Context, col CF, key []byte, opts ...ReadOptFunc) (value ValueGetter, err error)
GetRaw(ctx context.Context, col CF, key []byte, opts ...ReadOptFunc) (value []byte, err error)
MultiGet(ctx context.Context, col CF, keys [][]byte, opts ...ReadOptFunc) (values []ValueGetter, err error)
SetRaw(ctx context.Context, col CF, key []byte, value []byte, opts ...WriteOptFunc) error
Delete(ctx context.Context, col CF, key []byte, opts ...WriteOptFunc) error
DeleteRange(ctx context.Context, col CF, start, end []byte, opts ...WriteOptFunc) error
List(ctx context.Context, col CF, prefix []byte, marker []byte, readOpt ReadOption) ListReader
Write(ctx context.Context, batch WriteBatch, opts ...WriteOptFunc) error
Read(ctx context.Context, cols []CF, keys [][]byte, opts ...ReadOptFunc) (values []ValueGetter, err error)
GetOptionHelper() (helper OptionHelper)
NewReadOption() (readOption ReadOption)
NewWriteOption() (writeOption WriteOption)
NewWriteBatch() (writeBatch WriteBatch)
FlushCF(ctx context.Context, col CF) error
Stats(ctx context.Context) (Stats, error)
Close()
}
OptionHelper interface {
GetOption() Option
SetMaxBackgroundJobs(value int) error
SetMaxBackgroundCompactions(value int) error
SetMaxSubCompactions(value int) error
SetMaxOpenFiles(value int) error
SetMaxWriteBufferNumber(value int) error
SetWriteBufferSize(size int) error
SetArenaBlockSize(size int) error
SetTargetFileSizeBase(value uint64) error
SetMaxBytesForLevelBase(value uint64) error
SetLevel0SlowdownWritesTrigger(value int) error
SetLevel0StopWritesTrigger(value int) error
SetSoftPendingCompactionBytesLimit(value uint64) error
SetHardPendingCompactionBytesLimit(value uint64) error
SetBlockSize(size int) error
SetFIFOCompactionMaxTableFileSize(size int) error
SetFIFOCompactionAllow(value bool) error
}
ReadOption interface {
SetSnapShot(snap Snapshot)
Close()
}
ReadOptFunc func(opts *readOpts)
WriteOption interface {
SetSync(value bool)
DisableWAL(value bool)
Close()
}
WriteOptFunc func(*writeOpts)
LruCache interface {
GetUsage() uint64
GetPinnedUsage() uint64
Close()
}
WriteBufferManager interface {
Close()
}
RateLimiter interface {
SetBytesPerSec(value int64)
Close()
}
ListReader interface {
ReadNext() (key KeyGetter, val ValueGetter, err error)
ReadNextCopy() (key []byte, value []byte, err error)
ReadPrev() (key KeyGetter, val ValueGetter, err error)
ReadPrevCopy() (key []byte, value []byte, err error)
ReadLast() (key KeyGetter, val ValueGetter, err error)
SeekToLast()
SeekForPrev(key []byte) (err error)
Seek(key []byte)
SetFilterKey(key []byte)
Close()
}
KeyGetter interface {
Key() []byte
Close()
}
ValueGetter interface {
Value() []byte
Read([]byte) (n int, err error)
Size() int
Close()
}
Snapshot interface {
Close()
}
Env interface {
SetLowPriorityBackgroundThreads(n int)
SetHighPriorityBackgroundThreads(n int)
Close()
}
SstFileManager interface {
Close()
}
WriteBatch interface {
Put(col CF, key, value []byte)
Delete(col CF, key []byte)
DeleteRange(col CF, startKey, endKey []byte)
Data() []byte
From(data []byte)
Count() int
Clear()
Close()
// Iterator()
}
Stats struct {
Used uint64
MemoryUsage MemoryUsage
Level0FileNum uint64
WriteSlowdown bool
WriteStop bool
RunningFlush uint64
PendingFlush bool
RunningCompaction uint64
PendingCompaction bool
BackgroundErrors uint64
}
MemoryUsage struct {
BlockCacheUsage uint64
IndexAndFilterUsage uint64
MemtableUsage uint64
BlockPinnedUsage uint64
Total uint64
}
Option struct {
Sync bool `json:"sync,omitempty"`
DisableWal bool `json:"disable_wal,omitempty"`
ColumnFamily []CF `json:"column_family,omitempty"`
CreateIfMissing bool `json:"create_if_missingC"`
BlockSize int `json:"block_size,omitempty"`
BlockCache uint64 `json:"block_cache,omitempty"`
EnablePipelinedWrite bool `json:"enable_pipelined_write,omitempty"`
MaxBackgroundJobs int `json:"max_background_jobs,omitempty"`
MaxBackgroundCompactions int `json:"max_background_compactions,omitempty"`
MaxBackgroundFlushes int `json:"max_background_flushes,omitempty"`
MaxSubCompactions int `json:"max_sub_compactions,omitempty"`
LevelCompactionDynamicLevelBytes bool `json:"level_compaction_dynamic_level_bytes,omitempty"`
MaxOpenFiles int `json:"max_open_files,omitempty"`
MinWriteBufferNumberToMerge int `json:"min_write_buffer_number_to_merge,omitempty"`
MaxWriteBufferNumber int `json:"max_write_buffer_number,omitempty"`
WriteBufferSize int `json:"write_buffer_size,omitempty"`
ArenaBlockSize int `json:"arena_block_size,omitempty"`
TargetFileSizeBase uint64 `json:"target_file_size_base,omitempty"`
MaxBytesForLevelBase uint64 `json:"max_bytes_for_level_base,omitempty"`
KeepLogFileNum int `json:"keep_log_file_num,omitempty"`
MaxLogFileSize int `json:"max_log_file_size,omitempty"`
Level0SlowdownWritesTrigger int `json:"level_0_slowdown_writes_trigger,omitempty"`
Level0StopWritesTrigger int `json:"level_0_stop_writes_trigger,omitempty"`
SoftPendingCompactionBytesLimit uint64 `json:"soft_pending_compaction_bytes_limit,omitempty"`
HardPendingCompactionBytesLimit uint64 `json:"hard_pending_compaction_bytes_limit,omitempty"`
MaxWalLogSize uint64 `json:"max_wal_log_size,omitempty"`
CompactionStyle CompactionStyle `json:"compaction_style,omitempty"`
CompactionOptionFIFO CompactionOptionFIFO `json:"compaction_option_fifo,omitempty"`
Cache LruCache
WriteBufferManager WriteBufferManager
Env Env
SstFileManager SstFileManager
HandleError HandleError
ReadConcurrency int `json:"read_concurrency,omitempty"`
ReadQueueLen int `json:"read_queue_len,omitempty"`
WriteConcurrency int `json:"write_concurrency,omitempty"`
WriteQueueLen int `json:"write_queue_len,omitempty"`
}
CompactionOptionFIFO struct {
MaxTableFileSize int `json:"max_table_file_size,omitempty"`
AllowCompaction bool `json:"allow_compaction,omitempty"`
}
HandleError func(err error)
readOpts struct {
opt ReadOption
withNoMerge bool
}
writeOpts struct {
opt WriteOption
withNoMerge bool
}
)
func NewKVStore(ctx context.Context, path string, lsmType LsmKVType, option *Option) (Store, error) {
switch lsmType {
case RocksdbLsmKVType:
return newRocksdb(ctx, path, option)
default:
return nil, ErrNotFound
}
}
func NewCache(ctx context.Context, lsmType LsmKVType, size uint64) LruCache {
switch lsmType {
case RocksdbLsmKVType:
return newRocksdbLruCache(ctx, size)
default:
return nil
}
}
func NewWriteBufferManager(ctx context.Context, lsmType LsmKVType, size uint64) WriteBufferManager {
switch lsmType {
case RocksdbLsmKVType:
return newRocksdbWriteBufferManager(ctx, size)
default:
return nil
}
}
func NewEnv(ctx context.Context, lsmType LsmKVType) Env {
switch lsmType {
case RocksdbLsmKVType:
return newRocksdbEnv(ctx)
default:
return nil
}
}
func NewSstFileManager(ctx context.Context, lsmType LsmKVType, env Env) SstFileManager {
switch lsmType {
case RocksdbLsmKVType:
return newRocksdbSstFileManager(ctx, env)
default:
return nil
}
}
func WithReadOption(opt ReadOption) ReadOptFunc {
return func(ro *readOpts) {
ro.opt = opt
}
}
func WithNoMergeRead() ReadOptFunc {
return func(ro *readOpts) {
ro.withNoMerge = true
}
}
func WithWriteOption(opt WriteOption) WriteOptFunc {
return func(wo *writeOpts) {
wo.opt = opt
}
}
func WithNoMergeWrite() WriteOptFunc {
return func(wo *writeOpts) {
wo.withNoMerge = true
}
}
func (cf CF) String() string {
return string(cf)
}
func (ro *readOpts) applyOptions(opts []ReadOptFunc) {
for _, opt := range opts {
if opt != nil {
opt(ro)
}
}
}
func (wo *writeOpts) applyOptions(opts []WriteOptFunc) {
for _, opt := range opts {
if opt != nil {
opt(wo)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,64 @@
// Copyright 2023 The Cuber Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
package kvstore
import (
"context"
"os"
"testing"
"github.com/stretchr/testify/require"
)
//go:generate mockgen -source=./kvstore.go -destination=./kvstore_mock.go -package=kvstore
func TestNewEngine(t *testing.T) {
ctx := context.TODO()
path, err := genTmpPath()
require.NoError(t, err)
defer os.RemoveAll(path)
opt := new(Option)
opt.CreateIfMissing = true
eg, err := NewKVStore(ctx, path, RocksdbLsmKVType, opt)
require.NoError(t, err)
eg.Close()
}
func TestNewWriteBufferManager(t *testing.T) {
mgr1 := NewWriteBufferManager(context.TODO(), RocksdbLsmKVType, 1<<10)
mgr1.Close()
mgr2 := NewWriteBufferManager(context.TODO(), "", 1<<10)
require.Equal(t, nil, mgr2)
}
func TestNewCache(t *testing.T) {
cache1 := NewCache(context.TODO(), RocksdbLsmKVType, 1<<10)
cache1.Close()
cache2 := NewCache(context.TODO(), "", 1<<10)
require.Equal(t, nil, cache2)
}
func TestNewEnv(t *testing.T) {
ctx := context.TODO()
NewEnv(ctx, RocksdbLsmKVType)
}
func TestNewSstFileManager(t *testing.T) {
ctx := context.TODO()
env := NewEnv(ctx, RocksdbLsmKVType)
NewSstFileManager(ctx, RocksdbLsmKVType, env)
}

View File

@ -0,0 +1,285 @@
package kvstore
import (
"fmt"
"strconv"
rdb "github.com/tecbot/gorocksdb"
)
func (oph *optHelper) GetOption() Option {
oph.lock.RLock()
opt := *oph.opt
oph.lock.RUnlock()
return opt
}
func (oph *optHelper) SetMaxBackgroundJobs(value int) error {
oph.lock.Lock()
defer oph.lock.Unlock()
if err := oph.db.SetDBOptions([]string{"max_background_jobs"}, []string{strconv.Itoa(value)}); err != nil {
return err
}
oph.opt.MaxBackgroundJobs = value
return nil
}
func (oph *optHelper) SetMaxBackgroundCompactions(value int) error {
oph.lock.Lock()
defer oph.lock.Unlock()
if err := oph.db.SetDBOptions([]string{"max_background_compactions"}, []string{strconv.Itoa(value)}); err != nil {
return err
}
oph.opt.MaxBackgroundCompactions = value
return nil
}
func (oph *optHelper) SetMaxSubCompactions(value int) error {
oph.lock.Lock()
// todo
oph.opt.MaxSubCompactions = value
oph.lock.Unlock()
return nil
}
func (oph *optHelper) SetMaxOpenFiles(value int) error {
oph.lock.Lock()
defer oph.lock.Unlock()
if err := oph.db.SetDBOptions([]string{"max_open_files"}, []string{strconv.Itoa(value)}); err != nil {
return err
}
oph.opt.MaxOpenFiles = value
return nil
}
func (oph *optHelper) SetMaxWriteBufferNumber(value int) error {
oph.lock.Lock()
defer oph.lock.Unlock()
if err := oph.db.SetOptions([]string{"max_write_buffer_number"}, []string{strconv.Itoa(value)}); err != nil {
return err
}
oph.opt.MaxWriteBufferNumber = value
return nil
}
func (oph *optHelper) SetWriteBufferSize(size int) error {
oph.lock.Lock()
defer oph.lock.Unlock()
if err := oph.db.SetOptions([]string{"write_buffer_size"}, []string{strconv.Itoa(size)}); err != nil {
return err
}
oph.opt.WriteBufferSize = size
return nil
}
func (oph *optHelper) SetArenaBlockSize(size int) error {
oph.lock.Lock()
defer oph.lock.Unlock()
if err := oph.db.SetOptions([]string{"arena_block_size"}, []string{strconv.Itoa(size)}); err != nil {
return err
}
oph.opt.ArenaBlockSize = size
return nil
}
func (oph *optHelper) SetTargetFileSizeBase(value uint64) error {
oph.lock.Lock()
defer oph.lock.Unlock()
if err := oph.db.SetOptions([]string{"target_file_size_base"}, []string{strconv.FormatUint(value, 10)}); err != nil {
return err
}
oph.opt.TargetFileSizeBase = value
return nil
}
func (oph *optHelper) SetMaxBytesForLevelBase(value uint64) error {
oph.lock.Lock()
defer oph.lock.Unlock()
if err := oph.db.SetOptions([]string{"max_bytes_for_level_base"}, []string{strconv.FormatUint(value, 10)}); err != nil {
return err
}
oph.opt.MaxBytesForLevelBase = value
return nil
}
func (oph *optHelper) SetLevel0SlowdownWritesTrigger(value int) error {
oph.lock.Lock()
defer oph.lock.Unlock()
if err := oph.db.SetOptions([]string{"level0_slowdown_writes_trigger"}, []string{strconv.Itoa(value)}); err != nil {
return err
}
oph.opt.Level0SlowdownWritesTrigger = value
return nil
}
func (oph *optHelper) SetLevel0StopWritesTrigger(value int) error {
oph.lock.Lock()
defer oph.lock.Unlock()
if err := oph.db.SetOptions([]string{"level0_stop_writes_trigger"}, []string{strconv.Itoa(value)}); err != nil {
return err
}
oph.opt.Level0StopWritesTrigger = value
return nil
}
func (oph *optHelper) SetSoftPendingCompactionBytesLimit(value uint64) error {
oph.lock.Lock()
defer oph.lock.Unlock()
if err := oph.db.SetOptions([]string{"soft_pending_compaction_bytes_limit"}, []string{strconv.FormatUint(value, 10)}); err != nil {
return err
}
oph.opt.SoftPendingCompactionBytesLimit = value
return nil
}
func (oph *optHelper) SetHardPendingCompactionBytesLimit(value uint64) error {
oph.lock.Lock()
defer oph.lock.Unlock()
if err := oph.db.SetOptions([]string{"hard_pending_compaction_bytes_limit"}, []string{strconv.FormatUint(value, 10)}); err != nil {
return err
}
oph.opt.HardPendingCompactionBytesLimit = value
return nil
}
func (oph *optHelper) SetBlockSize(size int) error {
oph.lock.Lock()
// todo
oph.opt.BlockSize = size
oph.lock.Unlock()
return nil
}
func (oph *optHelper) SetFIFOCompactionMaxTableFileSize(size int) error {
oph.lock.Lock()
defer oph.lock.Unlock()
if err := oph.db.SetOptions(formatFIFOCompactionOption("max_table_files_size", strconv.Itoa(size))); err != nil {
return err
}
oph.opt.CompactionOptionFIFO.MaxTableFileSize = size
return nil
}
func (oph *optHelper) SetFIFOCompactionAllow(value bool) error {
oph.lock.Lock()
defer oph.lock.Unlock()
v := "false"
if value {
v = "true"
}
if err := oph.db.SetOptions(formatFIFOCompactionOption("allow_compaction", v)); err != nil {
return err
}
oph.opt.CompactionOptionFIFO.AllowCompaction = value
return nil
}
func genRocksdbOpts(opt *Option) (opts *rdb.Options) {
opts = rdb.NewDefaultOptions()
blockBaseOpt := rdb.NewDefaultBlockBasedTableOptions()
fifoCompactionOpt := rdb.NewDefaultFIFOCompactionOptions()
opts.SetCreateIfMissing(opt.CreateIfMissing)
if opt.BlockSize > 0 {
blockBaseOpt.SetBlockSize(opt.BlockSize)
}
if opt.Cache != nil {
blockBaseOpt.SetBlockCache(opt.Cache.(*lruCache).cache)
// blockBaseOpt.SetCacheIndexAndFilterBlocks(true)
} else {
if opt.BlockCache > 0 {
blockBaseOpt.SetBlockCache(rdb.NewLRUCache(opt.BlockCache))
}
}
opts.SetEnablePipelinedWrite(opt.EnablePipelinedWrite)
if opt.MaxBackgroundCompactions > 0 {
opts.SetMaxBackgroundCompactions(opt.MaxBackgroundCompactions)
}
if opt.MaxBackgroundFlushes > 0 {
opts.SetMaxBackgroundFlushes(opt.MaxBackgroundFlushes)
}
if opt.MaxSubCompactions > 0 {
opts.SetMaxSubCompactions(opt.MaxSubCompactions)
}
opts.SetLevelCompactionDynamicLevelBytes(opt.LevelCompactionDynamicLevelBytes)
if opt.MaxOpenFiles > 0 {
opts.SetMaxOpenFiles(opt.MaxOpenFiles)
}
if opt.MinWriteBufferNumberToMerge > 0 {
opts.SetMinWriteBufferNumberToMerge(opt.MinWriteBufferNumberToMerge)
}
if opt.MaxWriteBufferNumber > 0 {
opts.SetMaxWriteBufferNumber(opt.MaxWriteBufferNumber)
}
if opt.WriteBufferSize > 0 {
opts.SetWriteBufferSize(opt.WriteBufferSize)
}
if opt.ArenaBlockSize > 0 {
opts.SetArenaBlockSize(opt.ArenaBlockSize)
}
if opt.TargetFileSizeBase > 0 {
opts.SetTargetFileSizeBase(opt.TargetFileSizeBase)
}
if opt.MaxBytesForLevelBase > 0 {
opts.SetMaxBytesForLevelBase(opt.MaxBytesForLevelBase)
}
if opt.KeepLogFileNum > 0 {
opts.SetKeepLogFileNum(opt.KeepLogFileNum)
}
if opt.MaxLogFileSize > 0 {
opts.SetMaxLogFileSize(opt.MaxLogFileSize)
}
if opt.Level0SlowdownWritesTrigger > 0 {
opts.SetLevel0SlowdownWritesTrigger(opt.Level0SlowdownWritesTrigger)
}
if opt.Level0StopWritesTrigger > 0 {
opts.SetLevel0StopWritesTrigger(opt.Level0StopWritesTrigger)
}
if opt.SoftPendingCompactionBytesLimit > 0 {
opts.SetSoftPendingCompactionBytesLimit(opt.SoftPendingCompactionBytesLimit)
}
if opt.HardPendingCompactionBytesLimit > 0 {
opts.SetHardPendingCompactionBytesLimit(opt.HardPendingCompactionBytesLimit)
}
if len(opt.CompactionStyle) > 0 {
switch opt.CompactionStyle {
case FIFOStyle:
opts.SetCompactionStyle(rdb.FIFOCompactionStyle)
case LevelStyle:
opts.SetCompactionStyle(rdb.LevelCompactionStyle)
case UniversalStyle:
opts.SetCompactionStyle(rdb.UniversalCompactionStyle)
default:
}
}
if opt.CompactionOptionFIFO.MaxTableFileSize > 0 {
fifoCompactionOpt.SetMaxTableFilesSize(uint64(opt.CompactionOptionFIFO.MaxTableFileSize))
}
if opt.WriteBufferManager != nil {
opts.SetWriteBufferManager(opt.WriteBufferManager.(*writeBufferManager).manager)
}
if opt.MaxWalLogSize > 0 {
opts.SetMaxTotalWalSize(opt.MaxWalLogSize)
}
if opt.Env != nil {
opts.SetEnv(opt.Env.(*env).Env)
} else {
opts.SetEnv(rdb.NewDefaultEnv())
}
if opt.SstFileManager != nil {
opts.SetSstFileManager(opt.SstFileManager.(*sstFileManager).SstFileManager)
}
opts.SetStatsDumpPeriodSec(0)
opts.SetStatsPersistPeriodSec(0)
opts.SetBlockBasedTableFactory(blockBaseOpt)
opts.SetFIFOCompactionOptions(fifoCompactionOpt)
opts.SetCreateIfMissingColumnFamilies(true)
return
}
func formatFIFOCompactionOption(key, value string) ([]string, []string) {
s := fmt.Sprintf("%s=%s;", key, value)
return []string{"compaction_options_fifo"}, []string{s}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,549 @@
// Copyright 2023 The Cuber Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
package kvstore
import (
"context"
"errors"
"fmt"
"io"
"os"
"strconv"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
type testEg struct {
engine Store
path string
opt *Option
}
func genTmpPath() (string, error) {
id := uuid.NewString()
path := os.TempDir() + "/" + id
if err := os.RemoveAll(path); err != nil {
return "", err
}
if err := os.MkdirAll(path, 0o755); err != nil {
return "", err
}
return path, nil
}
func newEngine(ctx context.Context, opt *Option) (*testEg, error) {
path, err := genTmpPath()
if err != nil {
return nil, err
}
var _opt *Option
if opt != nil {
_opt = opt
} else {
_opt = new(Option)
}
_opt.CreateIfMissing = true
_opt.Sync = true
_opt.ReadConcurrency = 10
_opt.ReadQueueLen = 10
_opt.WriteConcurrency = 10
_opt.WriteQueueLen = 10
engine, err := newRocksdb(ctx, path, _opt)
if err != nil {
return nil, err
}
return &testEg{
engine: engine,
path: path,
opt: _opt,
}, nil
}
func (eg *testEg) close() {
eg.engine.Close()
os.RemoveAll(eg.path)
}
func Test_openRocksdb(t *testing.T) {
ctx := context.TODO()
path, err := genTmpPath()
require.NoError(t, err)
defer os.RemoveAll(path)
opt := new(Option)
opt.CreateIfMissing = true
opt.CompactionOptionFIFO = CompactionOptionFIFO{
MaxTableFileSize: 1 << 10,
AllowCompaction: false,
}
opt.BlockSize = 1 << 20
opt.BlockCache = 1 << 20
opt.MaxSubCompactions = 8
opt.MaxBackgroundJobs = 8
opt.MaxBackgroundCompactions = 8
opt.KeepLogFileNum = 10000
opt.MaxLogFileSize = 1 << 30
opt.ColumnFamily = []CF{"a", "b", "c"}
opt.CompactionStyle = FIFOStyle
eg, err := newRocksdb(ctx, path, opt)
require.NoError(t, err)
eg.Close()
// open with empty path
_, err = newRocksdb(ctx, "", opt)
require.Equal(t, errors.New("path is empty"), err)
// reopen db
eg, err = newRocksdb(ctx, path, opt)
require.NoError(t, err)
eg.Close()
// open with wrong cf
opt.ColumnFamily = []CF{"a", "b"}
_, err = newRocksdb(ctx, path, opt)
require.Error(t, err)
}
func TestInstance_CreateColumn(t *testing.T) {
ctx := context.TODO()
eg, err := newEngine(ctx, nil)
require.NoError(t, err)
defer eg.close()
err = eg.engine.CreateColumn("colA")
require.NoError(t, err)
cols := eg.engine.GetAllColumns()
fmt.Println(cols)
}
func TestInstance_SetGetRaw(t *testing.T) {
ctx := context.TODO()
eg, err := newEngine(ctx, nil)
require.NoError(t, err)
defer eg.close()
k := []byte("key1")
v := []byte("value1")
err = eg.engine.SetRaw(ctx, defaultCF, k, v)
require.NoError(t, err)
v1, err := eg.engine.GetRaw(ctx, defaultCF, k)
require.NoError(t, err)
require.Equal(t, v, v1)
v2, err := eg.engine.Get(ctx, defaultCF, k)
require.NoError(t, err)
require.Equal(t, v, v2.Value())
err = eg.engine.Delete(ctx, defaultCF, k)
require.NoError(t, err)
err = eg.engine.SetRaw(ctx, defaultCF, k, v, WithNoMergeWrite())
require.NoError(t, err)
v1, err = eg.engine.GetRaw(ctx, defaultCF, k)
require.NoError(t, err)
require.Equal(t, v, v1)
err = eg.engine.Delete(ctx, defaultCF, k)
require.NoError(t, err)
v1, err = eg.engine.GetRaw(ctx, defaultCF, k)
require.Nil(t, v1)
require.Equal(t, ErrNotFound, err)
}
func TestWriteRead(t *testing.T) {
ctx := context.TODO()
eg, err := newEngine(ctx, nil)
require.NoError(t, err)
defer eg.close()
col1 := CF("c1")
err = eg.engine.CreateColumn(col1)
require.Nil(t, err)
n := 100
cfs := make([]CF, n)
keys := make([][]byte, n)
values := make([][]byte, n)
batch := eg.engine.NewWriteBatch()
for i := 0; i < n; i++ {
keyStr := []byte(fmt.Sprintf("k%d", i))
valStr := []byte(fmt.Sprintf("v%d", i))
cfs[i] = col1
keys[i] = keyStr
values[i] = valStr
if i/2 != 0 {
batch.Put(col1, keyStr, valStr)
}
}
for i := 0; i < n; i++ {
if i/2 == 0 {
err = eg.engine.SetRaw(ctx, col1, keys[i], values[i])
require.Nil(t, err)
continue
}
err = eg.engine.Write(ctx, batch)
require.Nil(t, err)
}
// read
vgs, err := eg.engine.Read(ctx, cfs, keys)
require.Nil(t, err)
for i := range vgs {
require.Equal(t, values[i], vgs[i].Value())
vgs[i].Close()
}
ro := eg.engine.NewReadOption()
defer ro.Close()
// no merge read
vgs, err = eg.engine.Read(ctx, cfs, keys, WithNoMergeRead(), WithReadOption(ro))
require.Nil(t, err)
for i := range vgs {
require.Equal(t, values[i], vgs[i].Value())
vgs[i].Close()
}
// multi get
vgs, err = eg.engine.MultiGet(ctx, col1, keys)
require.Nil(t, err)
for i := range vgs {
require.Equal(t, values[i], vgs[i].Value())
vgs[i].Close()
}
// no merge multi get
vgs, err = eg.engine.MultiGet(ctx, col1, keys, WithNoMergeRead(), WithReadOption(ro))
require.Nil(t, err)
for i := range vgs {
require.Equal(t, values[i], vgs[i].Value())
vgs[i].Close()
}
batch = eg.engine.NewWriteBatch()
batch.DeleteRange(col1, keys[0], keys[n-1])
batch.Put(col1, keys[n-1], []byte("new"))
err = eg.engine.Write(ctx, batch)
require.NoError(t, err)
batch.Clear()
for i := 0; i < n-1; i++ {
_, err = eg.engine.GetRaw(ctx, col1, keys[i])
require.Equal(t, ErrNotFound, err)
}
v, err := eg.engine.GetRaw(ctx, col1, keys[n-1], WithNoMergeRead())
require.Nil(t, err)
require.Equal(t, []byte("new"), v)
for i := range keys {
batch.Put(col1, keys[i], values[i])
}
err = eg.engine.Write(ctx, batch, WithNoMergeWrite())
require.Nil(t, err)
err = eg.engine.DeleteRange(ctx, col1, keys[0], keys[n-1], WithNoMergeWrite())
require.Nil(t, err)
vg, err := eg.engine.Get(ctx, col1, keys[len(keys)-1], WithNoMergeRead())
require.Nil(t, err)
require.Equal(t, values[len(keys)-1], vg.Value())
_, err = eg.engine.Get(ctx, col1, keys[0], WithNoMergeRead())
require.Equal(t, ErrNotFound, err)
}
func Test_ShareCache(t *testing.T) {
ctx := context.TODO()
opt1 := new(Option)
opt2 := new(Option)
cache := NewCache(ctx, RocksdbLsmKVType, 1<<20)
defer cache.Close()
opt1.Cache = cache
opt2.Cache = cache
eg1, err := newEngine(ctx, opt1)
require.NoError(t, err)
eg2, err := newEngine(ctx, opt2)
require.NoError(t, err)
defer eg1.close()
defer eg2.close()
}
func Test_ShareWriteBufferManager(t *testing.T) {
ctx := context.TODO()
opt1 := new(Option)
opt2 := new(Option)
manager := NewWriteBufferManager(ctx, RocksdbLsmKVType, 1<<20)
defer manager.Close()
opt1.WriteBufferManager = manager
opt2.WriteBufferManager = manager
eg1, err := newEngine(ctx, opt1)
require.NoError(t, err)
eg2, err := newEngine(ctx, opt2)
require.NoError(t, err)
defer eg1.close()
defer eg2.close()
}
func TestOptHelper_SetGetOpts(t *testing.T) {
ctx := context.TODO()
eg, err := newEngine(ctx, nil)
require.NoError(t, err)
defer eg.close()
oph := eg.engine.GetOptionHelper()
require.NoError(t, oph.SetMaxBackgroundJobs(10))
require.NoError(t, oph.SetMaxBackgroundCompactions(8))
require.NoError(t, oph.SetMaxSubCompactions(8))
require.NoError(t, oph.SetMaxOpenFiles(5000))
require.NoError(t, oph.SetMaxWriteBufferNumber(36))
require.NoError(t, oph.SetWriteBufferSize(256<<20))
require.NoError(t, oph.SetArenaBlockSize(64<<20))
require.NoError(t, oph.SetTargetFileSizeBase(64<<20))
require.NoError(t, oph.SetMaxBytesForLevelBase(64<<20))
require.NoError(t, oph.SetLevel0StopWritesTrigger(42))
require.NoError(t, oph.SetLevel0SlowdownWritesTrigger(42))
require.NoError(t, oph.SetSoftPendingCompactionBytesLimit(64<<20))
require.NoError(t, oph.SetHardPendingCompactionBytesLimit(128<<20))
require.NoError(t, oph.SetBlockSize(4096))
require.NoError(t, oph.SetFIFOCompactionMaxTableFileSize(128<<20))
require.NoError(t, oph.SetFIFOCompactionAllow(true))
require.Equal(t, oph.GetOption(), *eg.opt)
}
func TestInstance_NewReadOption(t *testing.T) {
ctx := context.TODO()
eg, err := newEngine(ctx, nil)
require.NoError(t, err)
defer eg.close()
ro := eg.engine.NewReadOption()
k := []byte("key1")
v := []byte("value1")
err = eg.engine.SetRaw(ctx, defaultCF, k, v)
require.NoError(t, err)
v1, err := eg.engine.Get(ctx, defaultCF, k)
require.NoError(t, err)
snap := eg.engine.NewSnapshot()
ro.SetSnapShot(snap)
v2, err := eg.engine.Get(ctx, defaultCF, k, WithReadOption(ro))
require.NoError(t, err)
require.Equal(t, v, v1.Value())
require.Equal(t, v, v2.Value())
ro.Close()
}
func TestValueGetter_Read(t *testing.T) {
ctx := context.TODO()
eg, err := newEngine(ctx, nil)
require.NoError(t, err)
defer eg.close()
k := []byte("key")
err = eg.engine.SetRaw(ctx, defaultCF, k, []byte("helloworld"))
require.NoError(t, err)
vg, err := eg.engine.Get(ctx, defaultCF, k)
require.NoError(t, err)
defer vg.Close()
b := make([]byte, vg.Size()/2)
n, err := vg.Read(b)
require.NoError(t, err)
require.Equal(t, []byte("hello"), b)
require.Equal(t, vg.Size()/2, n)
n, err = vg.Read(b)
require.NoError(t, err)
require.Equal(t, []byte("world"), b)
require.Equal(t, vg.Size()/2, n)
n, err = vg.Read(b)
require.Equal(t, io.EOF, err)
require.Equal(t, 0, n)
}
func TestInstance_NewWriteOption(t *testing.T) {
ctx := context.TODO()
eg, err := newEngine(ctx, nil)
require.NoError(t, err)
defer eg.close()
wo := eg.engine.NewWriteOption()
wo.SetSync(false)
wo.DisableWAL(true)
k := []byte("key1")
v := []byte("value1")
err = eg.engine.SetRaw(ctx, defaultCF, k, v, WithWriteOption(wo))
require.NoError(t, err)
v1, err := eg.engine.Get(ctx, defaultCF, k)
require.NoError(t, err)
require.Equal(t, v, v1.Value())
wo.Close()
}
func TestInstance_List(t *testing.T) {
ctx := context.TODO()
eg, err := newEngine(ctx, nil)
require.NoError(t, err)
defer eg.close()
err = eg.engine.SetRaw(ctx, defaultCF, []byte("key1"), []byte("value1"))
require.NoError(t, err)
err = eg.engine.SetRaw(ctx, defaultCF, []byte("word1"), []byte("w1"))
require.NoError(t, err)
err = eg.engine.SetRaw(ctx, defaultCF, []byte("key2"), []byte("value2"))
require.NoError(t, err)
err = eg.engine.SetRaw(ctx, defaultCF, []byte("check"), []byte("0"))
require.NoError(t, err)
err = eg.engine.SetRaw(ctx, defaultCF, []byte("word2"), []byte("w2"))
require.NoError(t, err)
err = eg.engine.SetRaw(ctx, defaultCF, []byte("key3"), []byte("value3"))
require.NoError(t, err)
err = eg.engine.SetRaw(ctx, defaultCF, []byte("word3"), []byte("w3"))
require.NoError(t, err)
err = eg.engine.SetRaw(ctx, defaultCF, []byte("xyz"), []byte("zyx"))
require.NoError(t, err)
err = eg.engine.SetRaw(ctx, defaultCF, []byte("key4"), []byte("value4"))
require.NoError(t, err)
ls := eg.engine.List(ctx, defaultCF, []byte("word"), nil, nil)
ls.SetFilterKey([]byte("check"))
ls.Seek([]byte("word2"))
kg, vg, err := ls.ReadNext()
require.NoError(t, err)
require.Equal(t, []byte("word2"), kg.Key())
require.Equal(t, []byte("w2"), vg.Value())
kg, vg, err = ls.ReadNext()
require.NoError(t, err)
require.Equal(t, []byte("word3"), kg.Key())
require.Equal(t, []byte("w3"), vg.Value())
ls = eg.engine.List(ctx, defaultCF, []byte("key"), nil, nil)
ls.SetFilterKey([]byte("check"))
// prefix read
i := 0
for {
i++
kg, vg, err = ls.ReadNext()
if kg == nil {
i = 0
break
}
require.NoError(t, err)
require.Equal(t, []byte("key"+strconv.Itoa(i)), kg.Key())
require.Equal(t, []byte("value"+strconv.Itoa(i)), vg.Value())
kg.Close()
vg.Close()
}
// ls.SeekToPrefix([]byte("word"))
for {
i++
kg, vg, err = ls.ReadNext()
require.NoError(t, err)
if kg == nil {
break
}
require.Equal(t, []byte("word"+strconv.Itoa(i)), kg.Key())
require.Equal(t, []byte("w"+strconv.Itoa(i)), vg.Value())
kg.Close()
vg.Close()
}
ls.Close()
// marker read
ls = eg.engine.List(ctx, defaultCF, []byte("key"), []byte("key2"), nil)
_, v, err := ls.ReadNextCopy()
require.NoError(t, err)
require.Equal(t, []byte("value2"), v)
// read last
_, vg, err = ls.ReadLast()
require.NoError(t, err)
require.Equal(t, []byte("value4"), vg.Value())
require.Equal(t, 6, vg.Size())
// nil prefix read
ls = eg.engine.List(ctx, defaultCF, nil, nil, nil)
// nil prefix read last
_, vg, err = ls.ReadLast()
require.NoError(t, err)
require.Equal(t, []byte("zyx"), vg.Value())
require.Equal(t, 3, vg.Size())
vg.Close()
ls.Close()
}
func TestInstance_Stats(t *testing.T) {
ctx := context.TODO()
eg, err := newEngine(ctx, nil)
require.NoError(t, err)
defer eg.close()
eg.engine.FlushCF(ctx, defaultCF)
stats, err := eg.engine.Stats(ctx)
require.NoError(t, err)
fmt.Println(stats.Used/(1<<10), "kb")
}
func TestEnv_SetLowPriorityBackgroundThreads(t *testing.T) {
ctx := context.TODO()
env := NewEnv(ctx, RocksdbLsmKVType)
env.SetLowPriorityBackgroundThreads(1)
env.Close()
}
func TestSstFileManager_Close(t *testing.T) {
ctx := context.TODO()
mgr := NewSstFileManager(ctx, RocksdbLsmKVType, NewEnv(ctx, RocksdbLsmKVType))
mgr.Close()
}
func TestInstance_DeleteRange(t *testing.T) {
ctx := context.TODO()
eg, err := newEngine(ctx, nil)
require.NoError(t, err)
defer eg.close()
keys := [][]byte{[]byte("/k1/a"), []byte("/k1/b"), []byte("/k1/c"), []byte("/k10"), []byte("/k1012"), []byte("/k11")}
for _, key := range keys {
err = eg.engine.SetRaw(ctx, defaultCF, key, []byte("1"))
require.NoError(t, err)
}
for _, key := range keys {
var value ValueGetter
value, err = eg.engine.Get(ctx, defaultCF, key)
require.NoError(t, err)
require.Equal(t, []byte("1"), value.Value())
value.Close()
}
start := []byte("/k1/")
end := []byte("/k1/")
end[len(end)-1]++
t.Log("start: ", start, " end: ", end)
t.Log("start: ", string(start), " end: ", string(end))
err = eg.engine.DeleteRange(ctx, defaultCF, start, end)
require.NoError(t, err)
for _, key := range [][]byte{[]byte("/k1/a"), []byte("/k1/b"), []byte("/k1/c")} {
_, err := eg.engine.Get(ctx, defaultCF, key)
t.Log(key, err)
require.Equal(t, ErrNotFound, err)
}
for _, key := range [][]byte{[]byte("/k10"), []byte("/k1012"), []byte("/k11")} {
value, err := eg.engine.Get(ctx, defaultCF, key)
require.NoError(t, err)
require.Equal(t, []byte("1"), value.Value())
value.Close()
}
}

Binary file not shown.

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.17
replace (
github.com/jacobsa/fuse => ./depends/jacobsa/fuse
github.com/spf13/cobra => ./depends/spf13/cobra
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c => github.com/Cloudstriff/gorocksdb v1.0.1
)
require (

2
go.sum
View File

@ -35,6 +35,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Cloudstriff/gorocksdb v1.0.1 h1:X/53ktTmfJEog2llrTwJZtajH6ph9Uj72gG6A2QeN9c=
github.com/Cloudstriff/gorocksdb v1.0.1/go.mod h1:hH4gbgqW1A4ZTPtDC0CxhVmOrzVPiu1AaAbt1AEdxW4=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/Netflix/go-expect v0.0.0-20190729225929-0e00d9168667/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=

View File

@ -1,4 +1,4 @@
FROM registry.access.redhat.com/ubi8/ubi-minimal@sha256:e52fc1de73dc2879516431ff1865e0fb61b1a32f57b6f914bdcddb13c62f84e6
FROM registry.access.redhat.com/ubi8/ubi-minimal:latest
USER root

View File

@ -674,6 +674,12 @@ func (db *DB) SetOptions(keys, values []string) error {
cKeys[i] = C.CString(keys[i])
cValues[i] = C.CString(values[i])
}
defer func() {
for i := range cKeys {
C.free(unsafe.Pointer(cKeys[i]))
C.free(unsafe.Pointer(cValues[i]))
}
}()
var cErr *C.char
@ -691,6 +697,43 @@ func (db *DB) SetOptions(keys, values []string) error {
return nil
}
// SetDBOptions dynamically changes options through the SetDBOptions API.
func (db *DB) SetDBOptions(keys, values []string) error {
num_keys := len(keys)
if num_keys == 0 {
return nil
}
cKeys := make([]*C.char, num_keys)
cValues := make([]*C.char, num_keys)
for i := range keys {
cKeys[i] = C.CString(keys[i])
cValues[i] = C.CString(values[i])
}
defer func() {
for i := range cKeys {
C.free(unsafe.Pointer(cKeys[i]))
C.free(unsafe.Pointer(cValues[i]))
}
}()
var cErr *C.char
C.rocksdb_set_dboptions(
db.c,
C.int(num_keys),
&cKeys[0],
&cValues[0],
&cErr,
)
if cErr != nil {
defer C.rocksdb_free(unsafe.Pointer(cErr))
return errors.New(C.GoString(cErr))
}
return nil
}
// LiveFileMetadata is a metadata which is associated with each SST file.
type LiveFileMetadata struct {
Name string
@ -752,6 +795,17 @@ func (db *DB) Flush(opts *FlushOptions) error {
return nil
}
// FlushCF triggers a manuel column family flush for the database.
func (db *DB) FlushCF(opts *FlushOptions, cf *ColumnFamilyHandle) error {
var cErr *C.char
C.rocksdb_flush_cf(db.c, opts.c, cf.c, &cErr)
if cErr != nil {
defer C.rocksdb_free(unsafe.Pointer(cErr))
return errors.New(C.GoString(cErr))
}
return nil
}
// DisableFileDeletions disables file deletions and should be used when backup the database.
func (db *DB) DisableFileDeletions() error {
var cErr *C.char

View File

@ -918,6 +918,14 @@ func (opts *Options) SetStatsDumpPeriodSec(value uint) {
C.rocksdb_options_set_stats_dump_period_sec(opts.c, C.uint(value))
}
// SetStatsPersistPeriodSec sets the stats persist period in seconds.
//
// If not zero, persist stats to LOG every stats_persist_period_sec
// Default: 300 (5 min)
func (opts *Options) SetStatsPersistPeriodSec(value uint) {
C.rocksdb_options_set_stats_persist_period_sec(opts.c, C.uint(value))
}
// SetAdviseRandomOnOpen specifies whether we will hint the underlying
// file system that the file access pattern is random, when a sst file is opened.
// Default: true
@ -1196,6 +1204,55 @@ func (opts *Options) SetOptimizeFiltersForHits(value bool) {
C.rocksdb_options_set_optimize_filters_for_hits(opts.c, C.int(btoi(value)))
}
// SetMaxSubCompactions set max_subcompactions
// This value represents the maximum number of threads that will
// concurrently perform a compaction job by breaking it into multiple,
// smaller ones that are run simultaneously.
// Default: 1 (i.e. no subcompactions)
//
// Dynamically changeable through SetDBOptions() API.
func (opts *Options) SetMaxSubCompactions(value int) {
C.rocksdb_options_set_max_subcompactions(opts.c, C.uint32_t(value))
}
// SetWriteBufferManager set write_bufffer_manager for the option
// The memory usage of memtable will report to this object. The same object
// can be passed into multiple DBs and it will track the sum of size of all
// the DBs. If the total size of all live memtables of all the DBs exceeds
// a limit, a flush will be triggered in the next DB to which the next write
// is issued.
//
// If the object is only passed to one DB, the behavior is the same as
// db_write_buffer_size. When write_buffer_manager is set, the value set will
// override db_write_buffer_size.
//
// This feature is disabled by default. Specify a non-zero value
// to enable it.
//
// Default: null
func (opts *Options) SetWriteBufferManager(w *WriteBufferManager) {
C.rocksdb_options_set_write_buffer_manager(opts.c, w.c)
}
// SetSstFileManager set sst_file_manager for the option
// Use to track SST files and control their file deletion rate.
//
// Features:
// - Throttle the deletion rate of the SST files.
// - Keep track the total size of all SST files.
// - Set a maximum allowed space limit for SST files that when reached
// the DB wont do any further flushes or compactions and will set the
// background error.
// - Can be shared between multiple dbs.
// Limitations:
// - Only track and throttle deletes of SST files in
// first db_path (db_name if db_paths is empty).
//
// Default: null
func (opts *Options) SetSstFileManager(s *SstFileManager) {
C.rocksdb_options_set_sst_file_manager(opts.c, s.c)
}
// Destroy deallocates the Options object.
func (opts *Options) Destroy() {
C.rocksdb_options_destroy(opts.c)

View File

@ -24,6 +24,11 @@ func NewNativeRateLimiter(c *C.rocksdb_ratelimiter_t) *RateLimiter {
return &RateLimiter{c}
}
// SetBytesPerSecond set rate limiter bytes per second dynamically
func (self *RateLimiter) SetBytesPerSecond(val int64) {
C.rocksdb_ratelimiter_set_bytes_per_second(self.c, C.int64_t(val))
}
// Destroy deallocates the RateLimiter object.
func (self *RateLimiter) Destroy() {
C.rocksdb_ratelimiter_destroy(self.c)

21
vendor/github.com/tecbot/gorocksdb/sst_file_manager.go generated vendored Normal file
View File

@ -0,0 +1,21 @@
package gorocksdb
// #include <stdlib.h>
// #include "rocksdb/c.h"
import "C"
// SstFileManager manager sst file deletion between multiple column families or multiple db
type SstFileManager struct {
c *C.rocksdb_sst_file_manager_t
}
// NewSstFileManager creates a SstFileManager object.
func NewSstFileManager(env *Env) *SstFileManager {
return &SstFileManager{c: C.rocksdb_sst_file_manager_create(env.c)}
}
// Destroy deallocates the SstFileManager object.
func (w *SstFileManager) Destroy() {
C.rocksdb_sst_file_manager_destory(w.c)
w.c = nil
}

View File

@ -0,0 +1,21 @@
package gorocksdb
// #include <stdlib.h>
// #include "rocksdb/c.h"
import "C"
// WriteBufferManager manager the whole write buffer between multiple column families or multiple db
type WriteBufferManager struct {
c *C.rocksdb_write_buffer_manager_t
}
// NewWriteBufferManager creates a WriteBufferManager object.
func NewWriteBufferManager(bufferSize uint64) *WriteBufferManager {
return &WriteBufferManager{c: C.rocksdb_write_buffer_manager_create(C.size_t(bufferSize))}
}
// Destroy deallocates the WriterBufferManager object.
func (w *WriteBufferManager) Destroy() {
C.rocksdb_write_buffer_manager_destory(w.c)
w.c = nil
}

View File

@ -1,109 +0,0 @@
// Copyright 2019 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package confchange
import (
"errors"
"fmt"
"strconv"
"strings"
"testing"
"github.com/cockroachdb/datadriven"
pb "go.etcd.io/etcd/raft/v3/raftpb"
"go.etcd.io/etcd/raft/v3/tracker"
)
func TestConfChangeDataDriven(t *testing.T) {
datadriven.Walk(t, "testdata", func(t *testing.T, path string) {
tr := tracker.MakeProgressTracker(10)
c := Changer{
Tracker: tr,
LastIndex: 0, // incremented in this test with each cmd
}
// The test files use the commands
// - simple: run a simple conf change (i.e. no joint consensus),
// - enter-joint: enter a joint config, and
// - leave-joint: leave a joint config.
// The first two take a list of config changes, which have the following
// syntax:
// - vn: make n a voter,
// - ln: make n a learner,
// - rn: remove n, and
// - un: update n.
datadriven.RunTest(t, path, func(t *testing.T, d *datadriven.TestData) string {
defer func() {
c.LastIndex++
}()
var ccs []pb.ConfChangeSingle
toks := strings.Split(strings.TrimSpace(d.Input), " ")
if toks[0] == "" {
toks = nil
}
for _, tok := range toks {
if len(tok) < 2 {
return fmt.Sprintf("unknown token %s", tok)
}
var cc pb.ConfChangeSingle
switch tok[0] {
case 'v':
cc.Type = pb.ConfChangeAddNode
case 'l':
cc.Type = pb.ConfChangeAddLearnerNode
case 'r':
cc.Type = pb.ConfChangeRemoveNode
case 'u':
cc.Type = pb.ConfChangeUpdateNode
default:
return fmt.Sprintf("unknown input: %s", tok)
}
id, err := strconv.ParseUint(tok[1:], 10, 64)
if err != nil {
return err.Error()
}
cc.NodeID = id
ccs = append(ccs, cc)
}
var cfg tracker.Config
var prs tracker.ProgressMap
var err error
switch d.Cmd {
case "simple":
cfg, prs, err = c.Simple(ccs...)
case "enter-joint":
var autoLeave bool
if len(d.CmdArgs) > 0 {
d.ScanArgs(t, "autoleave", &autoLeave)
}
cfg, prs, err = c.EnterJoint(autoLeave, ccs...)
case "leave-joint":
if len(ccs) > 0 {
err = errors.New("this command takes no input")
} else {
cfg, prs, err = c.LeaveJoint()
}
default:
return "unknown command"
}
if err != nil {
return err.Error() + "\n"
}
c.Tracker.Config, c.Tracker.Progress = cfg, prs
return fmt.Sprintf("%s\n%s", c.Tracker.Config, c.Tracker.Progress)
})
})
}

View File

@ -1,191 +0,0 @@
// Copyright 2019 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package confchange
import (
"fmt"
"math/rand"
"reflect"
"testing"
"testing/quick"
pb "go.etcd.io/etcd/raft/v3/raftpb"
"go.etcd.io/etcd/raft/v3/tracker"
)
// TestConfChangeQuick uses quickcheck to verify that simple and joint config
// changes arrive at the same result.
func TestConfChangeQuick(t *testing.T) {
cfg := &quick.Config{
MaxCount: 1000,
}
// Log the first couple of runs to give some indication of things working
// as intended.
const infoCount = 5
runWithJoint := func(c *Changer, ccs []pb.ConfChangeSingle) error {
cfg, prs, err := c.EnterJoint(false /* autoLeave */, ccs...)
if err != nil {
return err
}
// Also do this with autoLeave on, just to check that we'd get the same
// result.
cfg2a, prs2a, err := c.EnterJoint(true /* autoLeave */, ccs...)
if err != nil {
return err
}
cfg2a.AutoLeave = false
if !reflect.DeepEqual(cfg, cfg2a) || !reflect.DeepEqual(prs, prs2a) {
return fmt.Errorf("cfg: %+v\ncfg2a: %+v\nprs: %+v\nprs2a: %+v",
cfg, cfg2a, prs, prs2a)
}
c.Tracker.Config = cfg
c.Tracker.Progress = prs
cfg2b, prs2b, err := c.LeaveJoint()
if err != nil {
return err
}
// Reset back to the main branch with autoLeave=false.
c.Tracker.Config = cfg
c.Tracker.Progress = prs
cfg, prs, err = c.LeaveJoint()
if err != nil {
return err
}
if !reflect.DeepEqual(cfg, cfg2b) || !reflect.DeepEqual(prs, prs2b) {
return fmt.Errorf("cfg: %+v\ncfg2b: %+v\nprs: %+v\nprs2b: %+v",
cfg, cfg2b, prs, prs2b)
}
c.Tracker.Config = cfg
c.Tracker.Progress = prs
return nil
}
runWithSimple := func(c *Changer, ccs []pb.ConfChangeSingle) error {
for _, cc := range ccs {
cfg, prs, err := c.Simple(cc)
if err != nil {
return err
}
c.Tracker.Config, c.Tracker.Progress = cfg, prs
}
return nil
}
type testFunc func(*Changer, []pb.ConfChangeSingle) error
wrapper := func(invoke testFunc) func(setup initialChanges, ccs confChanges) (*Changer, error) {
return func(setup initialChanges, ccs confChanges) (*Changer, error) {
tr := tracker.MakeProgressTracker(10)
c := &Changer{
Tracker: tr,
LastIndex: 10,
}
if err := runWithSimple(c, setup); err != nil {
return nil, err
}
err := invoke(c, ccs)
return c, err
}
}
var n int
f1 := func(setup initialChanges, ccs confChanges) *Changer {
c, err := wrapper(runWithSimple)(setup, ccs)
if err != nil {
t.Fatal(err)
}
if n < infoCount {
t.Log("initial setup:", Describe(setup...))
t.Log("changes:", Describe(ccs...))
t.Log(c.Tracker.Config)
t.Log(c.Tracker.Progress)
}
n++
return c
}
f2 := func(setup initialChanges, ccs confChanges) *Changer {
c, err := wrapper(runWithJoint)(setup, ccs)
if err != nil {
t.Fatal(err)
}
return c
}
err := quick.CheckEqual(f1, f2, cfg)
if err == nil {
return
}
cErr, ok := err.(*quick.CheckEqualError)
if !ok {
t.Fatal(err)
}
t.Error("setup:", Describe(cErr.In[0].([]pb.ConfChangeSingle)...))
t.Error("ccs:", Describe(cErr.In[1].([]pb.ConfChangeSingle)...))
t.Errorf("out1: %+v\nout2: %+v", cErr.Out1, cErr.Out2)
}
type confChangeTyp pb.ConfChangeType
func (confChangeTyp) Generate(rand *rand.Rand, _ int) reflect.Value {
return reflect.ValueOf(confChangeTyp(rand.Intn(4)))
}
type confChanges []pb.ConfChangeSingle
func genCC(num func() int, id func() uint64, typ func() pb.ConfChangeType) []pb.ConfChangeSingle {
var ccs []pb.ConfChangeSingle
n := num()
for i := 0; i < n; i++ {
ccs = append(ccs, pb.ConfChangeSingle{Type: typ(), NodeID: id()})
}
return ccs
}
func (confChanges) Generate(rand *rand.Rand, _ int) reflect.Value {
num := func() int {
return 1 + rand.Intn(9)
}
id := func() uint64 {
// Note that num() >= 1, so we're never returning 1 from this method,
// meaning that we'll never touch NodeID one, which is special to avoid
// voterless configs altogether in this test.
return 1 + uint64(num())
}
typ := func() pb.ConfChangeType {
return pb.ConfChangeType(rand.Intn(len(pb.ConfChangeType_name)))
}
return reflect.ValueOf(genCC(num, id, typ))
}
type initialChanges []pb.ConfChangeSingle
func (initialChanges) Generate(rand *rand.Rand, _ int) reflect.Value {
num := func() int {
return 1 + rand.Intn(5)
}
id := func() uint64 { return uint64(num()) }
typ := func() pb.ConfChangeType {
return pb.ConfChangeAddNode
}
// NodeID one is special - it's in the initial config and will be a voter
// always (this is to avoid uninteresting edge cases where the simple conf
// changes can't easily make progress).
ccs := append([]pb.ConfChangeSingle{{Type: pb.ConfChangeAddNode, NodeID: 1}}, genCC(num, id, typ)...)
return reflect.ValueOf(ccs)
}

View File

@ -1,142 +0,0 @@
// Copyright 2019 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package confchange
import (
"math/rand"
"reflect"
"sort"
"testing"
"testing/quick"
pb "go.etcd.io/etcd/raft/v3/raftpb"
"go.etcd.io/etcd/raft/v3/tracker"
)
type rndConfChange pb.ConfState
// Generate creates a random (valid) ConfState for use with quickcheck.
func (rndConfChange) Generate(rand *rand.Rand, _ int) reflect.Value {
conv := func(sl []int) []uint64 {
// We want IDs but the incoming slice is zero-indexed, so add one to
// each.
out := make([]uint64, len(sl))
for i := range sl {
out[i] = uint64(sl[i] + 1)
}
return out
}
var cs pb.ConfState
// NB: never generate the empty ConfState, that one should be unit tested.
nVoters := 1 + rand.Intn(5)
nLearners := rand.Intn(5)
// The number of voters that are in the outgoing config but not in the
// incoming one. (We'll additionally retain a random number of the
// incoming voters below).
nRemovedVoters := rand.Intn(3)
// Voters, learners, and removed voters must not overlap. A "removed voter"
// is one that we have in the outgoing config but not the incoming one.
ids := conv(rand.Perm(2 * (nVoters + nLearners + nRemovedVoters)))
cs.Voters = ids[:nVoters]
ids = ids[nVoters:]
if nLearners > 0 {
cs.Learners = ids[:nLearners]
ids = ids[nLearners:]
}
// Roll the dice on how many of the incoming voters we decide were also
// previously voters.
//
// NB: this code avoids creating non-nil empty slices (here and below).
nOutgoingRetainedVoters := rand.Intn(nVoters + 1)
if nOutgoingRetainedVoters > 0 || nRemovedVoters > 0 {
cs.VotersOutgoing = append([]uint64(nil), cs.Voters[:nOutgoingRetainedVoters]...)
cs.VotersOutgoing = append(cs.VotersOutgoing, ids[:nRemovedVoters]...)
}
// Only outgoing voters that are not also incoming voters can be in
// LearnersNext (they represent demotions).
if nRemovedVoters > 0 {
if nLearnersNext := rand.Intn(nRemovedVoters + 1); nLearnersNext > 0 {
cs.LearnersNext = ids[:nLearnersNext]
}
}
cs.AutoLeave = len(cs.VotersOutgoing) > 0 && rand.Intn(2) == 1
return reflect.ValueOf(rndConfChange(cs))
}
func TestRestore(t *testing.T) {
cfg := quick.Config{MaxCount: 1000}
f := func(cs pb.ConfState) bool {
chg := Changer{
Tracker: tracker.MakeProgressTracker(20),
LastIndex: 10,
}
cfg, prs, err := Restore(chg, cs)
if err != nil {
t.Error(err)
return false
}
chg.Tracker.Config = cfg
chg.Tracker.Progress = prs
for _, sl := range [][]uint64{
cs.Voters,
cs.Learners,
cs.VotersOutgoing,
cs.LearnersNext,
} {
sort.Slice(sl, func(i, j int) bool { return sl[i] < sl[j] })
}
cs2 := chg.Tracker.ConfState()
// NB: cs.Equivalent does the same "sorting" dance internally, but let's
// test it a bit here instead of relying on it.
if reflect.DeepEqual(cs, cs2) && cs.Equivalent(cs2) == nil && cs2.Equivalent(cs) == nil {
return true // success
}
t.Errorf(`
before: %+#v
after: %+#v`, cs, cs2)
return false
}
ids := func(sl ...uint64) []uint64 {
return sl
}
// Unit tests.
for _, cs := range []pb.ConfState{
{},
{Voters: ids(1, 2, 3)},
{Voters: ids(1, 2, 3), Learners: ids(4, 5, 6)},
{Voters: ids(1, 2, 3), Learners: ids(5), VotersOutgoing: ids(1, 2, 4, 6), LearnersNext: ids(4)},
} {
if !f(cs) {
t.FailNow() // f() already logged a nice t.Error()
}
}
if err := quick.Check(func(cs rndConfChange) bool {
return f(pb.ConfState(cs))
}, &cfg); err != nil {
t.Error(err)
}
}

View File

@ -1,29 +0,0 @@
# Test the autoleave argument to EnterJoint. It defaults to false in the
# datadriven tests. The flag has no associated semantics in this package,
# it is simply passed through.
simple
v1
----
voters=(1)
1: StateProbe match=0 next=0
# Autoleave is reflected in the config.
enter-joint autoleave=true
v2 v3
----
voters=(1 2 3)&&(1) autoleave
1: StateProbe match=0 next=0
2: StateProbe match=0 next=1
3: StateProbe match=0 next=1
# Can't enter-joint twice, even if autoleave changes.
enter-joint autoleave=false
----
config is already joint
leave-joint
----
voters=(1 2 3)
1: StateProbe match=0 next=0
2: StateProbe match=0 next=1
3: StateProbe match=0 next=1

View File

@ -1,23 +0,0 @@
# Verify that operations upon entering the joint state are idempotent, i.e.
# removing an absent node is fine, etc.
simple
v1
----
voters=(1)
1: StateProbe match=0 next=0
enter-joint
r1 r2 r9 v2 v3 v4 v2 v3 v4 l2 l2 r4 r4 l1 l1
----
voters=(3)&&(1) learners=(2) learners_next=(1)
1: StateProbe match=0 next=0
2: StateProbe match=0 next=1 learner
3: StateProbe match=0 next=1
leave-joint
----
voters=(3) learners=(1 2)
1: StateProbe match=0 next=0 learner
2: StateProbe match=0 next=1 learner
3: StateProbe match=0 next=1

View File

@ -1,24 +0,0 @@
# Verify that when a voter is demoted in a joint config, it will show up in
# learners_next until the joint config is left, and only then will the progress
# turn into that of a learner, without resetting the progress. Note that this
# last fact is verified by `next`, which can tell us which "round" the progress
# was originally created in.
simple
v1
----
voters=(1)
1: StateProbe match=0 next=0
enter-joint
v2 l1
----
voters=(2)&&(1) learners_next=(1)
1: StateProbe match=0 next=0
2: StateProbe match=0 next=1
leave-joint
----
voters=(2) learners=(1)
1: StateProbe match=0 next=0 learner
2: StateProbe match=0 next=1

View File

@ -1,81 +0,0 @@
leave-joint
----
can't leave a non-joint config
enter-joint
----
can't make a zero-voter config joint
enter-joint
v1
----
can't make a zero-voter config joint
simple
v1
----
voters=(1)
1: StateProbe match=0 next=3
leave-joint
----
can't leave a non-joint config
# Can enter into joint config.
enter-joint
----
voters=(1)&&(1)
1: StateProbe match=0 next=3
enter-joint
----
config is already joint
leave-joint
----
voters=(1)
1: StateProbe match=0 next=3
leave-joint
----
can't leave a non-joint config
# Can enter again, this time with some ops.
enter-joint
r1 v2 v3 l4
----
voters=(2 3)&&(1) learners=(4)
1: StateProbe match=0 next=3
2: StateProbe match=0 next=9
3: StateProbe match=0 next=9
4: StateProbe match=0 next=9 learner
enter-joint
----
config is already joint
enter-joint
v12
----
config is already joint
simple
l15
----
can't apply simple config change in joint config
leave-joint
----
voters=(2 3) learners=(4)
2: StateProbe match=0 next=9
3: StateProbe match=0 next=9
4: StateProbe match=0 next=9 learner
simple
l9
----
voters=(2 3) learners=(4 9)
2: StateProbe match=0 next=9
3: StateProbe match=0 next=9
4: StateProbe match=0 next=9 learner
9: StateProbe match=0 next=14 learner

View File

@ -1,69 +0,0 @@
simple
v1
----
voters=(1)
1: StateProbe match=0 next=0
simple
v1
----
voters=(1)
1: StateProbe match=0 next=0
simple
v2
----
voters=(1 2)
1: StateProbe match=0 next=0
2: StateProbe match=0 next=2
simple
l1
----
voters=(2) learners=(1)
1: StateProbe match=0 next=0 learner
2: StateProbe match=0 next=2
simple
l1
----
voters=(2) learners=(1)
1: StateProbe match=0 next=0 learner
2: StateProbe match=0 next=2
simple
r1
----
voters=(2)
2: StateProbe match=0 next=2
simple
r1
----
voters=(2)
2: StateProbe match=0 next=2
simple
v3
----
voters=(2 3)
2: StateProbe match=0 next=2
3: StateProbe match=0 next=7
simple
r3
----
voters=(2)
2: StateProbe match=0 next=2
simple
r3
----
voters=(2)
2: StateProbe match=0 next=2
simple
r4
----
voters=(2)
2: StateProbe match=0 next=2

View File

@ -1,60 +0,0 @@
# Set up three voters for this test.
simple
v1
----
voters=(1)
1: StateProbe match=0 next=0
simple
v2
----
voters=(1 2)
1: StateProbe match=0 next=0
2: StateProbe match=0 next=1
simple
v3
----
voters=(1 2 3)
1: StateProbe match=0 next=0
2: StateProbe match=0 next=1
3: StateProbe match=0 next=2
# Can atomically demote and promote without a hitch.
# This is pointless, but possible.
simple
l1 v1
----
voters=(1 2 3)
1: StateProbe match=0 next=0
2: StateProbe match=0 next=1
3: StateProbe match=0 next=2
# Can demote a voter.
simple
l2
----
voters=(1 3) learners=(2)
1: StateProbe match=0 next=0
2: StateProbe match=0 next=1 learner
3: StateProbe match=0 next=2
# Can atomically promote and demote the same voter.
# This is pointless, but possible.
simple
v2 l2
----
voters=(1 3) learners=(2)
1: StateProbe match=0 next=0
2: StateProbe match=0 next=1 learner
3: StateProbe match=0 next=2
# Can promote a voter.
simple
v2
----
voters=(1 2 3)
1: StateProbe match=0 next=0
2: StateProbe match=0 next=1
3: StateProbe match=0 next=2

View File

@ -1,64 +0,0 @@
simple
l1
----
removed all voters
simple
v1
----
voters=(1)
1: StateProbe match=0 next=1
simple
v2 l3
----
voters=(1 2) learners=(3)
1: StateProbe match=0 next=1
2: StateProbe match=0 next=2
3: StateProbe match=0 next=2 learner
simple
r1 v5
----
more than one voter changed without entering joint config
simple
r1 r2
----
removed all voters
simple
v3 v4
----
more than one voter changed without entering joint config
simple
l1 v5
----
more than one voter changed without entering joint config
simple
l1 l2
----
removed all voters
simple
l2 l3 l4 l5
----
voters=(1) learners=(2 3 4 5)
1: StateProbe match=0 next=1
2: StateProbe match=0 next=2 learner
3: StateProbe match=0 next=2 learner
4: StateProbe match=0 next=8 learner
5: StateProbe match=0 next=8 learner
simple
r1
----
removed all voters
simple
r2 r3 r4 r5
----
voters=(1)
1: StateProbe match=0 next=1

View File

@ -1,23 +0,0 @@
# Nobody cares about ConfChangeUpdateNode, but at least use it once. It is used
# by etcd as a convenient way to pass a blob through their conf change machinery
# that updates information tracked outside of raft.
simple
v1
----
voters=(1)
1: StateProbe match=0 next=0
simple
v2 u1
----
voters=(1 2)
1: StateProbe match=0 next=0
2: StateProbe match=0 next=1
simple
u1 u2 u3 u1 u2 u3
----
voters=(1 2)
1: StateProbe match=0 next=0
2: StateProbe match=0 next=1

View File

@ -1,6 +0,0 @@
# NodeID zero is ignored.
simple
v1 r0 v0 l0
----
voters=(1)
1: StateProbe match=0 next=0

View File

@ -1,65 +0,0 @@
// Copyright 2015 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package raft
import (
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"strings"
)
func diffu(a, b string) string {
if a == b {
return ""
}
aname, bname := mustTemp("base", a), mustTemp("other", b)
defer os.Remove(aname)
defer os.Remove(bname)
cmd := exec.Command("diff", "-u", aname, bname)
buf, err := cmd.CombinedOutput()
if err != nil {
if _, ok := err.(*exec.ExitError); ok {
// do nothing
return string(buf)
}
panic(err)
}
return string(buf)
}
func mustTemp(pre, body string) string {
f, err := ioutil.TempFile("", pre)
if err != nil {
panic(err)
}
_, err = io.Copy(f, strings.NewReader(body))
if err != nil {
panic(err)
}
f.Close()
return f.Name()
}
func ltoa(l *raftLog) string {
s := fmt.Sprintf("committed: %d\n", l.committed)
s += fmt.Sprintf("applied: %d\n", l.applied)
for i, e := range l.allEntries() {
s += fmt.Sprintf("#%d: %+v\n", i, e)
}
return s
}

View File

@ -1,47 +0,0 @@
// Copyright 2015 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package raft
import (
pb "go.etcd.io/etcd/raft/v3/raftpb"
)
func applyToStore(ents []pb.Entry) {}
func sendMessages(msgs []pb.Message) {}
func saveStateToDisk(st pb.HardState) {}
func saveToDisk(ents []pb.Entry) {}
func ExampleNode() {
c := &Config{}
n := StartNode(c, nil)
defer n.Stop()
// stuff to n happens in other goroutines
// the last known state
var prev pb.HardState
for {
// Ready blocks until there is new state ready.
rd := <-n.Ready()
if !isHardStateEqual(prev, rd.HardState) {
saveStateToDisk(rd.HardState)
prev = rd.HardState
}
saveToDisk(rd.Entries)
go applyToStore(rd.CommittedEntries)
sendMessages(rd.Messages)
}
}

View File

@ -1,34 +0,0 @@
// Copyright 2019 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package raft_test
import (
"testing"
"github.com/cockroachdb/datadriven"
"go.etcd.io/etcd/raft/v3/rafttest"
)
func TestInteraction(t *testing.T) {
// NB: if this test fails, run `go test ./raft -rewrite` and inspect the
// diff. Only commit the changes if you understand what caused them and if
// they are desired.
datadriven.Walk(t, "testdata", func(t *testing.T, path string) {
env := rafttest.NewInteractionEnv(nil)
datadriven.RunTest(t, path, func(t *testing.T, d *datadriven.TestData) string {
return env.Handle(t, *d)
})
})
}

View File

@ -1,821 +0,0 @@
// Copyright 2015 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package raft
import (
"reflect"
"testing"
pb "go.etcd.io/etcd/raft/v3/raftpb"
)
func TestFindConflict(t *testing.T) {
previousEnts := []pb.Entry{{Index: 1, Term: 1}, {Index: 2, Term: 2}, {Index: 3, Term: 3}}
tests := []struct {
ents []pb.Entry
wconflict uint64
}{
// no conflict, empty ent
{[]pb.Entry{}, 0},
// no conflict
{[]pb.Entry{{Index: 1, Term: 1}, {Index: 2, Term: 2}, {Index: 3, Term: 3}}, 0},
{[]pb.Entry{{Index: 2, Term: 2}, {Index: 3, Term: 3}}, 0},
{[]pb.Entry{{Index: 3, Term: 3}}, 0},
// no conflict, but has new entries
{[]pb.Entry{{Index: 1, Term: 1}, {Index: 2, Term: 2}, {Index: 3, Term: 3}, {Index: 4, Term: 4}, {Index: 5, Term: 4}}, 4},
{[]pb.Entry{{Index: 2, Term: 2}, {Index: 3, Term: 3}, {Index: 4, Term: 4}, {Index: 5, Term: 4}}, 4},
{[]pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 4}, {Index: 5, Term: 4}}, 4},
{[]pb.Entry{{Index: 4, Term: 4}, {Index: 5, Term: 4}}, 4},
// conflicts with existing entries
{[]pb.Entry{{Index: 1, Term: 4}, {Index: 2, Term: 4}}, 1},
{[]pb.Entry{{Index: 2, Term: 1}, {Index: 3, Term: 4}, {Index: 4, Term: 4}}, 2},
{[]pb.Entry{{Index: 3, Term: 1}, {Index: 4, Term: 2}, {Index: 5, Term: 4}, {Index: 6, Term: 4}}, 3},
}
for i, tt := range tests {
raftLog := newLog(NewMemoryStorage(), raftLogger)
raftLog.append(previousEnts...)
gconflict := raftLog.findConflict(tt.ents)
if gconflict != tt.wconflict {
t.Errorf("#%d: conflict = %d, want %d", i, gconflict, tt.wconflict)
}
}
}
func TestIsUpToDate(t *testing.T) {
previousEnts := []pb.Entry{{Index: 1, Term: 1}, {Index: 2, Term: 2}, {Index: 3, Term: 3}}
raftLog := newLog(NewMemoryStorage(), raftLogger)
raftLog.append(previousEnts...)
tests := []struct {
lastIndex uint64
term uint64
wUpToDate bool
}{
// greater term, ignore lastIndex
{raftLog.lastIndex() - 1, 4, true},
{raftLog.lastIndex(), 4, true},
{raftLog.lastIndex() + 1, 4, true},
// smaller term, ignore lastIndex
{raftLog.lastIndex() - 1, 2, false},
{raftLog.lastIndex(), 2, false},
{raftLog.lastIndex() + 1, 2, false},
// equal term, equal or lager lastIndex wins
{raftLog.lastIndex() - 1, 3, false},
{raftLog.lastIndex(), 3, true},
{raftLog.lastIndex() + 1, 3, true},
}
for i, tt := range tests {
gUpToDate := raftLog.isUpToDate(tt.lastIndex, tt.term)
if gUpToDate != tt.wUpToDate {
t.Errorf("#%d: uptodate = %v, want %v", i, gUpToDate, tt.wUpToDate)
}
}
}
func TestAppend(t *testing.T) {
previousEnts := []pb.Entry{{Index: 1, Term: 1}, {Index: 2, Term: 2}}
tests := []struct {
ents []pb.Entry
windex uint64
wents []pb.Entry
wunstable uint64
}{
{
[]pb.Entry{},
2,
[]pb.Entry{{Index: 1, Term: 1}, {Index: 2, Term: 2}},
3,
},
{
[]pb.Entry{{Index: 3, Term: 2}},
3,
[]pb.Entry{{Index: 1, Term: 1}, {Index: 2, Term: 2}, {Index: 3, Term: 2}},
3,
},
// conflicts with index 1
{
[]pb.Entry{{Index: 1, Term: 2}},
1,
[]pb.Entry{{Index: 1, Term: 2}},
1,
},
// conflicts with index 2
{
[]pb.Entry{{Index: 2, Term: 3}, {Index: 3, Term: 3}},
3,
[]pb.Entry{{Index: 1, Term: 1}, {Index: 2, Term: 3}, {Index: 3, Term: 3}},
2,
},
}
for i, tt := range tests {
storage := NewMemoryStorage()
storage.Append(previousEnts)
raftLog := newLog(storage, raftLogger)
index := raftLog.append(tt.ents...)
if index != tt.windex {
t.Errorf("#%d: lastIndex = %d, want %d", i, index, tt.windex)
}
g, err := raftLog.entries(1, noLimit)
if err != nil {
t.Fatalf("#%d: unexpected error %v", i, err)
}
if !reflect.DeepEqual(g, tt.wents) {
t.Errorf("#%d: logEnts = %+v, want %+v", i, g, tt.wents)
}
if goff := raftLog.unstable.offset; goff != tt.wunstable {
t.Errorf("#%d: unstable = %d, want %d", i, goff, tt.wunstable)
}
}
}
// TestLogMaybeAppend ensures:
// If the given (index, term) matches with the existing log:
// 1. If an existing entry conflicts with a new one (same index
// but different terms), delete the existing entry and all that
// follow it
// 2. Append any new entries not already in the log
//
// If the given (index, term) does not match with the existing log:
//
// return false
func TestLogMaybeAppend(t *testing.T) {
previousEnts := []pb.Entry{{Index: 1, Term: 1}, {Index: 2, Term: 2}, {Index: 3, Term: 3}}
lastindex := uint64(3)
lastterm := uint64(3)
commit := uint64(1)
tests := []struct {
logTerm uint64
index uint64
committed uint64
ents []pb.Entry
wlasti uint64
wappend bool
wcommit uint64
wpanic bool
}{
// not match: term is different
{
lastterm - 1, lastindex, lastindex, []pb.Entry{{Index: lastindex + 1, Term: 4}},
0, false, commit, false,
},
// not match: index out of bound
{
lastterm, lastindex + 1, lastindex, []pb.Entry{{Index: lastindex + 2, Term: 4}},
0, false, commit, false,
},
// match with the last existing entry
{
lastterm, lastindex, lastindex, nil,
lastindex, true, lastindex, false,
},
{
lastterm, lastindex, lastindex + 1, nil,
lastindex, true, lastindex, false, // do not increase commit higher than lastnewi
},
{
lastterm, lastindex, lastindex - 1, nil,
lastindex, true, lastindex - 1, false, // commit up to the commit in the message
},
{
lastterm, lastindex, 0, nil,
lastindex, true, commit, false, // commit do not decrease
},
{
0, 0, lastindex, nil,
0, true, commit, false, // commit do not decrease
},
{
lastterm, lastindex, lastindex, []pb.Entry{{Index: lastindex + 1, Term: 4}},
lastindex + 1, true, lastindex, false,
},
{
lastterm, lastindex, lastindex + 1, []pb.Entry{{Index: lastindex + 1, Term: 4}},
lastindex + 1, true, lastindex + 1, false,
},
{
lastterm, lastindex, lastindex + 2, []pb.Entry{{Index: lastindex + 1, Term: 4}},
lastindex + 1, true, lastindex + 1, false, // do not increase commit higher than lastnewi
},
{
lastterm, lastindex, lastindex + 2, []pb.Entry{{Index: lastindex + 1, Term: 4}, {Index: lastindex + 2, Term: 4}},
lastindex + 2, true, lastindex + 2, false,
},
// match with the the entry in the middle
{
lastterm - 1, lastindex - 1, lastindex, []pb.Entry{{Index: lastindex, Term: 4}},
lastindex, true, lastindex, false,
},
{
lastterm - 2, lastindex - 2, lastindex, []pb.Entry{{Index: lastindex - 1, Term: 4}},
lastindex - 1, true, lastindex - 1, false,
},
{
lastterm - 3, lastindex - 3, lastindex, []pb.Entry{{Index: lastindex - 2, Term: 4}},
lastindex - 2, true, lastindex - 2, true, // conflict with existing committed entry
},
{
lastterm - 2, lastindex - 2, lastindex, []pb.Entry{{Index: lastindex - 1, Term: 4}, {Index: lastindex, Term: 4}},
lastindex, true, lastindex, false,
},
}
for i, tt := range tests {
raftLog := newLog(NewMemoryStorage(), raftLogger)
raftLog.append(previousEnts...)
raftLog.committed = commit
func() {
defer func() {
if r := recover(); r != nil {
if !tt.wpanic {
t.Errorf("%d: panic = %v, want %v", i, true, tt.wpanic)
}
}
}()
glasti, gappend := raftLog.maybeAppend(tt.index, tt.logTerm, tt.committed, tt.ents...)
gcommit := raftLog.committed
if glasti != tt.wlasti {
t.Errorf("#%d: lastindex = %d, want %d", i, glasti, tt.wlasti)
}
if gappend != tt.wappend {
t.Errorf("#%d: append = %v, want %v", i, gappend, tt.wappend)
}
if gcommit != tt.wcommit {
t.Errorf("#%d: committed = %d, want %d", i, gcommit, tt.wcommit)
}
if gappend && len(tt.ents) != 0 {
gents, err := raftLog.slice(raftLog.lastIndex()-uint64(len(tt.ents))+1, raftLog.lastIndex()+1, noLimit)
if err != nil {
t.Fatalf("unexpected error %v", err)
}
if !reflect.DeepEqual(tt.ents, gents) {
t.Errorf("#%d: appended entries = %v, want %v", i, gents, tt.ents)
}
}
}()
}
}
// TestCompactionSideEffects ensures that all the log related functionality works correctly after
// a compaction.
func TestCompactionSideEffects(t *testing.T) {
var i uint64
// Populate the log with 1000 entries; 750 in stable storage and 250 in unstable.
lastIndex := uint64(1000)
unstableIndex := uint64(750)
lastTerm := lastIndex
storage := NewMemoryStorage()
for i = 1; i <= unstableIndex; i++ {
storage.Append([]pb.Entry{{Term: i, Index: i}})
}
raftLog := newLog(storage, raftLogger)
for i = unstableIndex; i < lastIndex; i++ {
raftLog.append(pb.Entry{Term: i + 1, Index: i + 1})
}
ok := raftLog.maybeCommit(lastIndex, lastTerm)
if !ok {
t.Fatalf("maybeCommit returned false")
}
raftLog.appliedTo(raftLog.committed)
offset := uint64(500)
storage.Compact(offset)
if raftLog.lastIndex() != lastIndex {
t.Errorf("lastIndex = %d, want %d", raftLog.lastIndex(), lastIndex)
}
for j := offset; j <= raftLog.lastIndex(); j++ {
if mustTerm(raftLog.term(j)) != j {
t.Errorf("term(%d) = %d, want %d", j, mustTerm(raftLog.term(j)), j)
}
}
for j := offset; j <= raftLog.lastIndex(); j++ {
if !raftLog.matchTerm(j, j) {
t.Errorf("matchTerm(%d) = false, want true", j)
}
}
unstableEnts := raftLog.unstableEntries()
if g := len(unstableEnts); g != 250 {
t.Errorf("len(unstableEntries) = %d, want = %d", g, 250)
}
if unstableEnts[0].Index != 751 {
t.Errorf("Index = %d, want = %d", unstableEnts[0].Index, 751)
}
prev := raftLog.lastIndex()
raftLog.append(pb.Entry{Index: raftLog.lastIndex() + 1, Term: raftLog.lastIndex() + 1})
if raftLog.lastIndex() != prev+1 {
t.Errorf("lastIndex = %d, want = %d", raftLog.lastIndex(), prev+1)
}
ents, err := raftLog.entries(raftLog.lastIndex(), noLimit)
if err != nil {
t.Fatalf("unexpected error %v", err)
}
if len(ents) != 1 {
t.Errorf("len(entries) = %d, want = %d", len(ents), 1)
}
}
func TestHasNextEnts(t *testing.T) {
snap := pb.Snapshot{
Metadata: pb.SnapshotMetadata{Term: 1, Index: 3},
}
ents := []pb.Entry{
{Term: 1, Index: 4},
{Term: 1, Index: 5},
{Term: 1, Index: 6},
}
tests := []struct {
applied uint64
hasNext bool
}{
{0, true},
{3, true},
{4, true},
{5, false},
}
for i, tt := range tests {
storage := NewMemoryStorage()
storage.ApplySnapshot(snap)
raftLog := newLog(storage, raftLogger)
raftLog.append(ents...)
raftLog.maybeCommit(5, 1)
raftLog.appliedTo(tt.applied)
hasNext := raftLog.hasNextEnts()
if hasNext != tt.hasNext {
t.Errorf("#%d: hasNext = %v, want %v", i, hasNext, tt.hasNext)
}
}
}
func TestNextEnts(t *testing.T) {
snap := pb.Snapshot{
Metadata: pb.SnapshotMetadata{Term: 1, Index: 3},
}
ents := []pb.Entry{
{Term: 1, Index: 4},
{Term: 1, Index: 5},
{Term: 1, Index: 6},
}
tests := []struct {
applied uint64
wents []pb.Entry
}{
{0, ents[:2]},
{3, ents[:2]},
{4, ents[1:2]},
{5, nil},
}
for i, tt := range tests {
storage := NewMemoryStorage()
storage.ApplySnapshot(snap)
raftLog := newLog(storage, raftLogger)
raftLog.append(ents...)
raftLog.maybeCommit(5, 1)
raftLog.appliedTo(tt.applied)
nents := raftLog.nextEnts()
if !reflect.DeepEqual(nents, tt.wents) {
t.Errorf("#%d: nents = %+v, want %+v", i, nents, tt.wents)
}
}
}
// TestUnstableEnts ensures unstableEntries returns the unstable part of the
// entries correctly.
func TestUnstableEnts(t *testing.T) {
previousEnts := []pb.Entry{{Term: 1, Index: 1}, {Term: 2, Index: 2}}
tests := []struct {
unstable uint64
wents []pb.Entry
}{
{3, nil},
{1, previousEnts},
}
for i, tt := range tests {
// append stable entries to storage
storage := NewMemoryStorage()
storage.Append(previousEnts[:tt.unstable-1])
// append unstable entries to raftlog
raftLog := newLog(storage, raftLogger)
raftLog.append(previousEnts[tt.unstable-1:]...)
ents := raftLog.unstableEntries()
if l := len(ents); l > 0 {
raftLog.stableTo(ents[l-1].Index, ents[l-1].Term)
}
if !reflect.DeepEqual(ents, tt.wents) {
t.Errorf("#%d: unstableEnts = %+v, want %+v", i, ents, tt.wents)
}
w := previousEnts[len(previousEnts)-1].Index + 1
if g := raftLog.unstable.offset; g != w {
t.Errorf("#%d: unstable = %d, want %d", i, g, w)
}
}
}
func TestCommitTo(t *testing.T) {
previousEnts := []pb.Entry{{Term: 1, Index: 1}, {Term: 2, Index: 2}, {Term: 3, Index: 3}}
commit := uint64(2)
tests := []struct {
commit uint64
wcommit uint64
wpanic bool
}{
{3, 3, false},
{1, 2, false}, // never decrease
{4, 0, true}, // commit out of range -> panic
}
for i, tt := range tests {
func() {
defer func() {
if r := recover(); r != nil {
if !tt.wpanic {
t.Errorf("%d: panic = %v, want %v", i, true, tt.wpanic)
}
}
}()
raftLog := newLog(NewMemoryStorage(), raftLogger)
raftLog.append(previousEnts...)
raftLog.committed = commit
raftLog.commitTo(tt.commit)
if raftLog.committed != tt.wcommit {
t.Errorf("#%d: committed = %d, want %d", i, raftLog.committed, tt.wcommit)
}
}()
}
}
func TestStableTo(t *testing.T) {
tests := []struct {
stablei uint64
stablet uint64
wunstable uint64
}{
{1, 1, 2},
{2, 2, 3},
{2, 1, 1}, // bad term
{3, 1, 1}, // bad index
}
for i, tt := range tests {
raftLog := newLog(NewMemoryStorage(), raftLogger)
raftLog.append([]pb.Entry{{Index: 1, Term: 1}, {Index: 2, Term: 2}}...)
raftLog.stableTo(tt.stablei, tt.stablet)
if raftLog.unstable.offset != tt.wunstable {
t.Errorf("#%d: unstable = %d, want %d", i, raftLog.unstable.offset, tt.wunstable)
}
}
}
func TestStableToWithSnap(t *testing.T) {
snapi, snapt := uint64(5), uint64(2)
tests := []struct {
stablei uint64
stablet uint64
newEnts []pb.Entry
wunstable uint64
}{
{snapi + 1, snapt, nil, snapi + 1},
{snapi, snapt, nil, snapi + 1},
{snapi - 1, snapt, nil, snapi + 1},
{snapi + 1, snapt + 1, nil, snapi + 1},
{snapi, snapt + 1, nil, snapi + 1},
{snapi - 1, snapt + 1, nil, snapi + 1},
{snapi + 1, snapt, []pb.Entry{{Index: snapi + 1, Term: snapt}}, snapi + 2},
{snapi, snapt, []pb.Entry{{Index: snapi + 1, Term: snapt}}, snapi + 1},
{snapi - 1, snapt, []pb.Entry{{Index: snapi + 1, Term: snapt}}, snapi + 1},
{snapi + 1, snapt + 1, []pb.Entry{{Index: snapi + 1, Term: snapt}}, snapi + 1},
{snapi, snapt + 1, []pb.Entry{{Index: snapi + 1, Term: snapt}}, snapi + 1},
{snapi - 1, snapt + 1, []pb.Entry{{Index: snapi + 1, Term: snapt}}, snapi + 1},
}
for i, tt := range tests {
s := NewMemoryStorage()
s.ApplySnapshot(pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: snapi, Term: snapt}})
raftLog := newLog(s, raftLogger)
raftLog.append(tt.newEnts...)
raftLog.stableTo(tt.stablei, tt.stablet)
if raftLog.unstable.offset != tt.wunstable {
t.Errorf("#%d: unstable = %d, want %d", i, raftLog.unstable.offset, tt.wunstable)
}
}
}
// TestCompaction ensures that the number of log entries is correct after compactions.
func TestCompaction(t *testing.T) {
tests := []struct {
lastIndex uint64
compact []uint64
wleft []int
wallow bool
}{
// out of upper bound
{1000, []uint64{1001}, []int{-1}, false},
{1000, []uint64{300, 500, 800, 900}, []int{700, 500, 200, 100}, true},
// out of lower bound
{1000, []uint64{300, 299}, []int{700, -1}, false},
}
for i, tt := range tests {
func() {
defer func() {
if r := recover(); r != nil {
if tt.wallow {
t.Errorf("%d: allow = %v, want %v: %v", i, false, true, r)
}
}
}()
storage := NewMemoryStorage()
for i := uint64(1); i <= tt.lastIndex; i++ {
storage.Append([]pb.Entry{{Index: i}})
}
raftLog := newLog(storage, raftLogger)
raftLog.maybeCommit(tt.lastIndex, 0)
raftLog.appliedTo(raftLog.committed)
for j := 0; j < len(tt.compact); j++ {
err := storage.Compact(tt.compact[j])
if err != nil {
if tt.wallow {
t.Errorf("#%d.%d allow = %t, want %t", i, j, false, tt.wallow)
}
continue
}
if len(raftLog.allEntries()) != tt.wleft[j] {
t.Errorf("#%d.%d len = %d, want %d", i, j, len(raftLog.allEntries()), tt.wleft[j])
}
}
}()
}
}
func TestLogRestore(t *testing.T) {
index := uint64(1000)
term := uint64(1000)
snap := pb.SnapshotMetadata{Index: index, Term: term}
storage := NewMemoryStorage()
storage.ApplySnapshot(pb.Snapshot{Metadata: snap})
raftLog := newLog(storage, raftLogger)
if len(raftLog.allEntries()) != 0 {
t.Errorf("len = %d, want 0", len(raftLog.allEntries()))
}
if raftLog.firstIndex() != index+1 {
t.Errorf("firstIndex = %d, want %d", raftLog.firstIndex(), index+1)
}
if raftLog.committed != index {
t.Errorf("committed = %d, want %d", raftLog.committed, index)
}
if raftLog.unstable.offset != index+1 {
t.Errorf("unstable = %d, want %d", raftLog.unstable.offset, index+1)
}
if mustTerm(raftLog.term(index)) != term {
t.Errorf("term = %d, want %d", mustTerm(raftLog.term(index)), term)
}
}
func TestIsOutOfBounds(t *testing.T) {
offset := uint64(100)
num := uint64(100)
storage := NewMemoryStorage()
storage.ApplySnapshot(pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: offset}})
l := newLog(storage, raftLogger)
for i := uint64(1); i <= num; i++ {
l.append(pb.Entry{Index: i + offset})
}
first := offset + 1
tests := []struct {
lo, hi uint64
wpanic bool
wErrCompacted bool
}{
{
first - 2, first + 1,
false,
true,
},
{
first - 1, first + 1,
false,
true,
},
{
first, first,
false,
false,
},
{
first + num/2, first + num/2,
false,
false,
},
{
first + num - 1, first + num - 1,
false,
false,
},
{
first + num, first + num,
false,
false,
},
{
first + num, first + num + 1,
true,
false,
},
{
first + num + 1, first + num + 1,
true,
false,
},
}
for i, tt := range tests {
func() {
defer func() {
if r := recover(); r != nil {
if !tt.wpanic {
t.Errorf("%d: panic = %v, want %v: %v", i, true, false, r)
}
}
}()
err := l.mustCheckOutOfBounds(tt.lo, tt.hi)
if tt.wpanic {
t.Errorf("#%d: panic = %v, want %v", i, false, true)
}
if tt.wErrCompacted && err != ErrCompacted {
t.Errorf("#%d: err = %v, want %v", i, err, ErrCompacted)
}
if !tt.wErrCompacted && err != nil {
t.Errorf("#%d: unexpected err %v", i, err)
}
}()
}
}
func TestTerm(t *testing.T) {
var i uint64
offset := uint64(100)
num := uint64(100)
storage := NewMemoryStorage()
storage.ApplySnapshot(pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: offset, Term: 1}})
l := newLog(storage, raftLogger)
for i = 1; i < num; i++ {
l.append(pb.Entry{Index: offset + i, Term: i})
}
tests := []struct {
index uint64
w uint64
}{
{offset - 1, 0},
{offset, 1},
{offset + num/2, num / 2},
{offset + num - 1, num - 1},
{offset + num, 0},
}
for j, tt := range tests {
term := mustTerm(l.term(tt.index))
if term != tt.w {
t.Errorf("#%d: at = %d, want %d", j, term, tt.w)
}
}
}
func TestTermWithUnstableSnapshot(t *testing.T) {
storagesnapi := uint64(100)
unstablesnapi := storagesnapi + 5
storage := NewMemoryStorage()
storage.ApplySnapshot(pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: storagesnapi, Term: 1}})
l := newLog(storage, raftLogger)
l.restore(pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: unstablesnapi, Term: 1}})
tests := []struct {
index uint64
w uint64
}{
// cannot get term from storage
{storagesnapi, 0},
// cannot get term from the gap between storage ents and unstable snapshot
{storagesnapi + 1, 0},
{unstablesnapi - 1, 0},
// get term from unstable snapshot index
{unstablesnapi, 1},
}
for i, tt := range tests {
term := mustTerm(l.term(tt.index))
if term != tt.w {
t.Errorf("#%d: at = %d, want %d", i, term, tt.w)
}
}
}
func TestSlice(t *testing.T) {
var i uint64
offset := uint64(100)
num := uint64(100)
last := offset + num
half := offset + num/2
halfe := pb.Entry{Index: half, Term: half}
storage := NewMemoryStorage()
storage.ApplySnapshot(pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: offset}})
for i = 1; i < num/2; i++ {
storage.Append([]pb.Entry{{Index: offset + i, Term: offset + i}})
}
l := newLog(storage, raftLogger)
for i = num / 2; i < num; i++ {
l.append(pb.Entry{Index: offset + i, Term: offset + i})
}
tests := []struct {
from uint64
to uint64
limit uint64
w []pb.Entry
wpanic bool
}{
// test no limit
{offset - 1, offset + 1, noLimit, nil, false},
{offset, offset + 1, noLimit, nil, false},
{half - 1, half + 1, noLimit, []pb.Entry{{Index: half - 1, Term: half - 1}, {Index: half, Term: half}}, false},
{half, half + 1, noLimit, []pb.Entry{{Index: half, Term: half}}, false},
{last - 1, last, noLimit, []pb.Entry{{Index: last - 1, Term: last - 1}}, false},
{last, last + 1, noLimit, nil, true},
// test limit
{half - 1, half + 1, 0, []pb.Entry{{Index: half - 1, Term: half - 1}}, false},
{half - 1, half + 1, uint64(halfe.Size() + 1), []pb.Entry{{Index: half - 1, Term: half - 1}}, false},
{half - 2, half + 1, uint64(halfe.Size() + 1), []pb.Entry{{Index: half - 2, Term: half - 2}}, false},
{half - 1, half + 1, uint64(halfe.Size() * 2), []pb.Entry{{Index: half - 1, Term: half - 1}, {Index: half, Term: half}}, false},
{half - 1, half + 2, uint64(halfe.Size() * 3), []pb.Entry{{Index: half - 1, Term: half - 1}, {Index: half, Term: half}, {Index: half + 1, Term: half + 1}}, false},
{half, half + 2, uint64(halfe.Size()), []pb.Entry{{Index: half, Term: half}}, false},
{half, half + 2, uint64(halfe.Size() * 2), []pb.Entry{{Index: half, Term: half}, {Index: half + 1, Term: half + 1}}, false},
}
for j, tt := range tests {
func() {
defer func() {
if r := recover(); r != nil {
if !tt.wpanic {
t.Errorf("%d: panic = %v, want %v: %v", j, true, false, r)
}
}
}()
g, err := l.slice(tt.from, tt.to, tt.limit)
if tt.from <= offset && err != ErrCompacted {
t.Fatalf("#%d: err = %v, want %v", j, err, ErrCompacted)
}
if tt.from > offset && err != nil {
t.Fatalf("#%d: unexpected error %v", j, err)
}
if !reflect.DeepEqual(g, tt.w) {
t.Errorf("#%d: from %d to %d = %v, want %v", j, tt.from, tt.to, g, tt.w)
}
}()
}
}
func mustTerm(term uint64, err error) uint64 {
if err != nil {
panic(err)
}
return term
}

View File

@ -1,359 +0,0 @@
// Copyright 2015 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package raft
import (
"reflect"
"testing"
pb "go.etcd.io/etcd/raft/v3/raftpb"
)
func TestUnstableMaybeFirstIndex(t *testing.T) {
tests := []struct {
entries []pb.Entry
offset uint64
snap *pb.Snapshot
wok bool
windex uint64
}{
// no snapshot
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, nil,
false, 0,
},
{
[]pb.Entry{}, 0, nil,
false, 0,
},
// has snapshot
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, &pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 4, Term: 1}},
true, 5,
},
{
[]pb.Entry{}, 5, &pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 4, Term: 1}},
true, 5,
},
}
for i, tt := range tests {
u := unstable{
entries: tt.entries,
offset: tt.offset,
snapshot: tt.snap,
logger: raftLogger,
}
index, ok := u.maybeFirstIndex()
if ok != tt.wok {
t.Errorf("#%d: ok = %t, want %t", i, ok, tt.wok)
}
if index != tt.windex {
t.Errorf("#%d: index = %d, want %d", i, index, tt.windex)
}
}
}
func TestMaybeLastIndex(t *testing.T) {
tests := []struct {
entries []pb.Entry
offset uint64
snap *pb.Snapshot
wok bool
windex uint64
}{
// last in entries
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, nil,
true, 5,
},
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, &pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 4, Term: 1}},
true, 5,
},
// last in snapshot
{
[]pb.Entry{}, 5, &pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 4, Term: 1}},
true, 4,
},
// empty unstable
{
[]pb.Entry{}, 0, nil,
false, 0,
},
}
for i, tt := range tests {
u := unstable{
entries: tt.entries,
offset: tt.offset,
snapshot: tt.snap,
logger: raftLogger,
}
index, ok := u.maybeLastIndex()
if ok != tt.wok {
t.Errorf("#%d: ok = %t, want %t", i, ok, tt.wok)
}
if index != tt.windex {
t.Errorf("#%d: index = %d, want %d", i, index, tt.windex)
}
}
}
func TestUnstableMaybeTerm(t *testing.T) {
tests := []struct {
entries []pb.Entry
offset uint64
snap *pb.Snapshot
index uint64
wok bool
wterm uint64
}{
// term from entries
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, nil,
5,
true, 1,
},
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, nil,
6,
false, 0,
},
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, nil,
4,
false, 0,
},
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, &pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 4, Term: 1}},
5,
true, 1,
},
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, &pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 4, Term: 1}},
6,
false, 0,
},
// term from snapshot
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, &pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 4, Term: 1}},
4,
true, 1,
},
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, &pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 4, Term: 1}},
3,
false, 0,
},
{
[]pb.Entry{}, 5, &pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 4, Term: 1}},
5,
false, 0,
},
{
[]pb.Entry{}, 5, &pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 4, Term: 1}},
4,
true, 1,
},
{
[]pb.Entry{}, 0, nil,
5,
false, 0,
},
}
for i, tt := range tests {
u := unstable{
entries: tt.entries,
offset: tt.offset,
snapshot: tt.snap,
logger: raftLogger,
}
term, ok := u.maybeTerm(tt.index)
if ok != tt.wok {
t.Errorf("#%d: ok = %t, want %t", i, ok, tt.wok)
}
if term != tt.wterm {
t.Errorf("#%d: term = %d, want %d", i, term, tt.wterm)
}
}
}
func TestUnstableRestore(t *testing.T) {
u := unstable{
entries: []pb.Entry{{Index: 5, Term: 1}},
offset: 5,
snapshot: &pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 4, Term: 1}},
logger: raftLogger,
}
s := pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 6, Term: 2}}
u.restore(s)
if u.offset != s.Metadata.Index+1 {
t.Errorf("offset = %d, want %d", u.offset, s.Metadata.Index+1)
}
if len(u.entries) != 0 {
t.Errorf("len = %d, want 0", len(u.entries))
}
if !reflect.DeepEqual(u.snapshot, &s) {
t.Errorf("snap = %v, want %v", u.snapshot, &s)
}
}
func TestUnstableStableTo(t *testing.T) {
tests := []struct {
entries []pb.Entry
offset uint64
snap *pb.Snapshot
index, term uint64
woffset uint64
wlen int
}{
{
[]pb.Entry{}, 0, nil,
5, 1,
0, 0,
},
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, nil,
5, 1, // stable to the first entry
6, 0,
},
{
[]pb.Entry{{Index: 5, Term: 1}, {Index: 6, Term: 1}}, 5, nil,
5, 1, // stable to the first entry
6, 1,
},
{
[]pb.Entry{{Index: 6, Term: 2}}, 6, nil,
6, 1, // stable to the first entry and term mismatch
6, 1,
},
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, nil,
4, 1, // stable to old entry
5, 1,
},
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, nil,
4, 2, // stable to old entry
5, 1,
},
// with snapshot
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, &pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 4, Term: 1}},
5, 1, // stable to the first entry
6, 0,
},
{
[]pb.Entry{{Index: 5, Term: 1}, {Index: 6, Term: 1}}, 5, &pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 4, Term: 1}},
5, 1, // stable to the first entry
6, 1,
},
{
[]pb.Entry{{Index: 6, Term: 2}}, 6, &pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 5, Term: 1}},
6, 1, // stable to the first entry and term mismatch
6, 1,
},
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, &pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 4, Term: 1}},
4, 1, // stable to snapshot
5, 1,
},
{
[]pb.Entry{{Index: 5, Term: 2}}, 5, &pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 4, Term: 2}},
4, 1, // stable to old entry
5, 1,
},
}
for i, tt := range tests {
u := unstable{
entries: tt.entries,
offset: tt.offset,
snapshot: tt.snap,
logger: raftLogger,
}
u.stableTo(tt.index, tt.term)
if u.offset != tt.woffset {
t.Errorf("#%d: offset = %d, want %d", i, u.offset, tt.woffset)
}
if len(u.entries) != tt.wlen {
t.Errorf("#%d: len = %d, want %d", i, len(u.entries), tt.wlen)
}
}
}
func TestUnstableTruncateAndAppend(t *testing.T) {
tests := []struct {
entries []pb.Entry
offset uint64
snap *pb.Snapshot
toappend []pb.Entry
woffset uint64
wentries []pb.Entry
}{
// append to the end
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, nil,
[]pb.Entry{{Index: 6, Term: 1}, {Index: 7, Term: 1}},
5, []pb.Entry{{Index: 5, Term: 1}, {Index: 6, Term: 1}, {Index: 7, Term: 1}},
},
// replace the unstable entries
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, nil,
[]pb.Entry{{Index: 5, Term: 2}, {Index: 6, Term: 2}},
5, []pb.Entry{{Index: 5, Term: 2}, {Index: 6, Term: 2}},
},
{
[]pb.Entry{{Index: 5, Term: 1}}, 5, nil,
[]pb.Entry{{Index: 4, Term: 2}, {Index: 5, Term: 2}, {Index: 6, Term: 2}},
4, []pb.Entry{{Index: 4, Term: 2}, {Index: 5, Term: 2}, {Index: 6, Term: 2}},
},
// truncate the existing entries and append
{
[]pb.Entry{{Index: 5, Term: 1}, {Index: 6, Term: 1}, {Index: 7, Term: 1}}, 5, nil,
[]pb.Entry{{Index: 6, Term: 2}},
5, []pb.Entry{{Index: 5, Term: 1}, {Index: 6, Term: 2}},
},
{
[]pb.Entry{{Index: 5, Term: 1}, {Index: 6, Term: 1}, {Index: 7, Term: 1}}, 5, nil,
[]pb.Entry{{Index: 7, Term: 2}, {Index: 8, Term: 2}},
5, []pb.Entry{{Index: 5, Term: 1}, {Index: 6, Term: 1}, {Index: 7, Term: 2}, {Index: 8, Term: 2}},
},
}
for i, tt := range tests {
u := unstable{
entries: tt.entries,
offset: tt.offset,
snapshot: tt.snap,
logger: raftLogger,
}
u.truncateAndAppend(tt.toappend)
if u.offset != tt.woffset {
t.Errorf("#%d: offset = %d, want %d", i, u.offset, tt.woffset)
}
if !reflect.DeepEqual(u.entries, tt.wentries) {
t.Errorf("#%d: entries = %v, want %v", i, u.entries, tt.wentries)
}
}
}

View File

@ -1,51 +0,0 @@
// Copyright 2015 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package raft
import (
"context"
"testing"
"time"
)
func BenchmarkOneNode(b *testing.B) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
s := newTestMemoryStorage(withPeers(1))
rn := newTestRawNode(1, 10, 1, s)
n := newNode(rn)
go n.run()
defer n.Stop()
n.Campaign(ctx)
go func() {
for i := 0; i < b.N; i++ {
n.Propose(ctx, []byte("foo"))
}
}()
for {
rd := <-n.Ready()
s.Append(rd.Entries)
// a reasonable disk sync latency
time.Sleep(1 * time.Millisecond)
n.Advance()
if rd.HardState.Commit == uint64(b.N+1) {
return
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +0,0 @@
// Copyright 2019 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package quorum
import (
"fmt"
"math"
"math/rand"
"testing"
)
func BenchmarkMajorityConfig_CommittedIndex(b *testing.B) {
// go test -run - -bench . -benchmem ./raft/quorum
for _, n := range []int{1, 3, 5, 7, 9, 11} {
b.Run(fmt.Sprintf("voters=%d", n), func(b *testing.B) {
c := MajorityConfig{}
l := mapAckIndexer{}
for i := uint64(0); i < uint64(n); i++ {
c[i+1] = struct{}{}
l[i+1] = Index(rand.Int63n(math.MaxInt64))
}
for i := 0; i < b.N; i++ {
_ = c.CommittedIndex(l)
}
})
}
}

View File

@ -1,250 +0,0 @@
// Copyright 2019 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package quorum
import (
"fmt"
"strings"
"testing"
"github.com/cockroachdb/datadriven"
)
// TestDataDriven parses and executes the test cases in ./testdata/*. An entry
// in such a file specifies the command, which is either of "committed" to check
// CommittedIndex or "vote" to verify a VoteResult. The underlying configuration
// and inputs are specified via the arguments 'cfg' and 'cfgj' (for the majority
// config and, optionally, majority config joint to the first one) and 'idx'
// (for CommittedIndex) and 'votes' (for VoteResult).
//
// Internally, the harness runs some additional checks on each test case for
// which it is known that the result shouldn't change. For example,
// interchanging the majority configurations of a joint quorum must not
// influence the result; if it does, this is noted in the test's output.
func TestDataDriven(t *testing.T) {
datadriven.Walk(t, "testdata", func(t *testing.T, path string) {
datadriven.RunTest(t, path, func(t *testing.T, d *datadriven.TestData) string {
// Two majority configs. The first one is always used (though it may
// be empty) and the second one is used iff joint is true.
var joint bool
var ids, idsj []uint64
// The committed indexes for the nodes in the config in the order in
// which they appear in (ids,idsj), without repetition. An underscore
// denotes an omission (i.e. no information for this voter); this is
// different from 0. For example,
//
// cfg=(1,2) cfgj=(2,3,4) idxs=(_,5,_,7) initializes the idx for voter 2
// to 5 and that for voter 4 to 7 (and no others).
//
// cfgj=zero is specified to instruct the test harness to treat cfgj
// as zero instead of not specified (i.e. it will trigger a joint
// quorum test instead of a majority quorum test for cfg only).
var idxs []Index
// Votes. These are initialized similar to idxs except the only values
// used are 1 (voted against) and 2 (voted for). This looks awkward,
// but is convenient because it allows sharing code between the two.
var votes []Index
// Parse the args.
for _, arg := range d.CmdArgs {
for i := range arg.Vals {
switch arg.Key {
case "cfg":
var n uint64
arg.Scan(t, i, &n)
ids = append(ids, n)
case "cfgj":
joint = true
if arg.Vals[i] == "zero" {
if len(arg.Vals) != 1 {
t.Fatalf("cannot mix 'zero' into configuration")
}
} else {
var n uint64
arg.Scan(t, i, &n)
idsj = append(idsj, n)
}
case "idx":
var n uint64
// Register placeholders as zeroes.
if arg.Vals[i] != "_" {
arg.Scan(t, i, &n)
if n == 0 {
// This is a restriction caused by the above
// special-casing for _.
t.Fatalf("cannot use 0 as idx")
}
}
idxs = append(idxs, Index(n))
case "votes":
var s string
arg.Scan(t, i, &s)
switch s {
case "y":
votes = append(votes, 2)
case "n":
votes = append(votes, 1)
case "_":
votes = append(votes, 0)
default:
t.Fatalf("unknown vote: %s", s)
}
default:
t.Fatalf("unknown arg %s", arg.Key)
}
}
}
// Build the two majority configs.
c := MajorityConfig{}
for _, id := range ids {
c[id] = struct{}{}
}
cj := MajorityConfig{}
for _, id := range idsj {
cj[id] = struct{}{}
}
// Helper that returns an AckedIndexer which has the specified indexes
// mapped to the right IDs.
makeLookuper := func(idxs []Index, ids, idsj []uint64) mapAckIndexer {
l := mapAckIndexer{}
var p int // next to consume from idxs
for _, id := range append(append([]uint64(nil), ids...), idsj...) {
if _, ok := l[id]; ok {
continue
}
if p < len(idxs) {
// NB: this creates zero entries for placeholders that we remove later.
// The upshot of doing it that way is to avoid having to specify place-
// holders multiple times when omitting voters present in both halves of
// a joint config.
l[id] = idxs[p]
p++
}
}
for id := range l {
// Zero entries are created by _ placeholders; we don't want
// them in the lookuper because "no entry" is different from
// "zero entry". Note that we prevent tests from specifying
// zero commit indexes, so that there's no confusion between
// the two concepts.
if l[id] == 0 {
delete(l, id)
}
}
return l
}
{
input := idxs
if d.Cmd == "vote" {
input = votes
}
if voters := JointConfig([2]MajorityConfig{c, cj}).IDs(); len(voters) != len(input) {
return fmt.Sprintf("error: mismatched input (explicit or _) for voters %v: %v",
voters, input)
}
}
var buf strings.Builder
switch d.Cmd {
case "committed":
l := makeLookuper(idxs, ids, idsj)
// Branch based on whether this is a majority or joint quorum
// test case.
if !joint {
idx := c.CommittedIndex(l)
fmt.Fprint(&buf, c.Describe(l))
// These alternative computations should return the same
// result. If not, print to the output.
if aIdx := alternativeMajorityCommittedIndex(c, l); aIdx != idx {
fmt.Fprintf(&buf, "%s <-- via alternative computation\n", aIdx)
}
// Joining a majority with the empty majority should give same result.
if aIdx := JointConfig([2]MajorityConfig{c, {}}).CommittedIndex(l); aIdx != idx {
fmt.Fprintf(&buf, "%s <-- via zero-joint quorum\n", aIdx)
}
// Joining a majority with itself should give same result.
if aIdx := JointConfig([2]MajorityConfig{c, c}).CommittedIndex(l); aIdx != idx {
fmt.Fprintf(&buf, "%s <-- via self-joint quorum\n", aIdx)
}
overlay := func(c MajorityConfig, l AckedIndexer, id uint64, idx Index) AckedIndexer {
ll := mapAckIndexer{}
for iid := range c {
if iid == id {
ll[iid] = idx
} else if idx, ok := l.AckedIndex(iid); ok {
ll[iid] = idx
}
}
return ll
}
for id := range c {
iidx, _ := l.AckedIndex(id)
if idx > iidx && iidx > 0 {
// If the committed index was definitely above the currently
// inspected idx, the result shouldn't change if we lower it
// further.
lo := overlay(c, l, id, iidx-1)
if aIdx := c.CommittedIndex(lo); aIdx != idx {
fmt.Fprintf(&buf, "%s <-- overlaying %d->%d", aIdx, id, iidx)
}
lo = overlay(c, l, id, 0)
if aIdx := c.CommittedIndex(lo); aIdx != idx {
fmt.Fprintf(&buf, "%s <-- overlaying %d->0", aIdx, id)
}
}
}
fmt.Fprintf(&buf, "%s\n", idx)
} else {
cc := JointConfig([2]MajorityConfig{c, cj})
fmt.Fprint(&buf, cc.Describe(l))
idx := cc.CommittedIndex(l)
// Interchanging the majorities shouldn't make a difference. If it does, print.
if aIdx := JointConfig([2]MajorityConfig{cj, c}).CommittedIndex(l); aIdx != idx {
fmt.Fprintf(&buf, "%s <-- via symmetry\n", aIdx)
}
fmt.Fprintf(&buf, "%s\n", idx)
}
case "vote":
ll := makeLookuper(votes, ids, idsj)
l := map[uint64]bool{}
for id, v := range ll {
l[id] = v != 1 // NB: 1 == false, 2 == true
}
if !joint {
// Test a majority quorum.
r := c.VoteResult(l)
fmt.Fprintf(&buf, "%v\n", r)
} else {
// Run a joint quorum test case.
r := JointConfig([2]MajorityConfig{c, cj}).VoteResult(l)
// Interchanging the majorities shouldn't make a difference. If it does, print.
if ar := JointConfig([2]MajorityConfig{cj, c}).VoteResult(l); ar != r {
fmt.Fprintf(&buf, "%v <-- via symmetry\n", ar)
}
fmt.Fprintf(&buf, "%v\n", r)
}
default:
t.Fatalf("unknown command: %s", d.Cmd)
}
return buf.String()
})
})
}

View File

@ -1,122 +0,0 @@
// Copyright 2019 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package quorum
import (
"math"
"math/rand"
"reflect"
"testing"
"testing/quick"
)
// TestQuick uses quickcheck to heuristically assert that the main
// implementation of (MajorityConfig).CommittedIndex agrees with a "dumb"
// alternative version.
func TestQuick(t *testing.T) {
cfg := &quick.Config{
MaxCount: 50000,
}
t.Run("majority_commit", func(t *testing.T) {
fn1 := func(c memberMap, l idxMap) uint64 {
return uint64(MajorityConfig(c).CommittedIndex(mapAckIndexer(l)))
}
fn2 := func(c memberMap, l idxMap) uint64 {
return uint64(alternativeMajorityCommittedIndex(MajorityConfig(c), mapAckIndexer(l)))
}
if err := quick.CheckEqual(fn1, fn2, cfg); err != nil {
t.Fatal(err)
}
})
}
// smallRandIdxMap returns a reasonably sized map of ids to commit indexes.
func smallRandIdxMap(rand *rand.Rand, _ int) map[uint64]Index {
// Hard-code a reasonably small size here (quick will hard-code 50, which
// is not useful here).
size := 10
n := rand.Intn(size)
ids := rand.Perm(2 * n)[:n]
idxs := make([]int, len(ids))
for i := range idxs {
idxs[i] = rand.Intn(n)
}
m := map[uint64]Index{}
for i := range ids {
m[uint64(ids[i])] = Index(idxs[i])
}
return m
}
type idxMap map[uint64]Index
func (idxMap) Generate(rand *rand.Rand, size int) reflect.Value {
m := smallRandIdxMap(rand, size)
return reflect.ValueOf(m)
}
type memberMap map[uint64]struct{}
func (memberMap) Generate(rand *rand.Rand, size int) reflect.Value {
m := smallRandIdxMap(rand, size)
mm := map[uint64]struct{}{}
for id := range m {
mm[id] = struct{}{}
}
return reflect.ValueOf(mm)
}
// This is an alternative implementation of (MajorityConfig).CommittedIndex(l).
func alternativeMajorityCommittedIndex(c MajorityConfig, l AckedIndexer) Index {
if len(c) == 0 {
return math.MaxUint64
}
idToIdx := map[uint64]Index{}
for id := range c {
if idx, ok := l.AckedIndex(id); ok {
idToIdx[id] = idx
}
}
// Build a map from index to voters who have acked that or any higher index.
idxToVotes := map[Index]int{}
for _, idx := range idToIdx {
idxToVotes[idx] = 0
}
for _, idx := range idToIdx {
for idy := range idxToVotes {
if idy > idx {
continue
}
idxToVotes[idy]++
}
}
// Find the maximum index that has achieved quorum.
q := len(c)/2 + 1
var maxQuorumIdx Index
for idx, n := range idxToVotes {
if n >= q && idx > maxQuorumIdx {
maxQuorumIdx = idx
}
}
return maxQuorumIdx
}

View File

@ -1,481 +0,0 @@
# No difference between a simple majority quorum and a simple majority quorum
# joint with an empty majority quorum. (This is asserted for all datadriven tests
# by the framework, so we don't dwell on it more).
#
# Note that by specifying cfgj explicitly we tell the test harness to treat the
# input as a joint quorum and not a majority quorum. If we didn't specify
# cfgj=zero the test would pass just the same, but it wouldn't be exercising the
# joint quorum path.
committed cfg=(1,2,3) cfgj=zero idx=(100,101,99)
----
idx
x> 100 (id=1)
xx> 101 (id=2)
> 99 (id=3)
100
# Joint nonoverlapping singleton quorums.
committed cfg=(1) cfgj=(2) idx=(_,_)
----
idx
? 0 (id=1)
? 0 (id=2)
0
# Voter 1 has 100 committed, 2 nothing. This means we definitely won't commit
# past 100.
committed cfg=(1) cfgj=(2) idx=(100,_)
----
idx
x> 100 (id=1)
? 0 (id=2)
0
# Committed index collapses once both majorities do, to the lower index.
committed cfg=(1) cfgj=(2) idx=(13, 100)
----
idx
> 13 (id=1)
x> 100 (id=2)
13
# Joint overlapping (i.e. identical) singleton quorum.
committed cfg=(1) cfgj=(1) idx=(_)
----
idx
? 0 (id=1)
0
committed cfg=(1) cfgj=(1) idx=(100)
----
idx
> 100 (id=1)
100
# Two-node config joint with non-overlapping single node config
committed cfg=(1,3) cfgj=(2) idx=(_,_,_)
----
idx
? 0 (id=1)
? 0 (id=2)
? 0 (id=3)
0
committed cfg=(1,3) cfgj=(2) idx=(100,_,_)
----
idx
xx> 100 (id=1)
? 0 (id=2)
? 0 (id=3)
0
# 1 has 100 committed, 2 has 50 (collapsing half of the joint quorum to 50).
committed cfg=(1,3) cfgj=(2) idx=(100,_,50)
----
idx
xx> 100 (id=1)
x> 50 (id=2)
? 0 (id=3)
0
# 2 reports 45, collapsing the other half (to 45).
committed cfg=(1,3) cfgj=(2) idx=(100,45,50)
----
idx
xx> 100 (id=1)
x> 50 (id=2)
> 45 (id=3)
45
# Two-node config with overlapping single-node config.
committed cfg=(1,2) cfgj=(2) idx=(_,_)
----
idx
? 0 (id=1)
? 0 (id=2)
0
# 1 reports 100.
committed cfg=(1,2) cfgj=(2) idx=(100,_)
----
idx
x> 100 (id=1)
? 0 (id=2)
0
# 2 reports 100.
committed cfg=(1,2) cfgj=(2) idx=(_,100)
----
idx
? 0 (id=1)
x> 100 (id=2)
0
committed cfg=(1,2) cfgj=(2) idx=(50,100)
----
idx
> 50 (id=1)
x> 100 (id=2)
50
committed cfg=(1,2) cfgj=(2) idx=(100,50)
----
idx
x> 100 (id=1)
> 50 (id=2)
50
# Joint non-overlapping two-node configs.
committed cfg=(1,2) cfgj=(3,4) idx=(50,_,_,_)
----
idx
xxx> 50 (id=1)
? 0 (id=2)
? 0 (id=3)
? 0 (id=4)
0
committed cfg=(1,2) cfgj=(3,4) idx=(50,_,49,_)
----
idx
xxx> 50 (id=1)
? 0 (id=2)
xx> 49 (id=3)
? 0 (id=4)
0
committed cfg=(1,2) cfgj=(3,4) idx=(50,48,49,_)
----
idx
xxx> 50 (id=1)
x> 48 (id=2)
xx> 49 (id=3)
? 0 (id=4)
0
committed cfg=(1,2) cfgj=(3,4) idx=(50,48,49,47)
----
idx
xxx> 50 (id=1)
x> 48 (id=2)
xx> 49 (id=3)
> 47 (id=4)
47
# Joint overlapping two-node configs.
committed cfg=(1,2) cfgj=(2,3) idx=(_,_,_)
----
idx
? 0 (id=1)
? 0 (id=2)
? 0 (id=3)
0
committed cfg=(1,2) cfgj=(2,3) idx=(100,_,_)
----
idx
xx> 100 (id=1)
? 0 (id=2)
? 0 (id=3)
0
committed cfg=(1,2) cfgj=(2,3) idx=(_,100,_)
----
idx
? 0 (id=1)
xx> 100 (id=2)
? 0 (id=3)
0
committed cfg=(1,2) cfgj=(2,3) idx=(_,100,99)
----
idx
? 0 (id=1)
xx> 100 (id=2)
x> 99 (id=3)
0
committed cfg=(1,2) cfgj=(2,3) idx=(101,100,99)
----
idx
xx> 101 (id=1)
x> 100 (id=2)
> 99 (id=3)
99
# Joint identical two-node configs.
committed cfg=(1,2) cfgj=(1,2) idx=(_,_)
----
idx
? 0 (id=1)
? 0 (id=2)
0
committed cfg=(1,2) cfgj=(1,2) idx=(_,40)
----
idx
? 0 (id=1)
x> 40 (id=2)
0
committed cfg=(1,2) cfgj=(1,2) idx=(41,40)
----
idx
x> 41 (id=1)
> 40 (id=2)
40
# Joint disjoint three-node configs.
committed cfg=(1,2,3) cfgj=(4,5,6) idx=(_,_,_,_,_,_)
----
idx
? 0 (id=1)
? 0 (id=2)
? 0 (id=3)
? 0 (id=4)
? 0 (id=5)
? 0 (id=6)
0
committed cfg=(1,2,3) cfgj=(4,5,6) idx=(100,_,_,_,_,_)
----
idx
xxxxx> 100 (id=1)
? 0 (id=2)
? 0 (id=3)
? 0 (id=4)
? 0 (id=5)
? 0 (id=6)
0
committed cfg=(1,2,3) cfgj=(4,5,6) idx=(100,_,_,90,_,_)
----
idx
xxxxx> 100 (id=1)
? 0 (id=2)
? 0 (id=3)
xxxx> 90 (id=4)
? 0 (id=5)
? 0 (id=6)
0
committed cfg=(1,2,3) cfgj=(4,5,6) idx=(100,99,_,_,_,_)
----
idx
xxxxx> 100 (id=1)
xxxx> 99 (id=2)
? 0 (id=3)
? 0 (id=4)
? 0 (id=5)
? 0 (id=6)
0
# First quorum <= 99, second one <= 97. Both quorums guarantee that 90 is
# committed.
committed cfg=(1,2,3) cfgj=(4,5,6) idx=(_,99,90,97,95,_)
----
idx
? 0 (id=1)
xxxxx> 99 (id=2)
xx> 90 (id=3)
xxxx> 97 (id=4)
xxx> 95 (id=5)
? 0 (id=6)
90
# First quorum collapsed to 92. Second one already had at least 95 committed,
# so the result also collapses.
committed cfg=(1,2,3) cfgj=(4,5,6) idx=(92,99,90,97,95,_)
----
idx
xx> 92 (id=1)
xxxxx> 99 (id=2)
x> 90 (id=3)
xxxx> 97 (id=4)
xxx> 95 (id=5)
? 0 (id=6)
92
# Second quorum collapses, but nothing changes in the output.
committed cfg=(1,2,3) cfgj=(4,5,6) idx=(92,99,90,97,95,77)
----
idx
xx> 92 (id=1)
xxxxx> 99 (id=2)
x> 90 (id=3)
xxxx> 97 (id=4)
xxx> 95 (id=5)
> 77 (id=6)
92
# Joint overlapping three-node configs.
committed cfg=(1,2,3) cfgj=(1,4,5) idx=(_,_,_,_,_)
----
idx
? 0 (id=1)
? 0 (id=2)
? 0 (id=3)
? 0 (id=4)
? 0 (id=5)
0
committed cfg=(1,2,3) cfgj=(1,4,5) idx=(100,_,_,_,_)
----
idx
xxxx> 100 (id=1)
? 0 (id=2)
? 0 (id=3)
? 0 (id=4)
? 0 (id=5)
0
committed cfg=(1,2,3) cfgj=(1,4,5) idx=(100,101,_,_,_)
----
idx
xxx> 100 (id=1)
xxxx> 101 (id=2)
? 0 (id=3)
? 0 (id=4)
? 0 (id=5)
0
committed cfg=(1,2,3) cfgj=(1,4,5) idx=(100,101,100,_,_)
----
idx
xx> 100 (id=1)
xxxx> 101 (id=2)
> 100 (id=3)
? 0 (id=4)
? 0 (id=5)
0
# Second quorum could commit either 98 or 99, but first quorum is open.
committed cfg=(1,2,3) cfgj=(1,4,5) idx=(_,100,_,99,98)
----
idx
? 0 (id=1)
xxxx> 100 (id=2)
? 0 (id=3)
xxx> 99 (id=4)
xx> 98 (id=5)
0
# Additionally, first quorum can commit either 100 or 99
committed cfg=(1,2,3) cfgj=(1,4,5) idx=(_,100,99,99,98)
----
idx
? 0 (id=1)
xxxx> 100 (id=2)
xx> 99 (id=3)
> 99 (id=4)
x> 98 (id=5)
98
committed cfg=(1,2,3) cfgj=(1,4,5) idx=(1,100,99,99,98)
----
idx
> 1 (id=1)
xxxx> 100 (id=2)
xx> 99 (id=3)
> 99 (id=4)
x> 98 (id=5)
98
committed cfg=(1,2,3) cfgj=(1,4,5) idx=(100,100,99,99,98)
----
idx
xxx> 100 (id=1)
> 100 (id=2)
x> 99 (id=3)
> 99 (id=4)
> 98 (id=5)
99
# More overlap.
committed cfg=(1,2,3) cfgj=(2,3,4) idx=(_,_,_,_)
----
idx
? 0 (id=1)
? 0 (id=2)
? 0 (id=3)
? 0 (id=4)
0
committed cfg=(1,2,3) cfgj=(2,3,4) idx=(_,100,99,_)
----
idx
? 0 (id=1)
xxx> 100 (id=2)
xx> 99 (id=3)
? 0 (id=4)
99
committed cfg=(1,2,3) cfgj=(2,3,4) idx=(98,100,99,_)
----
idx
x> 98 (id=1)
xxx> 100 (id=2)
xx> 99 (id=3)
? 0 (id=4)
99
committed cfg=(1,2,3) cfgj=(2,3,4) idx=(100,100,99,_)
----
idx
xx> 100 (id=1)
> 100 (id=2)
x> 99 (id=3)
? 0 (id=4)
99
committed cfg=(1,2,3) cfgj=(2,3,4) idx=(100,100,99,98)
----
idx
xx> 100 (id=1)
> 100 (id=2)
x> 99 (id=3)
> 98 (id=4)
99
committed cfg=(1,2,3) cfgj=(2,3,4) idx=(100,_,_,101)
----
idx
xx> 100 (id=1)
? 0 (id=2)
? 0 (id=3)
xxx> 101 (id=4)
0
committed cfg=(1,2,3) cfgj=(2,3,4) idx=(100,99,_,101)
----
idx
xx> 100 (id=1)
x> 99 (id=2)
? 0 (id=3)
xxx> 101 (id=4)
99
# Identical. This is also exercised in the test harness, so it's listed here
# only briefly.
committed cfg=(1,2,3) cfgj=(1,2,3) idx=(50,45,_)
----
idx
xx> 50 (id=1)
x> 45 (id=2)
? 0 (id=3)
45

View File

@ -1,165 +0,0 @@
# Empty joint config wins all votes. This isn't used in production. Note that
# by specifying cfgj explicitly we tell the test harness to treat the input as
# a joint quorum and not a majority quorum.
vote cfgj=zero
----
VoteWon
# More examples with close to trivial configs.
vote cfg=(1) cfgj=zero votes=(_)
----
VotePending
vote cfg=(1) cfgj=zero votes=(y)
----
VoteWon
vote cfg=(1) cfgj=zero votes=(n)
----
VoteLost
vote cfg=(1) cfgj=(1) votes=(_)
----
VotePending
vote cfg=(1) cfgj=(1) votes=(y)
----
VoteWon
vote cfg=(1) cfgj=(1) votes=(n)
----
VoteLost
vote cfg=(1) cfgj=(2) votes=(_,_)
----
VotePending
vote cfg=(1) cfgj=(2) votes=(y,_)
----
VotePending
vote cfg=(1) cfgj=(2) votes=(y,y)
----
VoteWon
vote cfg=(1) cfgj=(2) votes=(y,n)
----
VoteLost
vote cfg=(1) cfgj=(2) votes=(n,_)
----
VoteLost
vote cfg=(1) cfgj=(2) votes=(n,n)
----
VoteLost
vote cfg=(1) cfgj=(2) votes=(n,y)
----
VoteLost
# Two node configs.
vote cfg=(1,2) cfgj=(3,4) votes=(_,_,_,_)
----
VotePending
vote cfg=(1,2) cfgj=(3,4) votes=(y,_,_,_)
----
VotePending
vote cfg=(1,2) cfgj=(3,4) votes=(y,y,_,_)
----
VotePending
vote cfg=(1,2) cfgj=(3,4) votes=(y,y,n,_)
----
VoteLost
vote cfg=(1,2) cfgj=(3,4) votes=(y,y,n,n)
----
VoteLost
vote cfg=(1,2) cfgj=(3,4) votes=(y,y,y,n)
----
VoteLost
vote cfg=(1,2) cfgj=(3,4) votes=(y,y,y,y)
----
VoteWon
vote cfg=(1,2) cfgj=(2,3) votes=(_,_,_)
----
VotePending
vote cfg=(1,2) cfgj=(2,3) votes=(_,n,_)
----
VoteLost
vote cfg=(1,2) cfgj=(2,3) votes=(y,y,_)
----
VotePending
vote cfg=(1,2) cfgj=(2,3) votes=(y,y,n)
----
VoteLost
vote cfg=(1,2) cfgj=(2,3) votes=(y,y,y)
----
VoteWon
vote cfg=(1,2) cfgj=(1,2) votes=(_,_)
----
VotePending
vote cfg=(1,2) cfgj=(1,2) votes=(y,_)
----
VotePending
vote cfg=(1,2) cfgj=(1,2) votes=(y,n)
----
VoteLost
vote cfg=(1,2) cfgj=(1,2) votes=(n,_)
----
VoteLost
vote cfg=(1,2) cfgj=(1,2) votes=(n,n)
----
VoteLost
# Simple example for overlapping three node configs.
vote cfg=(1,2,3) cfgj=(2,3,4) votes=(_,_,_,_)
----
VotePending
vote cfg=(1,2,3) cfgj=(2,3,4) votes=(_,n,_,_)
----
VotePending
vote cfg=(1,2,3) cfgj=(2,3,4) votes=(_,n,n,_)
----
VoteLost
vote cfg=(1,2,3) cfgj=(2,3,4) votes=(_,y,y,_)
----
VoteWon
vote cfg=(1,2,3) cfgj=(2,3,4) votes=(y,y,_,_)
----
VotePending
vote cfg=(1,2,3) cfgj=(2,3,4) votes=(y,y,n,_)
----
VotePending
vote cfg=(1,2,3) cfgj=(2,3,4) votes=(y,y,n,n)
----
VoteLost
vote cfg=(1,2,3) cfgj=(2,3,4) votes=(y,y,n,y)
----
VoteWon

View File

@ -1,153 +0,0 @@
# The empty quorum commits "everything". This is useful for its use in joint
# quorums.
committed
----
<empty majority quorum>∞
# A single voter quorum is not final when no index is known.
committed cfg=(1) idx=(_)
----
idx
? 0 (id=1)
0
# When an index is known, that's the committed index, and that's final.
committed cfg=(1) idx=(12)
----
idx
> 12 (id=1)
12
# With two nodes, start out similarly.
committed cfg=(1, 2) idx=(_,_)
----
idx
? 0 (id=1)
? 0 (id=2)
0
# The first committed index becomes known (for n1). Nothing changes in the
# output because idx=12 is not known to be on a quorum (which is both nodes).
committed cfg=(1, 2) idx=(12,_)
----
idx
x> 12 (id=1)
? 0 (id=2)
0
# The second index comes in and finalize the decision. The result will be the
# smaller of the two indexes.
committed cfg=(1,2) idx=(12,5)
----
idx
x> 12 (id=1)
> 5 (id=2)
5
# No surprises for three nodes.
committed cfg=(1,2,3) idx=(_,_,_)
----
idx
? 0 (id=1)
? 0 (id=2)
? 0 (id=3)
0
committed cfg=(1,2,3) idx=(12,_,_)
----
idx
xx> 12 (id=1)
? 0 (id=2)
? 0 (id=3)
0
# We see a committed index, but a higher committed index for the last pending
# votes could change (increment) the outcome, so not final yet.
committed cfg=(1,2,3) idx=(12,5,_)
----
idx
xx> 12 (id=1)
x> 5 (id=2)
? 0 (id=3)
5
# a) the case in which it does:
committed cfg=(1,2,3) idx=(12,5,6)
----
idx
xx> 12 (id=1)
> 5 (id=2)
x> 6 (id=3)
6
# b) the case in which it does not:
committed cfg=(1,2,3) idx=(12,5,4)
----
idx
xx> 12 (id=1)
x> 5 (id=2)
> 4 (id=3)
5
# c) a different case in which the last index is pending but it has no chance of
# swaying the outcome (because nobody in the current quorum agrees on anything
# higher than the candidate):
committed cfg=(1,2,3) idx=(5,5,_)
----
idx
x> 5 (id=1)
> 5 (id=2)
? 0 (id=3)
5
# c) continued: Doesn't matter what shows up last. The result is final.
committed cfg=(1,2,3) idx=(5,5,12)
----
idx
> 5 (id=1)
> 5 (id=2)
xx> 12 (id=3)
5
# With all committed idx known, the result is final.
committed cfg=(1, 2, 3) idx=(100, 101, 103)
----
idx
> 100 (id=1)
x> 101 (id=2)
xx> 103 (id=3)
101
# Some more complicated examples. Similar to case c) above. The result is
# already final because no index higher than 103 is one short of quorum.
committed cfg=(1, 2, 3, 4, 5) idx=(101, 104, 103, 103,_)
----
idx
x> 101 (id=1)
xxxx> 104 (id=2)
xx> 103 (id=3)
> 103 (id=4)
? 0 (id=5)
103
# A similar case which is not final because another vote for >= 103 would change
# the outcome.
committed cfg=(1, 2, 3, 4, 5) idx=(101, 102, 103, 103,_)
----
idx
x> 101 (id=1)
xx> 102 (id=2)
xxx> 103 (id=3)
> 103 (id=4)
? 0 (id=5)
102

View File

@ -1,97 +0,0 @@
# The empty config always announces a won vote.
vote
----
VoteWon
vote cfg=(1) votes=(_)
----
VotePending
vote cfg=(1) votes=(n)
----
VoteLost
vote cfg=(123) votes=(y)
----
VoteWon
vote cfg=(4,8) votes=(_,_)
----
VotePending
# With two voters, a single rejection loses the vote.
vote cfg=(4,8) votes=(n,_)
----
VoteLost
vote cfg=(4,8) votes=(y,_)
----
VotePending
vote cfg=(4,8) votes=(n,y)
----
VoteLost
vote cfg=(4,8) votes=(y,y)
----
VoteWon
vote cfg=(2,4,7) votes=(_,_,_)
----
VotePending
vote cfg=(2,4,7) votes=(n,_,_)
----
VotePending
vote cfg=(2,4,7) votes=(y,_,_)
----
VotePending
vote cfg=(2,4,7) votes=(n,n,_)
----
VoteLost
vote cfg=(2,4,7) votes=(y,n,_)
----
VotePending
vote cfg=(2,4,7) votes=(y,y,_)
----
VoteWon
vote cfg=(2,4,7) votes=(y,y,n)
----
VoteWon
vote cfg=(2,4,7) votes=(n,y,n)
----
VoteLost
# Test some random example with seven nodes (why not).
vote cfg=(1,2,3,4,5,6,7) votes=(y,y,n,y,_,_,_)
----
VotePending
vote cfg=(1,2,3,4,5,6,7) votes=(_,y,y,_,n,y,n)
----
VotePending
vote cfg=(1,2,3,4,5,6,7) votes=(y,y,n,y,_,n,y)
----
VoteWon
vote cfg=(1,2,3,4,5,6,7) votes=(y,y,_,n,y,n,n)
----
VotePending
vote cfg=(1,2,3,4,5,6,7) votes=(y,y,n,y,n,n,n)
----
VoteLost

View File

@ -1,156 +0,0 @@
// Copyright 2015 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package raft
import (
"testing"
pb "go.etcd.io/etcd/raft/v3/raftpb"
)
// TestMsgAppFlowControlFull ensures:
// 1. msgApp can fill the sending window until full
// 2. when the window is full, no more msgApp can be sent.
func TestMsgAppFlowControlFull(t *testing.T) {
r := newTestRaft(1, 5, 1, newTestMemoryStorage(withPeers(1, 2)))
r.becomeCandidate()
r.becomeLeader()
pr2 := r.prs.Progress[2]
// force the progress to be in replicate state
pr2.BecomeReplicate()
// fill in the inflights window
for i := 0; i < r.prs.MaxInflight; i++ {
r.Step(pb.Message{From: 1, To: 1, Type: pb.MsgProp, Entries: []pb.Entry{{Data: []byte("somedata")}}})
ms := r.readMessages()
if len(ms) != 1 {
t.Fatalf("#%d: len(ms) = %d, want 1", i, len(ms))
}
}
// ensure 1
if !pr2.Inflights.Full() {
t.Fatalf("inflights.full = %t, want %t", pr2.Inflights.Full(), true)
}
// ensure 2
for i := 0; i < 10; i++ {
r.Step(pb.Message{From: 1, To: 1, Type: pb.MsgProp, Entries: []pb.Entry{{Data: []byte("somedata")}}})
ms := r.readMessages()
if len(ms) != 0 {
t.Fatalf("#%d: len(ms) = %d, want 0", i, len(ms))
}
}
}
// TestMsgAppFlowControlMoveForward ensures msgAppResp can move
// forward the sending window correctly:
// 1. valid msgAppResp.index moves the windows to pass all smaller or equal index.
// 2. out-of-dated msgAppResp has no effect on the sliding window.
func TestMsgAppFlowControlMoveForward(t *testing.T) {
r := newTestRaft(1, 5, 1, newTestMemoryStorage(withPeers(1, 2)))
r.becomeCandidate()
r.becomeLeader()
pr2 := r.prs.Progress[2]
// force the progress to be in replicate state
pr2.BecomeReplicate()
// fill in the inflights window
for i := 0; i < r.prs.MaxInflight; i++ {
r.Step(pb.Message{From: 1, To: 1, Type: pb.MsgProp, Entries: []pb.Entry{{Data: []byte("somedata")}}})
r.readMessages()
}
// 1 is noop, 2 is the first proposal we just sent.
// so we start with 2.
for tt := 2; tt < r.prs.MaxInflight; tt++ {
// move forward the window
r.Step(pb.Message{From: 2, To: 1, Type: pb.MsgAppResp, Index: uint64(tt)})
r.readMessages()
// fill in the inflights window again
r.Step(pb.Message{From: 1, To: 1, Type: pb.MsgProp, Entries: []pb.Entry{{Data: []byte("somedata")}}})
ms := r.readMessages()
if len(ms) != 1 {
t.Fatalf("#%d: len(ms) = %d, want 1", tt, len(ms))
}
// ensure 1
if !pr2.Inflights.Full() {
t.Fatalf("inflights.full = %t, want %t", pr2.Inflights.Full(), true)
}
// ensure 2
for i := 0; i < tt; i++ {
r.Step(pb.Message{From: 2, To: 1, Type: pb.MsgAppResp, Index: uint64(i)})
if !pr2.Inflights.Full() {
t.Fatalf("#%d: inflights.full = %t, want %t", tt, pr2.Inflights.Full(), true)
}
}
}
}
// TestMsgAppFlowControlRecvHeartbeat ensures a heartbeat response
// frees one slot if the window is full.
func TestMsgAppFlowControlRecvHeartbeat(t *testing.T) {
r := newTestRaft(1, 5, 1, newTestMemoryStorage(withPeers(1, 2)))
r.becomeCandidate()
r.becomeLeader()
pr2 := r.prs.Progress[2]
// force the progress to be in replicate state
pr2.BecomeReplicate()
// fill in the inflights window
for i := 0; i < r.prs.MaxInflight; i++ {
r.Step(pb.Message{From: 1, To: 1, Type: pb.MsgProp, Entries: []pb.Entry{{Data: []byte("somedata")}}})
r.readMessages()
}
for tt := 1; tt < 5; tt++ {
if !pr2.Inflights.Full() {
t.Fatalf("#%d: inflights.full = %t, want %t", tt, pr2.Inflights.Full(), true)
}
// recv tt msgHeartbeatResp and expect one free slot
for i := 0; i < tt; i++ {
r.Step(pb.Message{From: 2, To: 1, Type: pb.MsgHeartbeatResp})
r.readMessages()
if pr2.Inflights.Full() {
t.Fatalf("#%d.%d: inflights.full = %t, want %t", tt, i, pr2.Inflights.Full(), false)
}
}
// one slot
r.Step(pb.Message{From: 1, To: 1, Type: pb.MsgProp, Entries: []pb.Entry{{Data: []byte("somedata")}}})
ms := r.readMessages()
if len(ms) != 1 {
t.Fatalf("#%d: free slot = 0, want 1", tt)
}
// and just one slot
for i := 0; i < 10; i++ {
r.Step(pb.Message{From: 1, To: 1, Type: pb.MsgProp, Entries: []pb.Entry{{Data: []byte("somedata")}}})
ms1 := r.readMessages()
if len(ms1) != 0 {
t.Fatalf("#%d.%d: len(ms) = %d, want 0", tt, i, len(ms1))
}
}
// clear all pending messages.
r.Step(pb.Message{From: 2, To: 1, Type: pb.MsgHeartbeatResp})
r.readMessages()
}
}

View File

@ -1,937 +0,0 @@
// Copyright 2015 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
This file contains tests which verify that the scenarios described
in the raft paper (https://raft.github.io/raft.pdf) are
handled by the raft implementation correctly. Each test focuses on
several sentences written in the paper. This could help us to prevent
most implementation bugs.
Each test is composed of three parts: init, test and check.
Init part uses simple and understandable way to simulate the init state.
Test part uses Step function to generate the scenario. Check part checks
outgoing messages and state.
*/
package raft
import (
"fmt"
"reflect"
"sort"
"testing"
pb "go.etcd.io/etcd/raft/v3/raftpb"
)
func TestFollowerUpdateTermFromMessage(t *testing.T) {
testUpdateTermFromMessage(t, StateFollower)
}
func TestCandidateUpdateTermFromMessage(t *testing.T) {
testUpdateTermFromMessage(t, StateCandidate)
}
func TestLeaderUpdateTermFromMessage(t *testing.T) {
testUpdateTermFromMessage(t, StateLeader)
}
// testUpdateTermFromMessage tests that if one servers current term is
// smaller than the others, then it updates its current term to the larger
// value. If a candidate or leader discovers that its term is out of date,
// it immediately reverts to follower state.
// Reference: section 5.1
func testUpdateTermFromMessage(t *testing.T, state StateType) {
r := newTestRaft(1, 10, 1, newTestMemoryStorage(withPeers(1, 2, 3)))
switch state {
case StateFollower:
r.becomeFollower(1, 2)
case StateCandidate:
r.becomeCandidate()
case StateLeader:
r.becomeCandidate()
r.becomeLeader()
}
r.Step(pb.Message{Type: pb.MsgApp, Term: 2})
if r.Term != 2 {
t.Errorf("term = %d, want %d", r.Term, 2)
}
if r.state != StateFollower {
t.Errorf("state = %v, want %v", r.state, StateFollower)
}
}
// TestRejectStaleTermMessage tests that if a server receives a request with
// a stale term number, it rejects the request.
// Our implementation ignores the request instead.
// Reference: section 5.1
func TestRejectStaleTermMessage(t *testing.T) {
called := false
fakeStep := func(r *raft, m pb.Message) error {
called = true
return nil
}
r := newTestRaft(1, 10, 1, newTestMemoryStorage(withPeers(1, 2, 3)))
r.step = fakeStep
r.loadState(pb.HardState{Term: 2})
r.Step(pb.Message{Type: pb.MsgApp, Term: r.Term - 1})
if called {
t.Errorf("stepFunc called = %v, want %v", called, false)
}
}
// TestStartAsFollower tests that when servers start up, they begin as followers.
// Reference: section 5.2
func TestStartAsFollower(t *testing.T) {
r := newTestRaft(1, 10, 1, newTestMemoryStorage(withPeers(1, 2, 3)))
if r.state != StateFollower {
t.Errorf("state = %s, want %s", r.state, StateFollower)
}
}
// TestLeaderBcastBeat tests that if the leader receives a heartbeat tick,
// it will send a MsgHeartbeat with m.Index = 0, m.LogTerm=0 and empty entries
// as heartbeat to all followers.
// Reference: section 5.2
func TestLeaderBcastBeat(t *testing.T) {
// heartbeat interval
hi := 1
r := newTestRaft(1, 10, hi, newTestMemoryStorage(withPeers(1, 2, 3)))
r.becomeCandidate()
r.becomeLeader()
for i := 0; i < 10; i++ {
mustAppendEntry(r, pb.Entry{Index: uint64(i) + 1})
}
for i := 0; i < hi; i++ {
r.tick()
}
msgs := r.readMessages()
sort.Sort(messageSlice(msgs))
wmsgs := []pb.Message{
{From: 1, To: 2, Term: 1, Type: pb.MsgHeartbeat},
{From: 1, To: 3, Term: 1, Type: pb.MsgHeartbeat},
}
if !reflect.DeepEqual(msgs, wmsgs) {
t.Errorf("msgs = %v, want %v", msgs, wmsgs)
}
}
func TestFollowerStartElection(t *testing.T) {
testNonleaderStartElection(t, StateFollower)
}
func TestCandidateStartNewElection(t *testing.T) {
testNonleaderStartElection(t, StateCandidate)
}
// testNonleaderStartElection tests that if a follower receives no communication
// over election timeout, it begins an election to choose a new leader. It
// increments its current term and transitions to candidate state. It then
// votes for itself and issues RequestVote RPCs in parallel to each of the
// other servers in the cluster.
// Reference: section 5.2
// Also if a candidate fails to obtain a majority, it will time out and
// start a new election by incrementing its term and initiating another
// round of RequestVote RPCs.
// Reference: section 5.2
func testNonleaderStartElection(t *testing.T, state StateType) {
// election timeout
et := 10
r := newTestRaft(1, et, 1, newTestMemoryStorage(withPeers(1, 2, 3)))
switch state {
case StateFollower:
r.becomeFollower(1, 2)
case StateCandidate:
r.becomeCandidate()
}
for i := 1; i < 2*et; i++ {
r.tick()
}
if r.Term != 2 {
t.Errorf("term = %d, want 2", r.Term)
}
if r.state != StateCandidate {
t.Errorf("state = %s, want %s", r.state, StateCandidate)
}
if !r.prs.Votes[r.id] {
t.Errorf("vote for self = false, want true")
}
msgs := r.readMessages()
sort.Sort(messageSlice(msgs))
wmsgs := []pb.Message{
{From: 1, To: 2, Term: 2, Type: pb.MsgVote},
{From: 1, To: 3, Term: 2, Type: pb.MsgVote},
}
if !reflect.DeepEqual(msgs, wmsgs) {
t.Errorf("msgs = %v, want %v", msgs, wmsgs)
}
}
// TestLeaderElectionInOneRoundRPC tests all cases that may happen in
// leader election during one round of RequestVote RPC:
// a) it wins the election
// b) it loses the election
// c) it is unclear about the result
// Reference: section 5.2
func TestLeaderElectionInOneRoundRPC(t *testing.T) {
tests := []struct {
size int
votes map[uint64]bool
state StateType
}{
// win the election when receiving votes from a majority of the servers
{1, map[uint64]bool{}, StateLeader},
{3, map[uint64]bool{2: true, 3: true}, StateLeader},
{3, map[uint64]bool{2: true}, StateLeader},
{5, map[uint64]bool{2: true, 3: true, 4: true, 5: true}, StateLeader},
{5, map[uint64]bool{2: true, 3: true, 4: true}, StateLeader},
{5, map[uint64]bool{2: true, 3: true}, StateLeader},
// return to follower state if it receives vote denial from a majority
{3, map[uint64]bool{2: false, 3: false}, StateFollower},
{5, map[uint64]bool{2: false, 3: false, 4: false, 5: false}, StateFollower},
{5, map[uint64]bool{2: true, 3: false, 4: false, 5: false}, StateFollower},
// stay in candidate if it does not obtain the majority
{3, map[uint64]bool{}, StateCandidate},
{5, map[uint64]bool{2: true}, StateCandidate},
{5, map[uint64]bool{2: false, 3: false}, StateCandidate},
{5, map[uint64]bool{}, StateCandidate},
}
for i, tt := range tests {
r := newTestRaft(1, 10, 1, newTestMemoryStorage(withPeers(idsBySize(tt.size)...)))
r.Step(pb.Message{From: 1, To: 1, Type: pb.MsgHup})
for id, vote := range tt.votes {
r.Step(pb.Message{From: id, To: 1, Term: r.Term, Type: pb.MsgVoteResp, Reject: !vote})
}
if r.state != tt.state {
t.Errorf("#%d: state = %s, want %s", i, r.state, tt.state)
}
if g := r.Term; g != 1 {
t.Errorf("#%d: term = %d, want %d", i, g, 1)
}
}
}
// TestFollowerVote tests that each follower will vote for at most one
// candidate in a given term, on a first-come-first-served basis.
// Reference: section 5.2
func TestFollowerVote(t *testing.T) {
tests := []struct {
vote uint64
nvote uint64
wreject bool
}{
{None, 1, false},
{None, 2, false},
{1, 1, false},
{2, 2, false},
{1, 2, true},
{2, 1, true},
}
for i, tt := range tests {
r := newTestRaft(1, 10, 1, newTestMemoryStorage(withPeers(1, 2, 3)))
r.loadState(pb.HardState{Term: 1, Vote: tt.vote})
r.Step(pb.Message{From: tt.nvote, To: 1, Term: 1, Type: pb.MsgVote})
msgs := r.readMessages()
wmsgs := []pb.Message{
{From: 1, To: tt.nvote, Term: 1, Type: pb.MsgVoteResp, Reject: tt.wreject},
}
if !reflect.DeepEqual(msgs, wmsgs) {
t.Errorf("#%d: msgs = %v, want %v", i, msgs, wmsgs)
}
}
}
// TestCandidateFallback tests that while waiting for votes,
// if a candidate receives an AppendEntries RPC from another server claiming
// to be leader whose term is at least as large as the candidate's current term,
// it recognizes the leader as legitimate and returns to follower state.
// Reference: section 5.2
func TestCandidateFallback(t *testing.T) {
tests := []pb.Message{
{From: 2, To: 1, Term: 1, Type: pb.MsgApp},
{From: 2, To: 1, Term: 2, Type: pb.MsgApp},
}
for i, tt := range tests {
r := newTestRaft(1, 10, 1, newTestMemoryStorage(withPeers(1, 2, 3)))
r.Step(pb.Message{From: 1, To: 1, Type: pb.MsgHup})
if r.state != StateCandidate {
t.Fatalf("unexpected state = %s, want %s", r.state, StateCandidate)
}
r.Step(tt)
if g := r.state; g != StateFollower {
t.Errorf("#%d: state = %s, want %s", i, g, StateFollower)
}
if g := r.Term; g != tt.Term {
t.Errorf("#%d: term = %d, want %d", i, g, tt.Term)
}
}
}
func TestFollowerElectionTimeoutRandomized(t *testing.T) {
SetLogger(discardLogger)
defer SetLogger(defaultLogger)
testNonleaderElectionTimeoutRandomized(t, StateFollower)
}
func TestCandidateElectionTimeoutRandomized(t *testing.T) {
SetLogger(discardLogger)
defer SetLogger(defaultLogger)
testNonleaderElectionTimeoutRandomized(t, StateCandidate)
}
// testNonleaderElectionTimeoutRandomized tests that election timeout for
// follower or candidate is randomized.
// Reference: section 5.2
func testNonleaderElectionTimeoutRandomized(t *testing.T, state StateType) {
et := 10
r := newTestRaft(1, et, 1, newTestMemoryStorage(withPeers(1, 2, 3)))
timeouts := make(map[int]bool)
for round := 0; round < 50*et; round++ {
switch state {
case StateFollower:
r.becomeFollower(r.Term+1, 2)
case StateCandidate:
r.becomeCandidate()
}
time := 0
for len(r.readMessages()) == 0 {
r.tick()
time++
}
timeouts[time] = true
}
for d := et + 1; d < 2*et; d++ {
if !timeouts[d] {
t.Errorf("timeout in %d ticks should happen", d)
}
}
}
func TestFollowersElectionTimeoutNonconflict(t *testing.T) {
SetLogger(discardLogger)
defer SetLogger(defaultLogger)
testNonleadersElectionTimeoutNonconflict(t, StateFollower)
}
func TestCandidatesElectionTimeoutNonconflict(t *testing.T) {
SetLogger(discardLogger)
defer SetLogger(defaultLogger)
testNonleadersElectionTimeoutNonconflict(t, StateCandidate)
}
// testNonleadersElectionTimeoutNonconflict tests that in most cases only a
// single server(follower or candidate) will time out, which reduces the
// likelihood of split vote in the new election.
// Reference: section 5.2
func testNonleadersElectionTimeoutNonconflict(t *testing.T, state StateType) {
et := 10
size := 5
rs := make([]*raft, size)
ids := idsBySize(size)
for k := range rs {
rs[k] = newTestRaft(ids[k], et, 1, newTestMemoryStorage(withPeers(ids...)))
}
conflicts := 0
for round := 0; round < 1000; round++ {
for _, r := range rs {
switch state {
case StateFollower:
r.becomeFollower(r.Term+1, None)
case StateCandidate:
r.becomeCandidate()
}
}
timeoutNum := 0
for timeoutNum == 0 {
for _, r := range rs {
r.tick()
if len(r.readMessages()) > 0 {
timeoutNum++
}
}
}
// several rafts time out at the same tick
if timeoutNum > 1 {
conflicts++
}
}
if g := float64(conflicts) / 1000; g > 0.3 {
t.Errorf("probability of conflicts = %v, want <= 0.3", g)
}
}
// TestLeaderStartReplication tests that when receiving client proposals,
// the leader appends the proposal to its log as a new entry, then issues
// AppendEntries RPCs in parallel to each of the other servers to replicate
// the entry. Also, when sending an AppendEntries RPC, the leader includes
// the index and term of the entry in its log that immediately precedes
// the new entries.
// Also, it writes the new entry into stable storage.
// Reference: section 5.3
func TestLeaderStartReplication(t *testing.T) {
s := newTestMemoryStorage(withPeers(1, 2, 3))
r := newTestRaft(1, 10, 1, s)
r.becomeCandidate()
r.becomeLeader()
commitNoopEntry(r, s)
li := r.raftLog.lastIndex()
ents := []pb.Entry{{Data: []byte("some data")}}
r.Step(pb.Message{From: 1, To: 1, Type: pb.MsgProp, Entries: ents})
if g := r.raftLog.lastIndex(); g != li+1 {
t.Errorf("lastIndex = %d, want %d", g, li+1)
}
if g := r.raftLog.committed; g != li {
t.Errorf("committed = %d, want %d", g, li)
}
msgs := r.readMessages()
sort.Sort(messageSlice(msgs))
wents := []pb.Entry{{Index: li + 1, Term: 1, Data: []byte("some data")}}
wmsgs := []pb.Message{
{From: 1, To: 2, Term: 1, Type: pb.MsgApp, Index: li, LogTerm: 1, Entries: wents, Commit: li},
{From: 1, To: 3, Term: 1, Type: pb.MsgApp, Index: li, LogTerm: 1, Entries: wents, Commit: li},
}
if !reflect.DeepEqual(msgs, wmsgs) {
t.Errorf("msgs = %+v, want %+v", msgs, wmsgs)
}
if g := r.raftLog.unstableEntries(); !reflect.DeepEqual(g, wents) {
t.Errorf("ents = %+v, want %+v", g, wents)
}
}
// TestLeaderCommitEntry tests that when the entry has been safely replicated,
// the leader gives out the applied entries, which can be applied to its state
// machine.
// Also, the leader keeps track of the highest index it knows to be committed,
// and it includes that index in future AppendEntries RPCs so that the other
// servers eventually find out.
// Reference: section 5.3
func TestLeaderCommitEntry(t *testing.T) {
s := newTestMemoryStorage(withPeers(1, 2, 3))
r := newTestRaft(1, 10, 1, s)
r.becomeCandidate()
r.becomeLeader()
commitNoopEntry(r, s)
li := r.raftLog.lastIndex()
r.Step(pb.Message{From: 1, To: 1, Type: pb.MsgProp, Entries: []pb.Entry{{Data: []byte("some data")}}})
for _, m := range r.readMessages() {
r.Step(acceptAndReply(m))
}
if g := r.raftLog.committed; g != li+1 {
t.Errorf("committed = %d, want %d", g, li+1)
}
wents := []pb.Entry{{Index: li + 1, Term: 1, Data: []byte("some data")}}
if g := r.raftLog.nextEnts(); !reflect.DeepEqual(g, wents) {
t.Errorf("nextEnts = %+v, want %+v", g, wents)
}
msgs := r.readMessages()
sort.Sort(messageSlice(msgs))
for i, m := range msgs {
if w := uint64(i + 2); m.To != w {
t.Errorf("to = %x, want %x", m.To, w)
}
if m.Type != pb.MsgApp {
t.Errorf("type = %v, want %v", m.Type, pb.MsgApp)
}
if m.Commit != li+1 {
t.Errorf("commit = %d, want %d", m.Commit, li+1)
}
}
}
// TestLeaderAcknowledgeCommit tests that a log entry is committed once the
// leader that created the entry has replicated it on a majority of the servers.
// Reference: section 5.3
func TestLeaderAcknowledgeCommit(t *testing.T) {
tests := []struct {
size int
acceptors map[uint64]bool
wack bool
}{
{1, nil, true},
{3, nil, false},
{3, map[uint64]bool{2: true}, true},
{3, map[uint64]bool{2: true, 3: true}, true},
{5, nil, false},
{5, map[uint64]bool{2: true}, false},
{5, map[uint64]bool{2: true, 3: true}, true},
{5, map[uint64]bool{2: true, 3: true, 4: true}, true},
{5, map[uint64]bool{2: true, 3: true, 4: true, 5: true}, true},
}
for i, tt := range tests {
s := newTestMemoryStorage(withPeers(idsBySize(tt.size)...))
r := newTestRaft(1, 10, 1, s)
r.becomeCandidate()
r.becomeLeader()
commitNoopEntry(r, s)
li := r.raftLog.lastIndex()
r.Step(pb.Message{From: 1, To: 1, Type: pb.MsgProp, Entries: []pb.Entry{{Data: []byte("some data")}}})
for _, m := range r.readMessages() {
if tt.acceptors[m.To] {
r.Step(acceptAndReply(m))
}
}
if g := r.raftLog.committed > li; g != tt.wack {
t.Errorf("#%d: ack commit = %v, want %v", i, g, tt.wack)
}
}
}
// TestLeaderCommitPrecedingEntries tests that when leader commits a log entry,
// it also commits all preceding entries in the leaders log, including
// entries created by previous leaders.
// Also, it applies the entry to its local state machine (in log order).
// Reference: section 5.3
func TestLeaderCommitPrecedingEntries(t *testing.T) {
tests := [][]pb.Entry{
{},
{{Term: 2, Index: 1}},
{{Term: 1, Index: 1}, {Term: 2, Index: 2}},
{{Term: 1, Index: 1}},
}
for i, tt := range tests {
storage := newTestMemoryStorage(withPeers(1, 2, 3))
storage.Append(tt)
r := newTestRaft(1, 10, 1, storage)
r.loadState(pb.HardState{Term: 2})
r.becomeCandidate()
r.becomeLeader()
r.Step(pb.Message{From: 1, To: 1, Type: pb.MsgProp, Entries: []pb.Entry{{Data: []byte("some data")}}})
for _, m := range r.readMessages() {
r.Step(acceptAndReply(m))
}
li := uint64(len(tt))
wents := append(tt, pb.Entry{Term: 3, Index: li + 1}, pb.Entry{Term: 3, Index: li + 2, Data: []byte("some data")})
if g := r.raftLog.nextEnts(); !reflect.DeepEqual(g, wents) {
t.Errorf("#%d: ents = %+v, want %+v", i, g, wents)
}
}
}
// TestFollowerCommitEntry tests that once a follower learns that a log entry
// is committed, it applies the entry to its local state machine (in log order).
// Reference: section 5.3
func TestFollowerCommitEntry(t *testing.T) {
tests := []struct {
ents []pb.Entry
commit uint64
}{
{
[]pb.Entry{
{Term: 1, Index: 1, Data: []byte("some data")},
},
1,
},
{
[]pb.Entry{
{Term: 1, Index: 1, Data: []byte("some data")},
{Term: 1, Index: 2, Data: []byte("some data2")},
},
2,
},
{
[]pb.Entry{
{Term: 1, Index: 1, Data: []byte("some data2")},
{Term: 1, Index: 2, Data: []byte("some data")},
},
2,
},
{
[]pb.Entry{
{Term: 1, Index: 1, Data: []byte("some data")},
{Term: 1, Index: 2, Data: []byte("some data2")},
},
1,
},
}
for i, tt := range tests {
r := newTestRaft(1, 10, 1, newTestMemoryStorage(withPeers(1, 2, 3)))
r.becomeFollower(1, 2)
r.Step(pb.Message{From: 2, To: 1, Type: pb.MsgApp, Term: 1, Entries: tt.ents, Commit: tt.commit})
if g := r.raftLog.committed; g != tt.commit {
t.Errorf("#%d: committed = %d, want %d", i, g, tt.commit)
}
wents := tt.ents[:int(tt.commit)]
if g := r.raftLog.nextEnts(); !reflect.DeepEqual(g, wents) {
t.Errorf("#%d: nextEnts = %v, want %v", i, g, wents)
}
}
}
// TestFollowerCheckMsgApp tests that if the follower does not find an
// entry in its log with the same index and term as the one in AppendEntries RPC,
// then it refuses the new entries. Otherwise it replies that it accepts the
// append entries.
// Reference: section 5.3
func TestFollowerCheckMsgApp(t *testing.T) {
ents := []pb.Entry{{Term: 1, Index: 1}, {Term: 2, Index: 2}}
tests := []struct {
term uint64
index uint64
windex uint64
wreject bool
wrejectHint uint64
wlogterm uint64
}{
// match with committed entries
{0, 0, 1, false, 0, 0},
{ents[0].Term, ents[0].Index, 1, false, 0, 0},
// match with uncommitted entries
{ents[1].Term, ents[1].Index, 2, false, 0, 0},
// unmatch with existing entry
{ents[0].Term, ents[1].Index, ents[1].Index, true, 1, 1},
// unexisting entry
{ents[1].Term + 1, ents[1].Index + 1, ents[1].Index + 1, true, 2, 2},
}
for i, tt := range tests {
storage := newTestMemoryStorage(withPeers(1, 2, 3))
storage.Append(ents)
r := newTestRaft(1, 10, 1, storage)
r.loadState(pb.HardState{Commit: 1})
r.becomeFollower(2, 2)
r.Step(pb.Message{From: 2, To: 1, Type: pb.MsgApp, Term: 2, LogTerm: tt.term, Index: tt.index})
msgs := r.readMessages()
wmsgs := []pb.Message{
{From: 1, To: 2, Type: pb.MsgAppResp, Term: 2, Index: tt.windex, Reject: tt.wreject, RejectHint: tt.wrejectHint, LogTerm: tt.wlogterm},
}
if !reflect.DeepEqual(msgs, wmsgs) {
t.Errorf("#%d: msgs = %+v, want %+v", i, msgs, wmsgs)
}
}
}
// TestFollowerAppendEntries tests that when AppendEntries RPC is valid,
// the follower will delete the existing conflict entry and all that follow it,
// and append any new entries not already in the log.
// Also, it writes the new entry into stable storage.
// Reference: section 5.3
func TestFollowerAppendEntries(t *testing.T) {
tests := []struct {
index, term uint64
ents []pb.Entry
wents []pb.Entry
wunstable []pb.Entry
}{
{
2, 2,
[]pb.Entry{{Term: 3, Index: 3}},
[]pb.Entry{{Term: 1, Index: 1}, {Term: 2, Index: 2}, {Term: 3, Index: 3}},
[]pb.Entry{{Term: 3, Index: 3}},
},
{
1, 1,
[]pb.Entry{{Term: 3, Index: 2}, {Term: 4, Index: 3}},
[]pb.Entry{{Term: 1, Index: 1}, {Term: 3, Index: 2}, {Term: 4, Index: 3}},
[]pb.Entry{{Term: 3, Index: 2}, {Term: 4, Index: 3}},
},
{
0, 0,
[]pb.Entry{{Term: 1, Index: 1}},
[]pb.Entry{{Term: 1, Index: 1}, {Term: 2, Index: 2}},
nil,
},
{
0, 0,
[]pb.Entry{{Term: 3, Index: 1}},
[]pb.Entry{{Term: 3, Index: 1}},
[]pb.Entry{{Term: 3, Index: 1}},
},
}
for i, tt := range tests {
storage := newTestMemoryStorage(withPeers(1, 2, 3))
storage.Append([]pb.Entry{{Term: 1, Index: 1}, {Term: 2, Index: 2}})
r := newTestRaft(1, 10, 1, storage)
r.becomeFollower(2, 2)
r.Step(pb.Message{From: 2, To: 1, Type: pb.MsgApp, Term: 2, LogTerm: tt.term, Index: tt.index, Entries: tt.ents})
if g := r.raftLog.allEntries(); !reflect.DeepEqual(g, tt.wents) {
t.Errorf("#%d: ents = %+v, want %+v", i, g, tt.wents)
}
if g := r.raftLog.unstableEntries(); !reflect.DeepEqual(g, tt.wunstable) {
t.Errorf("#%d: unstableEnts = %+v, want %+v", i, g, tt.wunstable)
}
}
}
// TestLeaderSyncFollowerLog tests that the leader could bring a follower's log
// into consistency with its own.
// Reference: section 5.3, figure 7
func TestLeaderSyncFollowerLog(t *testing.T) {
ents := []pb.Entry{
{},
{Term: 1, Index: 1}, {Term: 1, Index: 2}, {Term: 1, Index: 3},
{Term: 4, Index: 4}, {Term: 4, Index: 5},
{Term: 5, Index: 6}, {Term: 5, Index: 7},
{Term: 6, Index: 8}, {Term: 6, Index: 9}, {Term: 6, Index: 10},
}
term := uint64(8)
tests := [][]pb.Entry{
{
{},
{Term: 1, Index: 1}, {Term: 1, Index: 2}, {Term: 1, Index: 3},
{Term: 4, Index: 4}, {Term: 4, Index: 5},
{Term: 5, Index: 6}, {Term: 5, Index: 7},
{Term: 6, Index: 8}, {Term: 6, Index: 9},
},
{
{},
{Term: 1, Index: 1}, {Term: 1, Index: 2}, {Term: 1, Index: 3},
{Term: 4, Index: 4},
},
{
{},
{Term: 1, Index: 1}, {Term: 1, Index: 2}, {Term: 1, Index: 3},
{Term: 4, Index: 4}, {Term: 4, Index: 5},
{Term: 5, Index: 6}, {Term: 5, Index: 7},
{Term: 6, Index: 8}, {Term: 6, Index: 9}, {Term: 6, Index: 10}, {Term: 6, Index: 11},
},
{
{},
{Term: 1, Index: 1}, {Term: 1, Index: 2}, {Term: 1, Index: 3},
{Term: 4, Index: 4}, {Term: 4, Index: 5},
{Term: 5, Index: 6}, {Term: 5, Index: 7},
{Term: 6, Index: 8}, {Term: 6, Index: 9}, {Term: 6, Index: 10},
{Term: 7, Index: 11}, {Term: 7, Index: 12},
},
{
{},
{Term: 1, Index: 1}, {Term: 1, Index: 2}, {Term: 1, Index: 3},
{Term: 4, Index: 4}, {Term: 4, Index: 5}, {Term: 4, Index: 6}, {Term: 4, Index: 7},
},
{
{},
{Term: 1, Index: 1}, {Term: 1, Index: 2}, {Term: 1, Index: 3},
{Term: 2, Index: 4}, {Term: 2, Index: 5}, {Term: 2, Index: 6},
{Term: 3, Index: 7}, {Term: 3, Index: 8}, {Term: 3, Index: 9}, {Term: 3, Index: 10}, {Term: 3, Index: 11},
},
}
for i, tt := range tests {
leadStorage := newTestMemoryStorage(withPeers(1, 2, 3))
leadStorage.Append(ents)
lead := newTestRaft(1, 10, 1, leadStorage)
lead.loadState(pb.HardState{Commit: lead.raftLog.lastIndex(), Term: term})
followerStorage := newTestMemoryStorage(withPeers(1, 2, 3))
followerStorage.Append(tt)
follower := newTestRaft(2, 10, 1, followerStorage)
follower.loadState(pb.HardState{Term: term - 1})
// It is necessary to have a three-node cluster.
// The second may have more up-to-date log than the first one, so the
// first node needs the vote from the third node to become the leader.
n := newNetwork(lead, follower, nopStepper)
n.send(pb.Message{From: 1, To: 1, Type: pb.MsgHup})
// The election occurs in the term after the one we loaded with
// lead.loadState above.
n.send(pb.Message{From: 3, To: 1, Type: pb.MsgVoteResp, Term: term + 1})
n.send(pb.Message{From: 1, To: 1, Type: pb.MsgProp, Entries: []pb.Entry{{}}})
if g := diffu(ltoa(lead.raftLog), ltoa(follower.raftLog)); g != "" {
t.Errorf("#%d: log diff:\n%s", i, g)
}
}
}
// TestVoteRequest tests that the vote request includes information about the candidates log
// and are sent to all of the other nodes.
// Reference: section 5.4.1
func TestVoteRequest(t *testing.T) {
tests := []struct {
ents []pb.Entry
wterm uint64
}{
{[]pb.Entry{{Term: 1, Index: 1}}, 2},
{[]pb.Entry{{Term: 1, Index: 1}, {Term: 2, Index: 2}}, 3},
}
for j, tt := range tests {
r := newTestRaft(1, 10, 1, newTestMemoryStorage(withPeers(1, 2, 3)))
r.Step(pb.Message{
From: 2, To: 1, Type: pb.MsgApp, Term: tt.wterm - 1, LogTerm: 0, Index: 0, Entries: tt.ents,
})
r.readMessages()
for i := 1; i < r.electionTimeout*2; i++ {
r.tickElection()
}
msgs := r.readMessages()
sort.Sort(messageSlice(msgs))
if len(msgs) != 2 {
t.Fatalf("#%d: len(msg) = %d, want %d", j, len(msgs), 2)
}
for i, m := range msgs {
if m.Type != pb.MsgVote {
t.Errorf("#%d: msgType = %d, want %d", i, m.Type, pb.MsgVote)
}
if m.To != uint64(i+2) {
t.Errorf("#%d: to = %d, want %d", i, m.To, i+2)
}
if m.Term != tt.wterm {
t.Errorf("#%d: term = %d, want %d", i, m.Term, tt.wterm)
}
windex, wlogterm := tt.ents[len(tt.ents)-1].Index, tt.ents[len(tt.ents)-1].Term
if m.Index != windex {
t.Errorf("#%d: index = %d, want %d", i, m.Index, windex)
}
if m.LogTerm != wlogterm {
t.Errorf("#%d: logterm = %d, want %d", i, m.LogTerm, wlogterm)
}
}
}
}
// TestVoter tests the voter denies its vote if its own log is more up-to-date
// than that of the candidate.
// Reference: section 5.4.1
func TestVoter(t *testing.T) {
tests := []struct {
ents []pb.Entry
logterm uint64
index uint64
wreject bool
}{
// same logterm
{[]pb.Entry{{Term: 1, Index: 1}}, 1, 1, false},
{[]pb.Entry{{Term: 1, Index: 1}}, 1, 2, false},
{[]pb.Entry{{Term: 1, Index: 1}, {Term: 1, Index: 2}}, 1, 1, true},
// candidate higher logterm
{[]pb.Entry{{Term: 1, Index: 1}}, 2, 1, false},
{[]pb.Entry{{Term: 1, Index: 1}}, 2, 2, false},
{[]pb.Entry{{Term: 1, Index: 1}, {Term: 1, Index: 2}}, 2, 1, false},
// voter higher logterm
{[]pb.Entry{{Term: 2, Index: 1}}, 1, 1, true},
{[]pb.Entry{{Term: 2, Index: 1}}, 1, 2, true},
{[]pb.Entry{{Term: 2, Index: 1}, {Term: 1, Index: 2}}, 1, 1, true},
}
for i, tt := range tests {
storage := newTestMemoryStorage(withPeers(1, 2))
storage.Append(tt.ents)
r := newTestRaft(1, 10, 1, storage)
r.Step(pb.Message{From: 2, To: 1, Type: pb.MsgVote, Term: 3, LogTerm: tt.logterm, Index: tt.index})
msgs := r.readMessages()
if len(msgs) != 1 {
t.Fatalf("#%d: len(msg) = %d, want %d", i, len(msgs), 1)
}
m := msgs[0]
if m.Type != pb.MsgVoteResp {
t.Errorf("#%d: msgType = %d, want %d", i, m.Type, pb.MsgVoteResp)
}
if m.Reject != tt.wreject {
t.Errorf("#%d: reject = %t, want %t", i, m.Reject, tt.wreject)
}
}
}
// TestLeaderOnlyCommitsLogFromCurrentTerm tests that only log entries from the leaders
// current term are committed by counting replicas.
// Reference: section 5.4.2
func TestLeaderOnlyCommitsLogFromCurrentTerm(t *testing.T) {
ents := []pb.Entry{{Term: 1, Index: 1}, {Term: 2, Index: 2}}
tests := []struct {
index uint64
wcommit uint64
}{
// do not commit log entries in previous terms
{1, 0},
{2, 0},
// commit log in current term
{3, 3},
}
for i, tt := range tests {
storage := newTestMemoryStorage(withPeers(1, 2))
storage.Append(ents)
r := newTestRaft(1, 10, 1, storage)
r.loadState(pb.HardState{Term: 2})
// become leader at term 3
r.becomeCandidate()
r.becomeLeader()
r.readMessages()
// propose a entry to current term
r.Step(pb.Message{From: 1, To: 1, Type: pb.MsgProp, Entries: []pb.Entry{{}}})
r.Step(pb.Message{From: 2, To: 1, Type: pb.MsgAppResp, Term: r.Term, Index: tt.index})
if r.raftLog.committed != tt.wcommit {
t.Errorf("#%d: commit = %d, want %d", i, r.raftLog.committed, tt.wcommit)
}
}
}
type messageSlice []pb.Message
func (s messageSlice) Len() int { return len(s) }
func (s messageSlice) Less(i, j int) bool { return fmt.Sprint(s[i]) < fmt.Sprint(s[j]) }
func (s messageSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func commitNoopEntry(r *raft, s *MemoryStorage) {
if r.state != StateLeader {
panic("it should only be used when it is the leader")
}
r.bcastAppend()
// simulate the response of MsgApp
msgs := r.readMessages()
for _, m := range msgs {
if m.Type != pb.MsgApp || len(m.Entries) != 1 || m.Entries[0].Data != nil {
panic("not a message to append noop entry")
}
r.Step(acceptAndReply(m))
}
// ignore further messages to refresh followers' commit index
r.readMessages()
s.Append(r.raftLog.unstableEntries())
r.raftLog.appliedTo(r.raftLog.committed)
r.raftLog.stableTo(r.raftLog.lastIndex(), r.raftLog.lastTerm())
}
func acceptAndReply(m pb.Message) pb.Message {
if m.Type != pb.MsgApp {
panic("type should be MsgApp")
}
return pb.Message{
From: m.To,
To: m.From,
Term: m.Term,
Type: pb.MsgAppResp,
Index: m.Index + uint64(len(m.Entries)),
}
}

View File

@ -1,141 +0,0 @@
// Copyright 2015 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package raft
import (
"testing"
pb "go.etcd.io/etcd/raft/v3/raftpb"
)
var (
testingSnap = pb.Snapshot{
Metadata: pb.SnapshotMetadata{
Index: 11, // magic number
Term: 11, // magic number
ConfState: pb.ConfState{Voters: []uint64{1, 2}},
},
}
)
func TestSendingSnapshotSetPendingSnapshot(t *testing.T) {
storage := newTestMemoryStorage(withPeers(1))
sm := newTestRaft(1, 10, 1, storage)
sm.restore(testingSnap)
sm.becomeCandidate()
sm.becomeLeader()
// force set the next of node 2, so that
// node 2 needs a snapshot
sm.prs.Progress[2].Next = sm.raftLog.firstIndex()
sm.Step(pb.Message{From: 2, To: 1, Type: pb.MsgAppResp, Index: sm.prs.Progress[2].Next - 1, Reject: true})
if sm.prs.Progress[2].PendingSnapshot != 11 {
t.Fatalf("PendingSnapshot = %d, want 11", sm.prs.Progress[2].PendingSnapshot)
}
}
func TestPendingSnapshotPauseReplication(t *testing.T) {
storage := newTestMemoryStorage(withPeers(1, 2))
sm := newTestRaft(1, 10, 1, storage)
sm.restore(testingSnap)
sm.becomeCandidate()
sm.becomeLeader()
sm.prs.Progress[2].BecomeSnapshot(11)
sm.Step(pb.Message{From: 1, To: 1, Type: pb.MsgProp, Entries: []pb.Entry{{Data: []byte("somedata")}}})
msgs := sm.readMessages()
if len(msgs) != 0 {
t.Fatalf("len(msgs) = %d, want 0", len(msgs))
}
}
func TestSnapshotFailure(t *testing.T) {
storage := newTestMemoryStorage(withPeers(1, 2))
sm := newTestRaft(1, 10, 1, storage)
sm.restore(testingSnap)
sm.becomeCandidate()
sm.becomeLeader()
sm.prs.Progress[2].Next = 1
sm.prs.Progress[2].BecomeSnapshot(11)
sm.Step(pb.Message{From: 2, To: 1, Type: pb.MsgSnapStatus, Reject: true})
if sm.prs.Progress[2].PendingSnapshot != 0 {
t.Fatalf("PendingSnapshot = %d, want 0", sm.prs.Progress[2].PendingSnapshot)
}
if sm.prs.Progress[2].Next != 1 {
t.Fatalf("Next = %d, want 1", sm.prs.Progress[2].Next)
}
if !sm.prs.Progress[2].ProbeSent {
t.Errorf("ProbeSent = %v, want true", sm.prs.Progress[2].ProbeSent)
}
}
func TestSnapshotSucceed(t *testing.T) {
storage := newTestMemoryStorage(withPeers(1, 2))
sm := newTestRaft(1, 10, 1, storage)
sm.restore(testingSnap)
sm.becomeCandidate()
sm.becomeLeader()
sm.prs.Progress[2].Next = 1
sm.prs.Progress[2].BecomeSnapshot(11)
sm.Step(pb.Message{From: 2, To: 1, Type: pb.MsgSnapStatus, Reject: false})
if sm.prs.Progress[2].PendingSnapshot != 0 {
t.Fatalf("PendingSnapshot = %d, want 0", sm.prs.Progress[2].PendingSnapshot)
}
if sm.prs.Progress[2].Next != 12 {
t.Fatalf("Next = %d, want 12", sm.prs.Progress[2].Next)
}
if !sm.prs.Progress[2].ProbeSent {
t.Errorf("ProbeSent = %v, want true", sm.prs.Progress[2].ProbeSent)
}
}
func TestSnapshotAbort(t *testing.T) {
storage := newTestMemoryStorage(withPeers(1, 2))
sm := newTestRaft(1, 10, 1, storage)
sm.restore(testingSnap)
sm.becomeCandidate()
sm.becomeLeader()
sm.prs.Progress[2].Next = 1
sm.prs.Progress[2].BecomeSnapshot(11)
// A successful msgAppResp that has a higher/equal index than the
// pending snapshot should abort the pending snapshot.
sm.Step(pb.Message{From: 2, To: 1, Type: pb.MsgAppResp, Index: 11})
if sm.prs.Progress[2].PendingSnapshot != 0 {
t.Fatalf("PendingSnapshot = %d, want 0", sm.prs.Progress[2].PendingSnapshot)
}
// The follower entered StateReplicate and the leader send an append
// and optimistically updated the progress (so we see 13 instead of 12).
// There is something to append because the leader appended an empty entry
// to the log at index 12 when it assumed leadership.
if sm.prs.Progress[2].Next != 13 {
t.Fatalf("Next = %d, want 13", sm.prs.Progress[2].Next)
}
if n := sm.prs.Progress[2].Inflights.Count(); n != 1 {
t.Fatalf("expected an inflight message, got %d", n)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,58 +0,0 @@
// Copyright 2019 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package raftpb
import (
"testing"
)
func TestConfState_Equivalent(t *testing.T) {
type testCase struct {
cs, cs2 ConfState
ok bool
}
testCases := []testCase{
// Reordered voters and learners.
{ConfState{
Voters: []uint64{1, 2, 3},
Learners: []uint64{5, 4, 6},
VotersOutgoing: []uint64{9, 8, 7},
LearnersNext: []uint64{10, 20, 15},
}, ConfState{
Voters: []uint64{1, 2, 3},
Learners: []uint64{4, 5, 6},
VotersOutgoing: []uint64{7, 9, 8},
LearnersNext: []uint64{20, 10, 15},
}, true},
// Not sensitive to nil vs empty slice.
{ConfState{Voters: []uint64{}}, ConfState{Voters: []uint64(nil)}, true},
// Non-equivalent voters.
{ConfState{Voters: []uint64{1, 2, 3, 4}}, ConfState{Voters: []uint64{2, 1, 3}}, false},
{ConfState{Voters: []uint64{1, 4, 3}}, ConfState{Voters: []uint64{2, 1, 3}}, false},
// Non-equivalent learners.
{ConfState{Voters: []uint64{1, 2, 3, 4}}, ConfState{Voters: []uint64{2, 1, 3}}, false},
// Sensitive to AutoLeave flag.
{ConfState{AutoLeave: true}, ConfState{}, false},
}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
if err := tc.cs.Equivalent(tc.cs2); (err == nil) != tc.ok {
t.Fatalf("wanted error: %t, got:\n%s", tc.ok, err)
}
})
}
}

View File

@ -1,64 +0,0 @@
// Copyright 2021 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package raftpb
import (
"math/bits"
"testing"
"unsafe"
)
func TestProtoMemorySizes(t *testing.T) {
assert := func(size, exp uintptr, name string) {
t.Helper()
if size != exp {
t.Errorf("expected size of %s proto to be %d bytes, found %d bytes", name, exp, size)
}
}
if64Bit := func(yes, no uintptr) uintptr {
if bits.UintSize == 64 {
return yes
}
return no
}
var e Entry
assert(unsafe.Sizeof(e), if64Bit(48, 32), "Entry")
var sm SnapshotMetadata
assert(unsafe.Sizeof(sm), if64Bit(120, 68), "SnapshotMetadata")
var s Snapshot
assert(unsafe.Sizeof(s), if64Bit(144, 80), "Snapshot")
var m Message
assert(unsafe.Sizeof(m), if64Bit(264, 168), "Message")
var hs HardState
assert(unsafe.Sizeof(hs), 24, "HardState")
var cs ConfState
assert(unsafe.Sizeof(cs), if64Bit(104, 52), "ConfState")
var cc ConfChange
assert(unsafe.Sizeof(cc), if64Bit(48, 32), "ConfChange")
var ccs ConfChangeSingle
assert(unsafe.Sizeof(ccs), if64Bit(16, 12), "ConfChangeSingle")
var ccv2 ConfChangeV2
assert(unsafe.Sizeof(ccv2), if64Bit(56, 28), "ConfChangeV2")
}

File diff suppressed because it is too large Load Diff

View File

@ -1,290 +0,0 @@
// Copyright 2015 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package raft
import (
"math"
"reflect"
"testing"
pb "go.etcd.io/etcd/raft/v3/raftpb"
)
func TestStorageTerm(t *testing.T) {
ents := []pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 4}, {Index: 5, Term: 5}}
tests := []struct {
i uint64
werr error
wterm uint64
wpanic bool
}{
{2, ErrCompacted, 0, false},
{3, nil, 3, false},
{4, nil, 4, false},
{5, nil, 5, false},
{6, ErrUnavailable, 0, false},
}
for i, tt := range tests {
s := &MemoryStorage{ents: ents}
func() {
defer func() {
if r := recover(); r != nil {
if !tt.wpanic {
t.Errorf("%d: panic = %v, want %v", i, true, tt.wpanic)
}
}
}()
term, err := s.Term(tt.i)
if err != tt.werr {
t.Errorf("#%d: err = %v, want %v", i, err, tt.werr)
}
if term != tt.wterm {
t.Errorf("#%d: term = %d, want %d", i, term, tt.wterm)
}
}()
}
}
func TestStorageEntries(t *testing.T) {
ents := []pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 4}, {Index: 5, Term: 5}, {Index: 6, Term: 6}}
tests := []struct {
lo, hi, maxsize uint64
werr error
wentries []pb.Entry
}{
{2, 6, math.MaxUint64, ErrCompacted, nil},
{3, 4, math.MaxUint64, ErrCompacted, nil},
{4, 5, math.MaxUint64, nil, []pb.Entry{{Index: 4, Term: 4}}},
{4, 6, math.MaxUint64, nil, []pb.Entry{{Index: 4, Term: 4}, {Index: 5, Term: 5}}},
{4, 7, math.MaxUint64, nil, []pb.Entry{{Index: 4, Term: 4}, {Index: 5, Term: 5}, {Index: 6, Term: 6}}},
// even if maxsize is zero, the first entry should be returned
{4, 7, 0, nil, []pb.Entry{{Index: 4, Term: 4}}},
// limit to 2
{4, 7, uint64(ents[1].Size() + ents[2].Size()), nil, []pb.Entry{{Index: 4, Term: 4}, {Index: 5, Term: 5}}},
// limit to 2
{4, 7, uint64(ents[1].Size() + ents[2].Size() + ents[3].Size()/2), nil, []pb.Entry{{Index: 4, Term: 4}, {Index: 5, Term: 5}}},
{4, 7, uint64(ents[1].Size() + ents[2].Size() + ents[3].Size() - 1), nil, []pb.Entry{{Index: 4, Term: 4}, {Index: 5, Term: 5}}},
// all
{4, 7, uint64(ents[1].Size() + ents[2].Size() + ents[3].Size()), nil, []pb.Entry{{Index: 4, Term: 4}, {Index: 5, Term: 5}, {Index: 6, Term: 6}}},
}
for i, tt := range tests {
s := &MemoryStorage{ents: ents}
entries, err := s.Entries(tt.lo, tt.hi, tt.maxsize)
if err != tt.werr {
t.Errorf("#%d: err = %v, want %v", i, err, tt.werr)
}
if !reflect.DeepEqual(entries, tt.wentries) {
t.Errorf("#%d: entries = %v, want %v", i, entries, tt.wentries)
}
}
}
func TestStorageLastIndex(t *testing.T) {
ents := []pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 4}, {Index: 5, Term: 5}}
s := &MemoryStorage{ents: ents}
last, err := s.LastIndex()
if err != nil {
t.Errorf("err = %v, want nil", err)
}
if last != 5 {
t.Errorf("last = %d, want %d", last, 5)
}
s.Append([]pb.Entry{{Index: 6, Term: 5}})
last, err = s.LastIndex()
if err != nil {
t.Errorf("err = %v, want nil", err)
}
if last != 6 {
t.Errorf("last = %d, want %d", last, 6)
}
}
func TestStorageFirstIndex(t *testing.T) {
ents := []pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 4}, {Index: 5, Term: 5}}
s := &MemoryStorage{ents: ents}
first, err := s.FirstIndex()
if err != nil {
t.Errorf("err = %v, want nil", err)
}
if first != 4 {
t.Errorf("first = %d, want %d", first, 4)
}
s.Compact(4)
first, err = s.FirstIndex()
if err != nil {
t.Errorf("err = %v, want nil", err)
}
if first != 5 {
t.Errorf("first = %d, want %d", first, 5)
}
}
func TestStorageCompact(t *testing.T) {
ents := []pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 4}, {Index: 5, Term: 5}}
tests := []struct {
i uint64
werr error
windex uint64
wterm uint64
wlen int
}{
{2, ErrCompacted, 3, 3, 3},
{3, ErrCompacted, 3, 3, 3},
{4, nil, 4, 4, 2},
{5, nil, 5, 5, 1},
}
for i, tt := range tests {
s := &MemoryStorage{ents: ents}
err := s.Compact(tt.i)
if err != tt.werr {
t.Errorf("#%d: err = %v, want %v", i, err, tt.werr)
}
if s.ents[0].Index != tt.windex {
t.Errorf("#%d: index = %d, want %d", i, s.ents[0].Index, tt.windex)
}
if s.ents[0].Term != tt.wterm {
t.Errorf("#%d: term = %d, want %d", i, s.ents[0].Term, tt.wterm)
}
if len(s.ents) != tt.wlen {
t.Errorf("#%d: len = %d, want %d", i, len(s.ents), tt.wlen)
}
}
}
func TestStorageCreateSnapshot(t *testing.T) {
ents := []pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 4}, {Index: 5, Term: 5}}
cs := &pb.ConfState{Voters: []uint64{1, 2, 3}}
data := []byte("data")
tests := []struct {
i uint64
werr error
wsnap pb.Snapshot
}{
{4, nil, pb.Snapshot{Data: data, Metadata: pb.SnapshotMetadata{Index: 4, Term: 4, ConfState: *cs}}},
{5, nil, pb.Snapshot{Data: data, Metadata: pb.SnapshotMetadata{Index: 5, Term: 5, ConfState: *cs}}},
}
for i, tt := range tests {
s := &MemoryStorage{ents: ents}
snap, err := s.CreateSnapshot(tt.i, cs, data)
if err != tt.werr {
t.Errorf("#%d: err = %v, want %v", i, err, tt.werr)
}
if !reflect.DeepEqual(snap, tt.wsnap) {
t.Errorf("#%d: snap = %+v, want %+v", i, snap, tt.wsnap)
}
}
}
func TestStorageAppend(t *testing.T) {
ents := []pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 4}, {Index: 5, Term: 5}}
tests := []struct {
entries []pb.Entry
werr error
wentries []pb.Entry
}{
{
[]pb.Entry{{Index: 1, Term: 1}, {Index: 2, Term: 2}},
nil,
[]pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 4}, {Index: 5, Term: 5}},
},
{
[]pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 4}, {Index: 5, Term: 5}},
nil,
[]pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 4}, {Index: 5, Term: 5}},
},
{
[]pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 6}, {Index: 5, Term: 6}},
nil,
[]pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 6}, {Index: 5, Term: 6}},
},
{
[]pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 4}, {Index: 5, Term: 5}, {Index: 6, Term: 5}},
nil,
[]pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 4}, {Index: 5, Term: 5}, {Index: 6, Term: 5}},
},
// truncate incoming entries, truncate the existing entries and append
{
[]pb.Entry{{Index: 2, Term: 3}, {Index: 3, Term: 3}, {Index: 4, Term: 5}},
nil,
[]pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 5}},
},
// truncate the existing entries and append
{
[]pb.Entry{{Index: 4, Term: 5}},
nil,
[]pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 5}},
},
// direct append
{
[]pb.Entry{{Index: 6, Term: 5}},
nil,
[]pb.Entry{{Index: 3, Term: 3}, {Index: 4, Term: 4}, {Index: 5, Term: 5}, {Index: 6, Term: 5}},
},
}
for i, tt := range tests {
s := &MemoryStorage{ents: ents}
err := s.Append(tt.entries)
if err != tt.werr {
t.Errorf("#%d: err = %v, want %v", i, err, tt.werr)
}
if !reflect.DeepEqual(s.ents, tt.wentries) {
t.Errorf("#%d: entries = %v, want %v", i, s.ents, tt.wentries)
}
}
}
func TestStorageApplySnapshot(t *testing.T) {
cs := &pb.ConfState{Voters: []uint64{1, 2, 3}}
data := []byte("data")
tests := []pb.Snapshot{{Data: data, Metadata: pb.SnapshotMetadata{Index: 4, Term: 4, ConfState: *cs}},
{Data: data, Metadata: pb.SnapshotMetadata{Index: 3, Term: 3, ConfState: *cs}},
}
s := NewMemoryStorage()
//Apply Snapshot successful
i := 0
tt := tests[i]
err := s.ApplySnapshot(tt)
if err != nil {
t.Errorf("#%d: err = %v, want %v", i, err, nil)
}
//Apply Snapshot fails due to ErrSnapOutOfDate
i = 1
tt = tests[i]
err = s.ApplySnapshot(tt)
if err != ErrSnapOutOfDate {
t.Errorf("#%d: err = %v, want %v", i, err, ErrSnapOutOfDate)
}
}

View File

@ -1,117 +0,0 @@
log-level info
----
ok
add-nodes 3 voters=(1,2,3) index=2
----
INFO 1 switched to configuration voters=(1 2 3)
INFO 1 became follower at term 0
INFO newRaft 1 [peers: [1,2,3], term: 0, commit: 2, applied: 2, lastindex: 2, lastterm: 1]
INFO 2 switched to configuration voters=(1 2 3)
INFO 2 became follower at term 0
INFO newRaft 2 [peers: [1,2,3], term: 0, commit: 2, applied: 2, lastindex: 2, lastterm: 1]
INFO 3 switched to configuration voters=(1 2 3)
INFO 3 became follower at term 0
INFO newRaft 3 [peers: [1,2,3], term: 0, commit: 2, applied: 2, lastindex: 2, lastterm: 1]
campaign 1
----
INFO 1 is starting a new election at term 0
INFO 1 became candidate at term 1
INFO 1 received MsgVoteResp from 1 at term 1
INFO 1 [logterm: 1, index: 2] sent MsgVote request to 2 at term 1
INFO 1 [logterm: 1, index: 2] sent MsgVote request to 3 at term 1
stabilize
----
> 1 handling Ready
Ready MustSync=true:
Lead:0 State:StateCandidate
HardState Term:1 Vote:1 Commit:2
Messages:
1->2 MsgVote Term:1 Log:1/2
1->3 MsgVote Term:1 Log:1/2
> 2 receiving messages
1->2 MsgVote Term:1 Log:1/2
INFO 2 [term: 0] received a MsgVote message with higher term from 1 [term: 1]
INFO 2 became follower at term 1
INFO 2 [logterm: 1, index: 2, vote: 0] cast MsgVote for 1 [logterm: 1, index: 2] at term 1
> 3 receiving messages
1->3 MsgVote Term:1 Log:1/2
INFO 3 [term: 0] received a MsgVote message with higher term from 1 [term: 1]
INFO 3 became follower at term 1
INFO 3 [logterm: 1, index: 2, vote: 0] cast MsgVote for 1 [logterm: 1, index: 2] at term 1
> 2 handling Ready
Ready MustSync=true:
HardState Term:1 Vote:1 Commit:2
Messages:
2->1 MsgVoteResp Term:1 Log:0/0
> 3 handling Ready
Ready MustSync=true:
HardState Term:1 Vote:1 Commit:2
Messages:
3->1 MsgVoteResp Term:1 Log:0/0
> 1 receiving messages
2->1 MsgVoteResp Term:1 Log:0/0
INFO 1 received MsgVoteResp from 2 at term 1
INFO 1 has received 2 MsgVoteResp votes and 0 vote rejections
INFO 1 became leader at term 1
3->1 MsgVoteResp Term:1 Log:0/0
> 1 handling Ready
Ready MustSync=true:
Lead:1 State:StateLeader
Entries:
1/3 EntryNormal ""
Messages:
1->2 MsgApp Term:1 Log:1/2 Commit:2 Entries:[1/3 EntryNormal ""]
1->3 MsgApp Term:1 Log:1/2 Commit:2 Entries:[1/3 EntryNormal ""]
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/2 Commit:2 Entries:[1/3 EntryNormal ""]
> 3 receiving messages
1->3 MsgApp Term:1 Log:1/2 Commit:2 Entries:[1/3 EntryNormal ""]
> 2 handling Ready
Ready MustSync=true:
Lead:1 State:StateFollower
Entries:
1/3 EntryNormal ""
Messages:
2->1 MsgAppResp Term:1 Log:0/3
> 3 handling Ready
Ready MustSync=true:
Lead:1 State:StateFollower
Entries:
1/3 EntryNormal ""
Messages:
3->1 MsgAppResp Term:1 Log:0/3
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/3
3->1 MsgAppResp Term:1 Log:0/3
> 1 handling Ready
Ready MustSync=false:
HardState Term:1 Vote:1 Commit:3
CommittedEntries:
1/3 EntryNormal ""
Messages:
1->2 MsgApp Term:1 Log:1/3 Commit:3
1->3 MsgApp Term:1 Log:1/3 Commit:3
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/3 Commit:3
> 3 receiving messages
1->3 MsgApp Term:1 Log:1/3 Commit:3
> 2 handling Ready
Ready MustSync=false:
HardState Term:1 Vote:1 Commit:3
CommittedEntries:
1/3 EntryNormal ""
Messages:
2->1 MsgAppResp Term:1 Log:0/3
> 3 handling Ready
Ready MustSync=false:
HardState Term:1 Vote:1 Commit:3
CommittedEntries:
1/3 EntryNormal ""
Messages:
3->1 MsgAppResp Term:1 Log:0/3
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/3
3->1 MsgAppResp Term:1 Log:0/3

View File

@ -1,152 +0,0 @@
# Regression test that verifies that learners can vote. This holds only in the
# sense that if a learner is asked to vote, a candidate believes that they are a
# voter based on its current config, which may be more recent than that of the
# learner. If learners which are actually voters but don't know it yet don't
# vote in that situation, the raft group may end up unavailable despite a quorum
# of voters (as of the latest config) being available.
#
# See:
# https://github.com/etcd-io/etcd/pull/10998
# Turn output off during boilerplate.
log-level none
----
ok
# Bootstrap three nodes.
add-nodes 3 voters=(1,2) learners=(3) index=2
----
ok
# n1 gets to be leader.
campaign 1
----
ok
stabilize
----
ok (quiet)
# Propose a conf change on n1 that promotes n3 to voter.
propose-conf-change 1
v3
----
ok
# Commit and fully apply said conf change. n1 and n2 now consider n3 a voter.
stabilize 1 2
----
ok (quiet)
# Drop all inflight messages to 3. We don't want it to be caught up when it is
# asked to vote.
deliver-msgs drop=(3)
----
ok (quiet)
# We now pretend that n1 is dead, and n2 is trying to become leader.
log-level debug
----
ok
campaign 2
----
INFO 2 is starting a new election at term 1
INFO 2 became candidate at term 2
INFO 2 received MsgVoteResp from 2 at term 2
INFO 2 [logterm: 1, index: 4] sent MsgVote request to 1 at term 2
INFO 2 [logterm: 1, index: 4] sent MsgVote request to 3 at term 2
# Send out the MsgVote requests.
process-ready 2
----
Ready MustSync=true:
Lead:0 State:StateCandidate
HardState Term:2 Vote:2 Commit:4
Messages:
2->1 MsgVote Term:2 Log:1/4
2->3 MsgVote Term:2 Log:1/4
# n2 is now campaigning while n1 is down (does not respond). The latest config
# has n3 as a voter, but n3 doesn't even have the corresponding conf change in
# its log. Still, it casts a vote for n2 which can in turn become leader and
# catches up n3.
stabilize 3
----
> 3 receiving messages
2->3 MsgVote Term:2 Log:1/4
INFO 3 [term: 1] received a MsgVote message with higher term from 2 [term: 2]
INFO 3 became follower at term 2
INFO 3 [logterm: 1, index: 3, vote: 0] cast MsgVote for 2 [logterm: 1, index: 4] at term 2
> 3 handling Ready
Ready MustSync=true:
Lead:0 State:StateFollower
HardState Term:2 Vote:2 Commit:3
Messages:
3->2 MsgVoteResp Term:2 Log:0/0
stabilize 2 3
----
> 2 receiving messages
3->2 MsgVoteResp Term:2 Log:0/0
INFO 2 received MsgVoteResp from 3 at term 2
INFO 2 has received 2 MsgVoteResp votes and 0 vote rejections
INFO 2 became leader at term 2
> 2 handling Ready
Ready MustSync=true:
Lead:2 State:StateLeader
Entries:
2/5 EntryNormal ""
Messages:
2->1 MsgApp Term:2 Log:1/4 Commit:4 Entries:[2/5 EntryNormal ""]
2->3 MsgApp Term:2 Log:1/4 Commit:4 Entries:[2/5 EntryNormal ""]
> 3 receiving messages
2->3 MsgApp Term:2 Log:1/4 Commit:4 Entries:[2/5 EntryNormal ""]
DEBUG 3 [logterm: 0, index: 4] rejected MsgApp [logterm: 1, index: 4] from 2
> 3 handling Ready
Ready MustSync=false:
Lead:2 State:StateFollower
Messages:
3->2 MsgAppResp Term:2 Log:1/4 Rejected (Hint: 3)
> 2 receiving messages
3->2 MsgAppResp Term:2 Log:1/4 Rejected (Hint: 3)
DEBUG 2 received MsgAppResp(rejected, hint: (index 3, term 1)) from 3 for index 4
DEBUG 2 decreased progress of 3 to [StateProbe match=0 next=4]
> 2 handling Ready
Ready MustSync=false:
Messages:
2->3 MsgApp Term:2 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v3, 2/5 EntryNormal ""]
> 3 receiving messages
2->3 MsgApp Term:2 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v3, 2/5 EntryNormal ""]
> 3 handling Ready
Ready MustSync=true:
HardState Term:2 Vote:2 Commit:4
Entries:
1/4 EntryConfChangeV2 v3
2/5 EntryNormal ""
CommittedEntries:
1/4 EntryConfChangeV2 v3
Messages:
3->2 MsgAppResp Term:2 Log:0/5
INFO 3 switched to configuration voters=(1 2 3)
> 2 receiving messages
3->2 MsgAppResp Term:2 Log:0/5
> 2 handling Ready
Ready MustSync=false:
HardState Term:2 Vote:2 Commit:5
CommittedEntries:
2/5 EntryNormal ""
Messages:
2->3 MsgApp Term:2 Log:2/5 Commit:5
> 3 receiving messages
2->3 MsgApp Term:2 Log:2/5 Commit:5
> 3 handling Ready
Ready MustSync=false:
HardState Term:2 Vote:2 Commit:5
CommittedEntries:
2/5 EntryNormal ""
Messages:
3->2 MsgAppResp Term:2 Log:0/5
> 2 receiving messages
3->2 MsgAppResp Term:2 Log:0/5

View File

@ -1,97 +0,0 @@
# Run a V1 membership change that adds a single voter.
# Bootstrap n1.
add-nodes 1 voters=(1) index=2
----
INFO 1 switched to configuration voters=(1)
INFO 1 became follower at term 0
INFO newRaft 1 [peers: [1], term: 0, commit: 2, applied: 2, lastindex: 2, lastterm: 1]
campaign 1
----
INFO 1 is starting a new election at term 0
INFO 1 became candidate at term 1
INFO 1 received MsgVoteResp from 1 at term 1
INFO 1 became leader at term 1
# Add v2 (with an auto transition).
propose-conf-change 1 v1=true
v2
----
ok
# Pull n2 out of thin air.
add-nodes 1
----
INFO 2 switched to configuration voters=()
INFO 2 became follower at term 0
INFO newRaft 2 [peers: [], term: 0, commit: 0, applied: 0, lastindex: 0, lastterm: 0]
# n1 commits the conf change using itself as commit quorum, immediately transitions into
# the final config, and catches up n2. Note that it's using an EntryConfChange, not an
# EntryConfChangeV2, so this is compatible with nodes that don't know about V2 conf changes.
stabilize
----
> 1 handling Ready
Ready MustSync=true:
Lead:1 State:StateLeader
HardState Term:1 Vote:1 Commit:4
Entries:
1/3 EntryNormal ""
1/4 EntryConfChange v2
CommittedEntries:
1/3 EntryNormal ""
1/4 EntryConfChange v2
INFO 1 switched to configuration voters=(1 2)
> 1 handling Ready
Ready MustSync=false:
Messages:
1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChange v2]
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChange v2]
INFO 2 [term: 0] received a MsgApp message with higher term from 1 [term: 1]
INFO 2 became follower at term 1
DEBUG 2 [logterm: 0, index: 3] rejected MsgApp [logterm: 1, index: 3] from 1
> 2 handling Ready
Ready MustSync=true:
Lead:1 State:StateFollower
HardState Term:1 Commit:0
Messages:
2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
DEBUG 1 received MsgAppResp(rejected, hint: (index 0, term 0)) from 2 for index 3
DEBUG 1 decreased progress of 2 to [StateProbe match=0 next=1]
DEBUG 1 [firstindex: 3, commit: 4] sent snapshot[index: 4, term: 1] to 2 [StateProbe match=0 next=1]
DEBUG 1 paused sending replication messages to 2 [StateSnapshot match=0 next=1 paused pendingSnap=4]
> 1 handling Ready
Ready MustSync=false:
Messages:
1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
> 2 receiving messages
1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
INFO log [committed=0, applied=0, unstable.offset=1, len(unstable.Entries)=0] starts to restore snapshot [index: 4, term: 1]
INFO 2 switched to configuration voters=(1 2)
INFO 2 [commit: 4, lastindex: 4, lastterm: 1] restored snapshot [index: 4, term: 1]
INFO 2 [commit: 4] restored snapshot [index: 4, term: 1]
> 2 handling Ready
Ready MustSync=false:
HardState Term:1 Commit:4
Snapshot Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
Messages:
2->1 MsgAppResp Term:1 Log:0/4
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/4
DEBUG 1 recovered from needing snapshot, resumed sending replication messages to 2 [StateSnapshot match=4 next=5 paused pendingSnap=4]
> 1 handling Ready
Ready MustSync=false:
Messages:
1->2 MsgApp Term:1 Log:1/4 Commit:4
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/4 Commit:4
> 2 handling Ready
Ready MustSync=false:
Messages:
2->1 MsgAppResp Term:1 Log:0/4
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/4

View File

@ -1,224 +0,0 @@
# We'll turn this back on after the boilerplate.
log-level none
----
ok
# Run a V1 membership change that removes the leader.
# Bootstrap n1, n2, n3.
add-nodes 3 voters=(1,2,3) index=2
----
ok
campaign 1
----
ok
stabilize
----
ok (quiet)
log-level debug
----
ok
# Start removing n1.
propose-conf-change 1 v1=true
r1
----
ok
# Propose an extra entry which will be sent out together with the conf change.
propose 1 foo
----
ok
# Send out the corresponding appends.
process-ready 1
----
Ready MustSync=true:
Entries:
1/4 EntryConfChange r1
1/5 EntryNormal "foo"
Messages:
1->2 MsgApp Term:1 Log:1/3 Commit:3 Entries:[1/4 EntryConfChange r1]
1->3 MsgApp Term:1 Log:1/3 Commit:3 Entries:[1/4 EntryConfChange r1]
1->2 MsgApp Term:1 Log:1/4 Commit:3 Entries:[1/5 EntryNormal "foo"]
1->3 MsgApp Term:1 Log:1/4 Commit:3 Entries:[1/5 EntryNormal "foo"]
# Send response from n2 (which is enough to commit the entries so far next time
# n1 runs).
stabilize 2
----
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/3 Commit:3 Entries:[1/4 EntryConfChange r1]
1->2 MsgApp Term:1 Log:1/4 Commit:3 Entries:[1/5 EntryNormal "foo"]
> 2 handling Ready
Ready MustSync=true:
Entries:
1/4 EntryConfChange r1
1/5 EntryNormal "foo"
Messages:
2->1 MsgAppResp Term:1 Log:0/4
2->1 MsgAppResp Term:1 Log:0/5
# Put another entry in n1's log.
propose 1 bar
----
ok
# n1 applies the conf change, so it has now removed itself. But it still has
# an uncommitted entry in the log. If the leader unconditionally counted itself
# as part of the commit quorum, we'd be in trouble. In the block below, we see
# it send out appends to the other nodes for the 'bar' entry.
stabilize 1
----
> 1 handling Ready
Ready MustSync=true:
Entries:
1/6 EntryNormal "bar"
Messages:
1->2 MsgApp Term:1 Log:1/5 Commit:3 Entries:[1/6 EntryNormal "bar"]
1->3 MsgApp Term:1 Log:1/5 Commit:3 Entries:[1/6 EntryNormal "bar"]
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/4
2->1 MsgAppResp Term:1 Log:0/5
> 1 handling Ready
Ready MustSync=false:
HardState Term:1 Vote:1 Commit:5
CommittedEntries:
1/4 EntryConfChange r1
1/5 EntryNormal "foo"
Messages:
1->2 MsgApp Term:1 Log:1/6 Commit:4
1->3 MsgApp Term:1 Log:1/6 Commit:4
1->2 MsgApp Term:1 Log:1/6 Commit:5
1->3 MsgApp Term:1 Log:1/6 Commit:5
INFO 1 switched to configuration voters=(2 3)
# n2 responds, n3 doesn't yet. Quorum for 'bar' should not be reached...
stabilize 2
----
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/5 Commit:3 Entries:[1/6 EntryNormal "bar"]
1->2 MsgApp Term:1 Log:1/6 Commit:4
1->2 MsgApp Term:1 Log:1/6 Commit:5
> 2 handling Ready
Ready MustSync=true:
HardState Term:1 Vote:1 Commit:5
Entries:
1/6 EntryNormal "bar"
CommittedEntries:
1/4 EntryConfChange r1
1/5 EntryNormal "foo"
Messages:
2->1 MsgAppResp Term:1 Log:0/6
2->1 MsgAppResp Term:1 Log:0/6
2->1 MsgAppResp Term:1 Log:0/6
INFO 2 switched to configuration voters=(2 3)
# ... which thankfully is what we see on the leader.
stabilize 1
----
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/6
2->1 MsgAppResp Term:1 Log:0/6
2->1 MsgAppResp Term:1 Log:0/6
# When n3 responds, quorum is reached and everything falls into place.
stabilize
----
> 3 receiving messages
1->3 MsgApp Term:1 Log:1/3 Commit:3 Entries:[1/4 EntryConfChange r1]
1->3 MsgApp Term:1 Log:1/4 Commit:3 Entries:[1/5 EntryNormal "foo"]
1->3 MsgApp Term:1 Log:1/5 Commit:3 Entries:[1/6 EntryNormal "bar"]
1->3 MsgApp Term:1 Log:1/6 Commit:4
1->3 MsgApp Term:1 Log:1/6 Commit:5
> 3 handling Ready
Ready MustSync=true:
HardState Term:1 Vote:1 Commit:5
Entries:
1/4 EntryConfChange r1
1/5 EntryNormal "foo"
1/6 EntryNormal "bar"
CommittedEntries:
1/4 EntryConfChange r1
1/5 EntryNormal "foo"
Messages:
3->1 MsgAppResp Term:1 Log:0/4
3->1 MsgAppResp Term:1 Log:0/5
3->1 MsgAppResp Term:1 Log:0/6
3->1 MsgAppResp Term:1 Log:0/6
3->1 MsgAppResp Term:1 Log:0/6
INFO 3 switched to configuration voters=(2 3)
> 1 receiving messages
3->1 MsgAppResp Term:1 Log:0/4
3->1 MsgAppResp Term:1 Log:0/5
3->1 MsgAppResp Term:1 Log:0/6
3->1 MsgAppResp Term:1 Log:0/6
3->1 MsgAppResp Term:1 Log:0/6
> 1 handling Ready
Ready MustSync=false:
HardState Term:1 Vote:1 Commit:6
CommittedEntries:
1/6 EntryNormal "bar"
Messages:
1->2 MsgApp Term:1 Log:1/6 Commit:6
1->3 MsgApp Term:1 Log:1/6 Commit:6
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/6 Commit:6
> 3 receiving messages
1->3 MsgApp Term:1 Log:1/6 Commit:6
> 2 handling Ready
Ready MustSync=false:
HardState Term:1 Vote:1 Commit:6
CommittedEntries:
1/6 EntryNormal "bar"
Messages:
2->1 MsgAppResp Term:1 Log:0/6
> 3 handling Ready
Ready MustSync=false:
HardState Term:1 Vote:1 Commit:6
CommittedEntries:
1/6 EntryNormal "bar"
Messages:
3->1 MsgAppResp Term:1 Log:0/6
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/6
3->1 MsgAppResp Term:1 Log:0/6
# However not all is well. n1 is still leader but unconditionally drops all
# proposals on the floor, so we're effectively stuck if it still heartbeats
# its followers...
propose 1 baz
----
raft proposal dropped
tick-heartbeat 1
----
ok
# ... which, uh oh, it does.
# TODO(tbg): change behavior so that a leader that is removed immediately steps
# down, and initiates an optimistic handover.
stabilize
----
> 1 handling Ready
Ready MustSync=false:
Messages:
1->2 MsgHeartbeat Term:1 Log:0/0 Commit:6
1->3 MsgHeartbeat Term:1 Log:0/0 Commit:6
> 2 receiving messages
1->2 MsgHeartbeat Term:1 Log:0/0 Commit:6
> 3 receiving messages
1->3 MsgHeartbeat Term:1 Log:0/0 Commit:6
> 2 handling Ready
Ready MustSync=false:
Messages:
2->1 MsgHeartbeatResp Term:1 Log:0/0
> 3 handling Ready
Ready MustSync=false:
Messages:
3->1 MsgHeartbeatResp Term:1 Log:0/0
> 1 receiving messages
2->1 MsgHeartbeatResp Term:1 Log:0/0
3->1 MsgHeartbeatResp Term:1 Log:0/0

View File

@ -1,408 +0,0 @@
# Run a V2 membership change that adds two voters at once and auto-leaves the
# joint configuration. (This is the same as specifying an explicit transition
# since more than one change is being made atomically).
# Bootstrap n1.
add-nodes 1 voters=(1) index=2
----
INFO 1 switched to configuration voters=(1)
INFO 1 became follower at term 0
INFO newRaft 1 [peers: [1], term: 0, commit: 2, applied: 2, lastindex: 2, lastterm: 1]
campaign 1
----
INFO 1 is starting a new election at term 0
INFO 1 became candidate at term 1
INFO 1 received MsgVoteResp from 1 at term 1
INFO 1 became leader at term 1
propose-conf-change 1 transition=auto
v2 v3
----
ok
# Add two "empty" nodes to the cluster, n2 and n3.
add-nodes 2
----
INFO 2 switched to configuration voters=()
INFO 2 became follower at term 0
INFO newRaft 2 [peers: [], term: 0, commit: 0, applied: 0, lastindex: 0, lastterm: 0]
INFO 3 switched to configuration voters=()
INFO 3 became follower at term 0
INFO newRaft 3 [peers: [], term: 0, commit: 0, applied: 0, lastindex: 0, lastterm: 0]
# n1 immediately gets to commit & apply the conf change using only itself. We see that
# it starts transitioning out of that joint configuration (though we will only see that
# proposal in the next ready handling loop, when it is emitted). We also see that this
# is using joint consensus, which it has to since we're carrying out two additions at
# once.
process-ready 1
----
Ready MustSync=true:
Lead:1 State:StateLeader
HardState Term:1 Vote:1 Commit:4
Entries:
1/3 EntryNormal ""
1/4 EntryConfChangeV2 v2 v3
CommittedEntries:
1/3 EntryNormal ""
1/4 EntryConfChangeV2 v2 v3
INFO 1 switched to configuration voters=(1 2 3)&&(1) autoleave
INFO initiating automatic transition out of joint configuration voters=(1 2 3)&&(1) autoleave
# n1 immediately probes n2 and n3.
stabilize 1
----
> 1 handling Ready
Ready MustSync=true:
Entries:
1/5 EntryConfChangeV2
Messages:
1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2 v3]
1->3 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2 v3]
# First, play out the whole interaction between n1 and n2. We see n1's probe to
# n2 get rejected (since n2 needs a snapshot); the snapshot is delivered at which
# point n2 switches to the correct config, and n1 catches it up. This notably
# includes the empty conf change which gets committed and applied by both and
# which transitions them out of their joint configuration into the final one (1 2 3).
stabilize 1 2
----
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2 v3]
INFO 2 [term: 0] received a MsgApp message with higher term from 1 [term: 1]
INFO 2 became follower at term 1
DEBUG 2 [logterm: 0, index: 3] rejected MsgApp [logterm: 1, index: 3] from 1
> 2 handling Ready
Ready MustSync=true:
Lead:1 State:StateFollower
HardState Term:1 Commit:0
Messages:
2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
DEBUG 1 received MsgAppResp(rejected, hint: (index 0, term 0)) from 2 for index 3
DEBUG 1 decreased progress of 2 to [StateProbe match=0 next=1]
DEBUG 1 [firstindex: 3, commit: 4] sent snapshot[index: 4, term: 1] to 2 [StateProbe match=0 next=1]
DEBUG 1 paused sending replication messages to 2 [StateSnapshot match=0 next=1 paused pendingSnap=4]
> 1 handling Ready
Ready MustSync=false:
Messages:
1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:true
> 2 receiving messages
1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:true
INFO log [committed=0, applied=0, unstable.offset=1, len(unstable.Entries)=0] starts to restore snapshot [index: 4, term: 1]
INFO 2 switched to configuration voters=(1 2 3)&&(1) autoleave
INFO 2 [commit: 4, lastindex: 4, lastterm: 1] restored snapshot [index: 4, term: 1]
INFO 2 [commit: 4] restored snapshot [index: 4, term: 1]
> 2 handling Ready
Ready MustSync=false:
HardState Term:1 Commit:4
Snapshot Index:4 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:true
Messages:
2->1 MsgAppResp Term:1 Log:0/4
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/4
DEBUG 1 recovered from needing snapshot, resumed sending replication messages to 2 [StateSnapshot match=4 next=5 paused pendingSnap=4]
> 1 handling Ready
Ready MustSync=false:
Messages:
1->2 MsgApp Term:1 Log:1/4 Commit:4 Entries:[1/5 EntryConfChangeV2]
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/4 Commit:4 Entries:[1/5 EntryConfChangeV2]
> 2 handling Ready
Ready MustSync=true:
Entries:
1/5 EntryConfChangeV2
Messages:
2->1 MsgAppResp Term:1 Log:0/5
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/5
> 1 handling Ready
Ready MustSync=false:
HardState Term:1 Vote:1 Commit:5
CommittedEntries:
1/5 EntryConfChangeV2
Messages:
1->2 MsgApp Term:1 Log:1/5 Commit:5
INFO 1 switched to configuration voters=(1 2 3)
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/5 Commit:5
> 2 handling Ready
Ready MustSync=false:
HardState Term:1 Commit:5
CommittedEntries:
1/5 EntryConfChangeV2
Messages:
2->1 MsgAppResp Term:1 Log:0/5
INFO 2 switched to configuration voters=(1 2 3)
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/5
# n3 immediately receives a snapshot in the final configuration.
stabilize 1 3
----
> 3 receiving messages
1->3 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2 v3]
INFO 3 [term: 0] received a MsgApp message with higher term from 1 [term: 1]
INFO 3 became follower at term 1
DEBUG 3 [logterm: 0, index: 3] rejected MsgApp [logterm: 1, index: 3] from 1
> 3 handling Ready
Ready MustSync=true:
Lead:1 State:StateFollower
HardState Term:1 Commit:0
Messages:
3->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
> 1 receiving messages
3->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
DEBUG 1 received MsgAppResp(rejected, hint: (index 0, term 0)) from 3 for index 3
DEBUG 1 decreased progress of 3 to [StateProbe match=0 next=1]
DEBUG 1 [firstindex: 3, commit: 5] sent snapshot[index: 5, term: 1] to 3 [StateProbe match=0 next=1]
DEBUG 1 paused sending replication messages to 3 [StateSnapshot match=0 next=1 paused pendingSnap=5]
> 1 handling Ready
Ready MustSync=false:
Messages:
1->3 MsgSnap Term:1 Log:0/0 Snapshot: Index:5 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
> 3 receiving messages
1->3 MsgSnap Term:1 Log:0/0 Snapshot: Index:5 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
INFO log [committed=0, applied=0, unstable.offset=1, len(unstable.Entries)=0] starts to restore snapshot [index: 5, term: 1]
INFO 3 switched to configuration voters=(1 2 3)
INFO 3 [commit: 5, lastindex: 5, lastterm: 1] restored snapshot [index: 5, term: 1]
INFO 3 [commit: 5] restored snapshot [index: 5, term: 1]
> 3 handling Ready
Ready MustSync=false:
HardState Term:1 Commit:5
Snapshot Index:5 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
Messages:
3->1 MsgAppResp Term:1 Log:0/5
> 1 receiving messages
3->1 MsgAppResp Term:1 Log:0/5
DEBUG 1 recovered from needing snapshot, resumed sending replication messages to 3 [StateSnapshot match=5 next=6 paused pendingSnap=5]
> 1 handling Ready
Ready MustSync=false:
Messages:
1->3 MsgApp Term:1 Log:1/5 Commit:5
> 3 receiving messages
1->3 MsgApp Term:1 Log:1/5 Commit:5
> 3 handling Ready
Ready MustSync=false:
Messages:
3->1 MsgAppResp Term:1 Log:0/5
> 1 receiving messages
3->1 MsgAppResp Term:1 Log:0/5
# Nothing else happens.
stabilize
----
ok
# Now remove two nodes. What's new here is that the leader will actually have
# to go to a quorum to commit the transition into the joint config.
propose-conf-change 1
r2 r3
----
ok
# n1 sends out MsgApps.
stabilize 1
----
> 1 handling Ready
Ready MustSync=true:
Entries:
1/6 EntryConfChangeV2 r2 r3
Messages:
1->2 MsgApp Term:1 Log:1/5 Commit:5 Entries:[1/6 EntryConfChangeV2 r2 r3]
1->3 MsgApp Term:1 Log:1/5 Commit:5 Entries:[1/6 EntryConfChangeV2 r2 r3]
# n2, n3 ack them.
stabilize 2 3
----
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/5 Commit:5 Entries:[1/6 EntryConfChangeV2 r2 r3]
> 3 receiving messages
1->3 MsgApp Term:1 Log:1/5 Commit:5 Entries:[1/6 EntryConfChangeV2 r2 r3]
> 2 handling Ready
Ready MustSync=true:
Entries:
1/6 EntryConfChangeV2 r2 r3
Messages:
2->1 MsgAppResp Term:1 Log:0/6
> 3 handling Ready
Ready MustSync=true:
Entries:
1/6 EntryConfChangeV2 r2 r3
Messages:
3->1 MsgAppResp Term:1 Log:0/6
# n1 gets some more proposals. This is part of a regression test: There used to
# be a bug in which these proposals would prompt the leader to transition out of
# the same joint state multiple times, which would cause a panic.
propose 1 foo
----
ok
propose 1 bar
----
ok
# n1 switches to the joint config, then initiates a transition into the final
# config.
stabilize 1
----
> 1 handling Ready
Ready MustSync=true:
Entries:
1/7 EntryNormal "foo"
1/8 EntryNormal "bar"
Messages:
1->2 MsgApp Term:1 Log:1/6 Commit:5 Entries:[1/7 EntryNormal "foo"]
1->3 MsgApp Term:1 Log:1/6 Commit:5 Entries:[1/7 EntryNormal "foo"]
1->2 MsgApp Term:1 Log:1/7 Commit:5 Entries:[1/8 EntryNormal "bar"]
1->3 MsgApp Term:1 Log:1/7 Commit:5 Entries:[1/8 EntryNormal "bar"]
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/6
3->1 MsgAppResp Term:1 Log:0/6
> 1 handling Ready
Ready MustSync=false:
HardState Term:1 Vote:1 Commit:6
CommittedEntries:
1/6 EntryConfChangeV2 r2 r3
Messages:
1->2 MsgApp Term:1 Log:1/8 Commit:6
1->3 MsgApp Term:1 Log:1/8 Commit:6
INFO 1 switched to configuration voters=(1)&&(1 2 3) autoleave
INFO initiating automatic transition out of joint configuration voters=(1)&&(1 2 3) autoleave
> 1 handling Ready
Ready MustSync=true:
Entries:
1/9 EntryConfChangeV2
# n2 and n3 also switch to the joint config, and ack the transition out of it.
stabilize 2 3
----
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/6 Commit:5 Entries:[1/7 EntryNormal "foo"]
1->2 MsgApp Term:1 Log:1/7 Commit:5 Entries:[1/8 EntryNormal "bar"]
1->2 MsgApp Term:1 Log:1/8 Commit:6
> 3 receiving messages
1->3 MsgApp Term:1 Log:1/6 Commit:5 Entries:[1/7 EntryNormal "foo"]
1->3 MsgApp Term:1 Log:1/7 Commit:5 Entries:[1/8 EntryNormal "bar"]
1->3 MsgApp Term:1 Log:1/8 Commit:6
> 2 handling Ready
Ready MustSync=true:
HardState Term:1 Commit:6
Entries:
1/7 EntryNormal "foo"
1/8 EntryNormal "bar"
CommittedEntries:
1/6 EntryConfChangeV2 r2 r3
Messages:
2->1 MsgAppResp Term:1 Log:0/7
2->1 MsgAppResp Term:1 Log:0/8
2->1 MsgAppResp Term:1 Log:0/8
INFO 2 switched to configuration voters=(1)&&(1 2 3) autoleave
> 3 handling Ready
Ready MustSync=true:
HardState Term:1 Commit:6
Entries:
1/7 EntryNormal "foo"
1/8 EntryNormal "bar"
CommittedEntries:
1/6 EntryConfChangeV2 r2 r3
Messages:
3->1 MsgAppResp Term:1 Log:0/7
3->1 MsgAppResp Term:1 Log:0/8
3->1 MsgAppResp Term:1 Log:0/8
INFO 3 switched to configuration voters=(1)&&(1 2 3) autoleave
# n2 and n3 also leave the joint config and the dust settles. We see at the very
# end that n1 receives some messages from them that it refuses because it does
# not have them in its config any more.
stabilize
----
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/7
2->1 MsgAppResp Term:1 Log:0/8
2->1 MsgAppResp Term:1 Log:0/8
3->1 MsgAppResp Term:1 Log:0/7
3->1 MsgAppResp Term:1 Log:0/8
3->1 MsgAppResp Term:1 Log:0/8
> 1 handling Ready
Ready MustSync=false:
HardState Term:1 Vote:1 Commit:8
CommittedEntries:
1/7 EntryNormal "foo"
1/8 EntryNormal "bar"
Messages:
1->2 MsgApp Term:1 Log:1/8 Commit:7 Entries:[1/9 EntryConfChangeV2]
1->3 MsgApp Term:1 Log:1/8 Commit:7 Entries:[1/9 EntryConfChangeV2]
1->2 MsgApp Term:1 Log:1/9 Commit:8
1->3 MsgApp Term:1 Log:1/9 Commit:8
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/8 Commit:7 Entries:[1/9 EntryConfChangeV2]
1->2 MsgApp Term:1 Log:1/9 Commit:8
> 3 receiving messages
1->3 MsgApp Term:1 Log:1/8 Commit:7 Entries:[1/9 EntryConfChangeV2]
1->3 MsgApp Term:1 Log:1/9 Commit:8
> 2 handling Ready
Ready MustSync=true:
HardState Term:1 Commit:8
Entries:
1/9 EntryConfChangeV2
CommittedEntries:
1/7 EntryNormal "foo"
1/8 EntryNormal "bar"
Messages:
2->1 MsgAppResp Term:1 Log:0/9
2->1 MsgAppResp Term:1 Log:0/9
> 3 handling Ready
Ready MustSync=true:
HardState Term:1 Commit:8
Entries:
1/9 EntryConfChangeV2
CommittedEntries:
1/7 EntryNormal "foo"
1/8 EntryNormal "bar"
Messages:
3->1 MsgAppResp Term:1 Log:0/9
3->1 MsgAppResp Term:1 Log:0/9
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/9
2->1 MsgAppResp Term:1 Log:0/9
3->1 MsgAppResp Term:1 Log:0/9
3->1 MsgAppResp Term:1 Log:0/9
> 1 handling Ready
Ready MustSync=false:
HardState Term:1 Vote:1 Commit:9
CommittedEntries:
1/9 EntryConfChangeV2
Messages:
1->2 MsgApp Term:1 Log:1/9 Commit:9
1->3 MsgApp Term:1 Log:1/9 Commit:9
INFO 1 switched to configuration voters=(1)
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/9 Commit:9
> 3 receiving messages
1->3 MsgApp Term:1 Log:1/9 Commit:9
> 2 handling Ready
Ready MustSync=false:
HardState Term:1 Commit:9
CommittedEntries:
1/9 EntryConfChangeV2
Messages:
2->1 MsgAppResp Term:1 Log:0/9
INFO 2 switched to configuration voters=(1)
> 3 handling Ready
Ready MustSync=false:
HardState Term:1 Commit:9
CommittedEntries:
1/9 EntryConfChangeV2
Messages:
3->1 MsgAppResp Term:1 Log:0/9
INFO 3 switched to configuration voters=(1)
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/9
raft: cannot step as peer not found
3->1 MsgAppResp Term:1 Log:0/9
raft: cannot step as peer not found

View File

@ -1,125 +0,0 @@
# Run a V2 membership change that adds a single voter but explicitly asks for the
# use of joint consensus (with auto-leaving).
# TODO(tbg): also verify that if the leader changes while in the joint state, the
# new leader will auto-transition out of the joint state just the same.
# Bootstrap n1.
add-nodes 1 voters=(1) index=2
----
INFO 1 switched to configuration voters=(1)
INFO 1 became follower at term 0
INFO newRaft 1 [peers: [1], term: 0, commit: 2, applied: 2, lastindex: 2, lastterm: 1]
campaign 1
----
INFO 1 is starting a new election at term 0
INFO 1 became candidate at term 1
INFO 1 received MsgVoteResp from 1 at term 1
INFO 1 became leader at term 1
propose-conf-change 1 transition=implicit
v2
----
ok
# Add n2.
add-nodes 1
----
INFO 2 switched to configuration voters=()
INFO 2 became follower at term 0
INFO newRaft 2 [peers: [], term: 0, commit: 0, applied: 0, lastindex: 0, lastterm: 0]
# n1 commits the conf change using itself as commit quorum, then starts catching up n2.
# When that's done, it starts auto-transitioning out. Note that the snapshots propagating
# the joint config have the AutoLeave flag set in their config.
stabilize 1 2
----
> 1 handling Ready
Ready MustSync=true:
Lead:1 State:StateLeader
HardState Term:1 Vote:1 Commit:4
Entries:
1/3 EntryNormal ""
1/4 EntryConfChangeV2 v2
CommittedEntries:
1/3 EntryNormal ""
1/4 EntryConfChangeV2 v2
INFO 1 switched to configuration voters=(1 2)&&(1) autoleave
INFO initiating automatic transition out of joint configuration voters=(1 2)&&(1) autoleave
> 1 handling Ready
Ready MustSync=true:
Entries:
1/5 EntryConfChangeV2
Messages:
1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2]
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2]
INFO 2 [term: 0] received a MsgApp message with higher term from 1 [term: 1]
INFO 2 became follower at term 1
DEBUG 2 [logterm: 0, index: 3] rejected MsgApp [logterm: 1, index: 3] from 1
> 2 handling Ready
Ready MustSync=true:
Lead:1 State:StateFollower
HardState Term:1 Commit:0
Messages:
2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
DEBUG 1 received MsgAppResp(rejected, hint: (index 0, term 0)) from 2 for index 3
DEBUG 1 decreased progress of 2 to [StateProbe match=0 next=1]
DEBUG 1 [firstindex: 3, commit: 4] sent snapshot[index: 4, term: 1] to 2 [StateProbe match=0 next=1]
DEBUG 1 paused sending replication messages to 2 [StateSnapshot match=0 next=1 paused pendingSnap=4]
> 1 handling Ready
Ready MustSync=false:
Messages:
1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:true
> 2 receiving messages
1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:true
INFO log [committed=0, applied=0, unstable.offset=1, len(unstable.Entries)=0] starts to restore snapshot [index: 4, term: 1]
INFO 2 switched to configuration voters=(1 2)&&(1) autoleave
INFO 2 [commit: 4, lastindex: 4, lastterm: 1] restored snapshot [index: 4, term: 1]
INFO 2 [commit: 4] restored snapshot [index: 4, term: 1]
> 2 handling Ready
Ready MustSync=false:
HardState Term:1 Commit:4
Snapshot Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:true
Messages:
2->1 MsgAppResp Term:1 Log:0/4
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/4
DEBUG 1 recovered from needing snapshot, resumed sending replication messages to 2 [StateSnapshot match=4 next=5 paused pendingSnap=4]
> 1 handling Ready
Ready MustSync=false:
Messages:
1->2 MsgApp Term:1 Log:1/4 Commit:4 Entries:[1/5 EntryConfChangeV2]
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/4 Commit:4 Entries:[1/5 EntryConfChangeV2]
> 2 handling Ready
Ready MustSync=true:
Entries:
1/5 EntryConfChangeV2
Messages:
2->1 MsgAppResp Term:1 Log:0/5
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/5
> 1 handling Ready
Ready MustSync=false:
HardState Term:1 Vote:1 Commit:5
CommittedEntries:
1/5 EntryConfChangeV2
Messages:
1->2 MsgApp Term:1 Log:1/5 Commit:5
INFO 1 switched to configuration voters=(1 2)
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/5 Commit:5
> 2 handling Ready
Ready MustSync=false:
HardState Term:1 Commit:5
CommittedEntries:
1/5 EntryConfChangeV2
Messages:
2->1 MsgAppResp Term:1 Log:0/5
INFO 2 switched to configuration voters=(1 2)
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/5

View File

@ -1,98 +0,0 @@
# Run a V2 membership change that adds a single voter in auto mode, which means
# that joint consensus is not used but a direct transition into the new config
# takes place.
# Bootstrap n1.
add-nodes 1 voters=(1) index=2
----
INFO 1 switched to configuration voters=(1)
INFO 1 became follower at term 0
INFO newRaft 1 [peers: [1], term: 0, commit: 2, applied: 2, lastindex: 2, lastterm: 1]
campaign 1
----
INFO 1 is starting a new election at term 0
INFO 1 became candidate at term 1
INFO 1 received MsgVoteResp from 1 at term 1
INFO 1 became leader at term 1
# Add v2 (with an auto transition).
propose-conf-change 1
v2
----
ok
# Pull n2 out of thin air.
add-nodes 1
----
INFO 2 switched to configuration voters=()
INFO 2 became follower at term 0
INFO newRaft 2 [peers: [], term: 0, commit: 0, applied: 0, lastindex: 0, lastterm: 0]
# n1 commits the conf change using itself as commit quorum, immediately transitions into
# the final config, and catches up n2.
stabilize
----
> 1 handling Ready
Ready MustSync=true:
Lead:1 State:StateLeader
HardState Term:1 Vote:1 Commit:4
Entries:
1/3 EntryNormal ""
1/4 EntryConfChangeV2 v2
CommittedEntries:
1/3 EntryNormal ""
1/4 EntryConfChangeV2 v2
INFO 1 switched to configuration voters=(1 2)
> 1 handling Ready
Ready MustSync=false:
Messages:
1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2]
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2]
INFO 2 [term: 0] received a MsgApp message with higher term from 1 [term: 1]
INFO 2 became follower at term 1
DEBUG 2 [logterm: 0, index: 3] rejected MsgApp [logterm: 1, index: 3] from 1
> 2 handling Ready
Ready MustSync=true:
Lead:1 State:StateFollower
HardState Term:1 Commit:0
Messages:
2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
DEBUG 1 received MsgAppResp(rejected, hint: (index 0, term 0)) from 2 for index 3
DEBUG 1 decreased progress of 2 to [StateProbe match=0 next=1]
DEBUG 1 [firstindex: 3, commit: 4] sent snapshot[index: 4, term: 1] to 2 [StateProbe match=0 next=1]
DEBUG 1 paused sending replication messages to 2 [StateSnapshot match=0 next=1 paused pendingSnap=4]
> 1 handling Ready
Ready MustSync=false:
Messages:
1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
> 2 receiving messages
1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
INFO log [committed=0, applied=0, unstable.offset=1, len(unstable.Entries)=0] starts to restore snapshot [index: 4, term: 1]
INFO 2 switched to configuration voters=(1 2)
INFO 2 [commit: 4, lastindex: 4, lastterm: 1] restored snapshot [index: 4, term: 1]
INFO 2 [commit: 4] restored snapshot [index: 4, term: 1]
> 2 handling Ready
Ready MustSync=false:
HardState Term:1 Commit:4
Snapshot Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
Messages:
2->1 MsgAppResp Term:1 Log:0/4
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/4
DEBUG 1 recovered from needing snapshot, resumed sending replication messages to 2 [StateSnapshot match=4 next=5 paused pendingSnap=4]
> 1 handling Ready
Ready MustSync=false:
Messages:
1->2 MsgApp Term:1 Log:1/4 Commit:4
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/4 Commit:4
> 2 handling Ready
Ready MustSync=false:
Messages:
2->1 MsgAppResp Term:1 Log:0/4
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/4

View File

@ -1,206 +0,0 @@
# Run a V2 membership change that adds a single voter but explicitly asks for the
# use of joint consensus, including wanting to transition out of the joint config
# manually.
# Bootstrap n1.
add-nodes 1 voters=(1) index=2
----
INFO 1 switched to configuration voters=(1)
INFO 1 became follower at term 0
INFO newRaft 1 [peers: [1], term: 0, commit: 2, applied: 2, lastindex: 2, lastterm: 1]
campaign 1
----
INFO 1 is starting a new election at term 0
INFO 1 became candidate at term 1
INFO 1 received MsgVoteResp from 1 at term 1
INFO 1 became leader at term 1
# Add v2 with an explicit transition.
propose-conf-change 1 transition=explicit
v2
----
ok
# Pull n2 out of thin air.
add-nodes 1
----
INFO 2 switched to configuration voters=()
INFO 2 became follower at term 0
INFO newRaft 2 [peers: [], term: 0, commit: 0, applied: 0, lastindex: 0, lastterm: 0]
# n1 commits the conf change using itself as commit quorum, then starts catching up n2.
# Everyone remains in the joint config. Note that the snapshot below has AutoLeave unset.
stabilize 1 2
----
> 1 handling Ready
Ready MustSync=true:
Lead:1 State:StateLeader
HardState Term:1 Vote:1 Commit:4
Entries:
1/3 EntryNormal ""
1/4 EntryConfChangeV2 v2
CommittedEntries:
1/3 EntryNormal ""
1/4 EntryConfChangeV2 v2
INFO 1 switched to configuration voters=(1 2)&&(1)
> 1 handling Ready
Ready MustSync=false:
Messages:
1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2]
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2]
INFO 2 [term: 0] received a MsgApp message with higher term from 1 [term: 1]
INFO 2 became follower at term 1
DEBUG 2 [logterm: 0, index: 3] rejected MsgApp [logterm: 1, index: 3] from 1
> 2 handling Ready
Ready MustSync=true:
Lead:1 State:StateFollower
HardState Term:1 Commit:0
Messages:
2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
DEBUG 1 received MsgAppResp(rejected, hint: (index 0, term 0)) from 2 for index 3
DEBUG 1 decreased progress of 2 to [StateProbe match=0 next=1]
DEBUG 1 [firstindex: 3, commit: 4] sent snapshot[index: 4, term: 1] to 2 [StateProbe match=0 next=1]
DEBUG 1 paused sending replication messages to 2 [StateSnapshot match=0 next=1 paused pendingSnap=4]
> 1 handling Ready
Ready MustSync=false:
Messages:
1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:false
> 2 receiving messages
1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:false
INFO log [committed=0, applied=0, unstable.offset=1, len(unstable.Entries)=0] starts to restore snapshot [index: 4, term: 1]
INFO 2 switched to configuration voters=(1 2)&&(1)
INFO 2 [commit: 4, lastindex: 4, lastterm: 1] restored snapshot [index: 4, term: 1]
INFO 2 [commit: 4] restored snapshot [index: 4, term: 1]
> 2 handling Ready
Ready MustSync=false:
HardState Term:1 Commit:4
Snapshot Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:false
Messages:
2->1 MsgAppResp Term:1 Log:0/4
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/4
DEBUG 1 recovered from needing snapshot, resumed sending replication messages to 2 [StateSnapshot match=4 next=5 paused pendingSnap=4]
> 1 handling Ready
Ready MustSync=false:
Messages:
1->2 MsgApp Term:1 Log:1/4 Commit:4
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/4 Commit:4
> 2 handling Ready
Ready MustSync=false:
Messages:
2->1 MsgAppResp Term:1 Log:0/4
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/4
# Check that we're not allowed to change membership again while in the joint state.
# This leads to an empty entry being proposed instead (index 5 in the stabilize block
# below).
propose-conf-change 1
v3 v4 v5
----
INFO 1 ignoring conf change {ConfChangeTransitionAuto [{ConfChangeAddNode 3} {ConfChangeAddNode 4} {ConfChangeAddNode 5}] []} at config voters=(1 2)&&(1): must transition out of joint config first
# Propose a transition out of the joint config. We'll see this at index 6 below.
propose-conf-change 1
----
ok
# The group commits the command and everyone switches to the final config.
stabilize
----
> 1 handling Ready
Ready MustSync=true:
Entries:
1/5 EntryNormal ""
1/6 EntryConfChangeV2
Messages:
1->2 MsgApp Term:1 Log:1/4 Commit:4 Entries:[1/5 EntryNormal ""]
1->2 MsgApp Term:1 Log:1/5 Commit:4 Entries:[1/6 EntryConfChangeV2]
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/4 Commit:4 Entries:[1/5 EntryNormal ""]
1->2 MsgApp Term:1 Log:1/5 Commit:4 Entries:[1/6 EntryConfChangeV2]
> 2 handling Ready
Ready MustSync=true:
Entries:
1/5 EntryNormal ""
1/6 EntryConfChangeV2
Messages:
2->1 MsgAppResp Term:1 Log:0/5
2->1 MsgAppResp Term:1 Log:0/6
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/5
2->1 MsgAppResp Term:1 Log:0/6
> 1 handling Ready
Ready MustSync=false:
HardState Term:1 Vote:1 Commit:6
CommittedEntries:
1/5 EntryNormal ""
1/6 EntryConfChangeV2
Messages:
1->2 MsgApp Term:1 Log:1/6 Commit:5
1->2 MsgApp Term:1 Log:1/6 Commit:6
INFO 1 switched to configuration voters=(1 2)
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/6 Commit:5
1->2 MsgApp Term:1 Log:1/6 Commit:6
> 2 handling Ready
Ready MustSync=false:
HardState Term:1 Commit:6
CommittedEntries:
1/5 EntryNormal ""
1/6 EntryConfChangeV2
Messages:
2->1 MsgAppResp Term:1 Log:0/6
2->1 MsgAppResp Term:1 Log:0/6
INFO 2 switched to configuration voters=(1 2)
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/6
2->1 MsgAppResp Term:1 Log:0/6
# Check that trying to transition out again won't do anything.
propose-conf-change 1
----
INFO 1 ignoring conf change {ConfChangeTransitionAuto [] []} at config voters=(1 2): not in joint state; refusing empty conf change
# Finishes work for the empty entry we just proposed.
stabilize
----
> 1 handling Ready
Ready MustSync=true:
Entries:
1/7 EntryNormal ""
Messages:
1->2 MsgApp Term:1 Log:1/6 Commit:6 Entries:[1/7 EntryNormal ""]
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/6 Commit:6 Entries:[1/7 EntryNormal ""]
> 2 handling Ready
Ready MustSync=true:
Entries:
1/7 EntryNormal ""
Messages:
2->1 MsgAppResp Term:1 Log:0/7
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/7
> 1 handling Ready
Ready MustSync=false:
HardState Term:1 Vote:1 Commit:7
CommittedEntries:
1/7 EntryNormal ""
Messages:
1->2 MsgApp Term:1 Log:1/7 Commit:7
> 2 receiving messages
1->2 MsgApp Term:1 Log:1/7 Commit:7
> 2 handling Ready
Ready MustSync=false:
HardState Term:1 Commit:7
CommittedEntries:
1/7 EntryNormal ""
Messages:
2->1 MsgAppResp Term:1 Log:0/7
> 1 receiving messages
2->1 MsgAppResp Term:1 Log:0/7

View File

@ -1,767 +0,0 @@
# This test creates a complete Raft log configuration and demonstrates how a
# leader probes and replicates to each of its followers. The log configuration
# constructed is almost[*] identical to the one present in Figure 7 of the raft
# paper (https://raft.github.io/raft.pdf), which looks like:
#
# 1 2 3 4 5 6 7 8 9 10 11 12
# n1: [1][1][1][4][4][5][5][6][6][6]
# n2: [1][1][1][4][4][5][5][6][6]
# n3: [1][1][1][4]
# n4: [1][1][1][4][4][5][5][6][6][6][6]
# n5: [1][1][1][4][4][5][5][6][7][7][7][7]
# n6: [1][1][1][4][4][4][4]
# n7: [1][1][1][2][2][2][3][3][3][3][3]
#
# Once in this state, we then elect node 1 as the leader and stabilize the
# entire raft group. This demonstrates how a newly elected leader probes for
# matching indexes, overwrites conflicting entries, and catches up all
# followers.
#
# [*] the only differences are:
# 1. n5 is given a larger uncommitted log tail, which is used to demonstrate a
# follower-side probing optimization.
# 2. the log indexes are shifted by 10 in this test because add-nodes wants to
# start with an index > 1.
#
# Set up the log configuration. This is mostly unintersting, but the order of
# each leadership change and the nodes that are allowed to hear about them is
# very important. Most readers of this test can skip this section.
log-level none
----
ok
## Start with seven nodes.
add-nodes 7 voters=(1,2,3,4,5,6,7) index=10
----
ok
## Create term 1 entries.
campaign 1
----
ok
stabilize
----
ok (quiet)
propose 1 prop_1_12
----
ok
propose 1 prop_1_13
----
ok
stabilize
----
ok (quiet)
## Create term 2 entries.
campaign 2
----
ok
stabilize 2
----
ok (quiet)
stabilize 6
----
ok (quiet)
stabilize 2 5 7
----
ok (quiet)
propose 2 prop_2_15
----
ok
propose 2 prop_2_16
----
ok
stabilize 2 7
----
ok (quiet)
deliver-msgs drop=(1,2,3,4,5,6,7)
----
ok (quiet)
## Create term 3 entries.
campaign 7
----
ok
stabilize 7
----
ok (quiet)
stabilize 1 2 3 4 5 6
----
ok (quiet)
stabilize 7
----
ok (quiet)
propose 7 prop_3_18
----
ok
propose 7 prop_3_19
----
ok
propose 7 prop_3_20
----
ok
propose 7 prop_3_21
----
ok
stabilize 7
----
ok (quiet)
deliver-msgs drop=(1,2,3,4,5,6,7)
----
ok (quiet)
## Create term 4 entries.
campaign 6
----
ok
stabilize 1 2 3 4 5 6
----
ok (quiet)
propose 6 prop_4_15
----
ok
stabilize 1 2 4 5 6
----
ok (quiet)
propose 6 prop_4_16
----
ok
propose 6 prop_4_17
----
ok
stabilize 6
----
ok (quiet)
deliver-msgs drop=(1,2,3,4,5,6,7)
----
ok (quiet)
## Create term 5 entries.
campaign 5
----
ok
stabilize 1 2 4 5
----
ok (quiet)
propose 5 prop_5_17
----
ok
stabilize 1 2 4 5
----
ok (quiet)
deliver-msgs drop=(1,2,3,4,5,6,7)
----
ok (quiet)
## Create term 6 entries.
campaign 4
----
ok
stabilize 1 2 4 5
----
ok (quiet)
propose 4 prop_6_19
----
ok
stabilize 1 2 4
----
ok (quiet)
propose 4 prop_6_20
----
ok
stabilize 1 4
----
ok (quiet)
propose 4 prop_6_21
----
ok
stabilize 4
----
ok (quiet)
deliver-msgs drop=(1,2,3,4,5,6,7)
----
ok (quiet)
## Create term 7 entries.
campaign 5
----
ok
stabilize 5
----
ok (quiet)
stabilize 1 3 6 7
----
ok (quiet)
stabilize 5
----
ok (quiet)
propose 5 prop_7_20
----
ok
propose 5 prop_7_21
----
ok
propose 5 prop_7_22
----
ok
stabilize 5
----
ok (quiet)
deliver-msgs drop=(1,2,3,4,5,6,7)
----
ok (quiet)
# Show the Raft log from each node.
log-level info
----
ok
raft-log 1
----
1/11 EntryNormal ""
1/12 EntryNormal "prop_1_12"
1/13 EntryNormal "prop_1_13"
4/14 EntryNormal ""
4/15 EntryNormal "prop_4_15"
5/16 EntryNormal ""
5/17 EntryNormal "prop_5_17"
6/18 EntryNormal ""
6/19 EntryNormal "prop_6_19"
6/20 EntryNormal "prop_6_20"
raft-log 2
----
1/11 EntryNormal ""
1/12 EntryNormal "prop_1_12"
1/13 EntryNormal "prop_1_13"
4/14 EntryNormal ""
4/15 EntryNormal "prop_4_15"
5/16 EntryNormal ""
5/17 EntryNormal "prop_5_17"
6/18 EntryNormal ""
6/19 EntryNormal "prop_6_19"
raft-log 3
----
1/11 EntryNormal ""
1/12 EntryNormal "prop_1_12"
1/13 EntryNormal "prop_1_13"
4/14 EntryNormal ""
raft-log 4
----
1/11 EntryNormal ""
1/12 EntryNormal "prop_1_12"
1/13 EntryNormal "prop_1_13"
4/14 EntryNormal ""
4/15 EntryNormal "prop_4_15"
5/16 EntryNormal ""
5/17 EntryNormal "prop_5_17"
6/18 EntryNormal ""
6/19 EntryNormal "prop_6_19"
6/20 EntryNormal "prop_6_20"
6/21 EntryNormal "prop_6_21"
raft-log 5
----
1/11 EntryNormal ""
1/12 EntryNormal "prop_1_12"
1/13 EntryNormal "prop_1_13"
4/14 EntryNormal ""
4/15 EntryNormal "prop_4_15"
5/16 EntryNormal ""
5/17 EntryNormal "prop_5_17"
6/18 EntryNormal ""
7/19 EntryNormal ""
7/20 EntryNormal "prop_7_20"
7/21 EntryNormal "prop_7_21"
7/22 EntryNormal "prop_7_22"
raft-log 6
----
1/11 EntryNormal ""
1/12 EntryNormal "prop_1_12"
1/13 EntryNormal "prop_1_13"
4/14 EntryNormal ""
4/15 EntryNormal "prop_4_15"
4/16 EntryNormal "prop_4_16"
4/17 EntryNormal "prop_4_17"
raft-log 7
----
1/11 EntryNormal ""
1/12 EntryNormal "prop_1_12"
1/13 EntryNormal "prop_1_13"
2/14 EntryNormal ""
2/15 EntryNormal "prop_2_15"
2/16 EntryNormal "prop_2_16"
3/17 EntryNormal ""
3/18 EntryNormal "prop_3_18"
3/19 EntryNormal "prop_3_19"
3/20 EntryNormal "prop_3_20"
3/21 EntryNormal "prop_3_21"
# Elect node 1 as leader and stabilize.
campaign 1
----
INFO 1 is starting a new election at term 7
INFO 1 became candidate at term 8
INFO 1 received MsgVoteResp from 1 at term 8
INFO 1 [logterm: 6, index: 20] sent MsgVote request to 2 at term 8
INFO 1 [logterm: 6, index: 20] sent MsgVote request to 3 at term 8
INFO 1 [logterm: 6, index: 20] sent MsgVote request to 4 at term 8
INFO 1 [logterm: 6, index: 20] sent MsgVote request to 5 at term 8
INFO 1 [logterm: 6, index: 20] sent MsgVote request to 6 at term 8
INFO 1 [logterm: 6, index: 20] sent MsgVote request to 7 at term 8
## Get elected.
stabilize 1
----
> 1 handling Ready
Ready MustSync=true:
Lead:0 State:StateCandidate
HardState Term:8 Vote:1 Commit:18
Messages:
1->2 MsgVote Term:8 Log:6/20
1->3 MsgVote Term:8 Log:6/20
1->4 MsgVote Term:8 Log:6/20
1->5 MsgVote Term:8 Log:6/20
1->6 MsgVote Term:8 Log:6/20
1->7 MsgVote Term:8 Log:6/20
stabilize 2 3 4 5 6 7
----
> 2 receiving messages
1->2 MsgVote Term:8 Log:6/20
INFO 2 [term: 6] received a MsgVote message with higher term from 1 [term: 8]
INFO 2 became follower at term 8
INFO 2 [logterm: 6, index: 19, vote: 0] cast MsgVote for 1 [logterm: 6, index: 20] at term 8
> 3 receiving messages
1->3 MsgVote Term:8 Log:6/20
INFO 3 [term: 7] received a MsgVote message with higher term from 1 [term: 8]
INFO 3 became follower at term 8
INFO 3 [logterm: 4, index: 14, vote: 0] cast MsgVote for 1 [logterm: 6, index: 20] at term 8
> 4 receiving messages
1->4 MsgVote Term:8 Log:6/20
INFO 4 [term: 6] received a MsgVote message with higher term from 1 [term: 8]
INFO 4 became follower at term 8
INFO 4 [logterm: 6, index: 21, vote: 0] rejected MsgVote from 1 [logterm: 6, index: 20] at term 8
> 5 receiving messages
1->5 MsgVote Term:8 Log:6/20
INFO 5 [term: 7] received a MsgVote message with higher term from 1 [term: 8]
INFO 5 became follower at term 8
INFO 5 [logterm: 7, index: 22, vote: 0] rejected MsgVote from 1 [logterm: 6, index: 20] at term 8
> 6 receiving messages
1->6 MsgVote Term:8 Log:6/20
INFO 6 [term: 7] received a MsgVote message with higher term from 1 [term: 8]
INFO 6 became follower at term 8
INFO 6 [logterm: 4, index: 17, vote: 0] cast MsgVote for 1 [logterm: 6, index: 20] at term 8
> 7 receiving messages
1->7 MsgVote Term:8 Log:6/20
INFO 7 [term: 7] received a MsgVote message with higher term from 1 [term: 8]
INFO 7 became follower at term 8
INFO 7 [logterm: 3, index: 21, vote: 0] cast MsgVote for 1 [logterm: 6, index: 20] at term 8
> 2 handling Ready
Ready MustSync=true:
Lead:0 State:StateFollower
HardState Term:8 Vote:1 Commit:18
Messages:
2->1 MsgVoteResp Term:8 Log:0/0
> 3 handling Ready
Ready MustSync=true:
HardState Term:8 Vote:1 Commit:14
Messages:
3->1 MsgVoteResp Term:8 Log:0/0
> 4 handling Ready
Ready MustSync=true:
Lead:0 State:StateFollower
HardState Term:8 Commit:18
Messages:
4->1 MsgVoteResp Term:8 Log:0/0 Rejected (Hint: 0)
> 5 handling Ready
Ready MustSync=true:
Lead:0 State:StateFollower
HardState Term:8 Commit:18
Messages:
5->1 MsgVoteResp Term:8 Log:0/0 Rejected (Hint: 0)
> 6 handling Ready
Ready MustSync=true:
HardState Term:8 Vote:1 Commit:15
Messages:
6->1 MsgVoteResp Term:8 Log:0/0
> 7 handling Ready
Ready MustSync=true:
HardState Term:8 Vote:1 Commit:13
Messages:
7->1 MsgVoteResp Term:8 Log:0/0
stabilize 1
----
> 1 receiving messages
2->1 MsgVoteResp Term:8 Log:0/0
INFO 1 received MsgVoteResp from 2 at term 8
INFO 1 has received 2 MsgVoteResp votes and 0 vote rejections
3->1 MsgVoteResp Term:8 Log:0/0
INFO 1 received MsgVoteResp from 3 at term 8
INFO 1 has received 3 MsgVoteResp votes and 0 vote rejections
4->1 MsgVoteResp Term:8 Log:0/0 Rejected (Hint: 0)
INFO 1 received MsgVoteResp rejection from 4 at term 8
INFO 1 has received 3 MsgVoteResp votes and 1 vote rejections
5->1 MsgVoteResp Term:8 Log:0/0 Rejected (Hint: 0)
INFO 1 received MsgVoteResp rejection from 5 at term 8
INFO 1 has received 3 MsgVoteResp votes and 2 vote rejections
6->1 MsgVoteResp Term:8 Log:0/0
INFO 1 received MsgVoteResp from 6 at term 8
INFO 1 has received 4 MsgVoteResp votes and 2 vote rejections
INFO 1 became leader at term 8
7->1 MsgVoteResp Term:8 Log:0/0
> 1 handling Ready
Ready MustSync=true:
Lead:1 State:StateLeader
Entries:
8/21 EntryNormal ""
Messages:
1->2 MsgApp Term:8 Log:6/20 Commit:18 Entries:[8/21 EntryNormal ""]
1->3 MsgApp Term:8 Log:6/20 Commit:18 Entries:[8/21 EntryNormal ""]
1->4 MsgApp Term:8 Log:6/20 Commit:18 Entries:[8/21 EntryNormal ""]
1->5 MsgApp Term:8 Log:6/20 Commit:18 Entries:[8/21 EntryNormal ""]
1->6 MsgApp Term:8 Log:6/20 Commit:18 Entries:[8/21 EntryNormal ""]
1->7 MsgApp Term:8 Log:6/20 Commit:18 Entries:[8/21 EntryNormal ""]
## Recover each follower, one by one.
stabilize 1 2
----
> 2 receiving messages
1->2 MsgApp Term:8 Log:6/20 Commit:18 Entries:[8/21 EntryNormal ""]
> 2 handling Ready
Ready MustSync=false:
Lead:1 State:StateFollower
Messages:
2->1 MsgAppResp Term:8 Log:6/20 Rejected (Hint: 19)
> 1 receiving messages
2->1 MsgAppResp Term:8 Log:6/20 Rejected (Hint: 19)
> 1 handling Ready
Ready MustSync=false:
Messages:
1->2 MsgApp Term:8 Log:6/19 Commit:18 Entries:[6/20 EntryNormal "prop_6_20", 8/21 EntryNormal ""]
> 2 receiving messages
1->2 MsgApp Term:8 Log:6/19 Commit:18 Entries:[6/20 EntryNormal "prop_6_20", 8/21 EntryNormal ""]
> 2 handling Ready
Ready MustSync=true:
Entries:
6/20 EntryNormal "prop_6_20"
8/21 EntryNormal ""
Messages:
2->1 MsgAppResp Term:8 Log:0/21
> 1 receiving messages
2->1 MsgAppResp Term:8 Log:0/21
> 1 handling Ready
Ready MustSync=false:
Messages:
1->2 MsgApp Term:8 Log:8/21 Commit:18
> 2 receiving messages
1->2 MsgApp Term:8 Log:8/21 Commit:18
> 2 handling Ready
Ready MustSync=false:
Messages:
2->1 MsgAppResp Term:8 Log:0/21
> 1 receiving messages
2->1 MsgAppResp Term:8 Log:0/21
stabilize 1 3
----
> 3 receiving messages
1->3 MsgApp Term:8 Log:6/20 Commit:18 Entries:[8/21 EntryNormal ""]
> 3 handling Ready
Ready MustSync=false:
Lead:1 State:StateFollower
Messages:
3->1 MsgAppResp Term:8 Log:4/20 Rejected (Hint: 14)
> 1 receiving messages
3->1 MsgAppResp Term:8 Log:4/20 Rejected (Hint: 14)
> 1 handling Ready
Ready MustSync=false:
Messages:
1->3 MsgApp Term:8 Log:4/14 Commit:18 Entries:[4/15 EntryNormal "prop_4_15", 5/16 EntryNormal "", 5/17 EntryNormal "prop_5_17", 6/18 EntryNormal "", 6/19 EntryNormal "prop_6_19", 6/20 EntryNormal "prop_6_20", 8/21 EntryNormal ""]
> 3 receiving messages
1->3 MsgApp Term:8 Log:4/14 Commit:18 Entries:[4/15 EntryNormal "prop_4_15", 5/16 EntryNormal "", 5/17 EntryNormal "prop_5_17", 6/18 EntryNormal "", 6/19 EntryNormal "prop_6_19", 6/20 EntryNormal "prop_6_20", 8/21 EntryNormal ""]
> 3 handling Ready
Ready MustSync=true:
HardState Term:8 Vote:1 Commit:18
Entries:
4/15 EntryNormal "prop_4_15"
5/16 EntryNormal ""
5/17 EntryNormal "prop_5_17"
6/18 EntryNormal ""
6/19 EntryNormal "prop_6_19"
6/20 EntryNormal "prop_6_20"
8/21 EntryNormal ""
CommittedEntries:
4/15 EntryNormal "prop_4_15"
5/16 EntryNormal ""
5/17 EntryNormal "prop_5_17"
6/18 EntryNormal ""
Messages:
3->1 MsgAppResp Term:8 Log:0/21
> 1 receiving messages
3->1 MsgAppResp Term:8 Log:0/21
> 1 handling Ready
Ready MustSync=false:
Messages:
1->3 MsgApp Term:8 Log:8/21 Commit:18
> 3 receiving messages
1->3 MsgApp Term:8 Log:8/21 Commit:18
> 3 handling Ready
Ready MustSync=false:
Messages:
3->1 MsgAppResp Term:8 Log:0/21
> 1 receiving messages
3->1 MsgAppResp Term:8 Log:0/21
stabilize 1 4
----
> 4 receiving messages
1->4 MsgApp Term:8 Log:6/20 Commit:18 Entries:[8/21 EntryNormal ""]
INFO found conflict at index 21 [existing term: 6, conflicting term: 8]
INFO replace the unstable entries from index 21
> 4 handling Ready
Ready MustSync=true:
Lead:1 State:StateFollower
Entries:
8/21 EntryNormal ""
Messages:
4->1 MsgAppResp Term:8 Log:0/21
> 1 receiving messages
4->1 MsgAppResp Term:8 Log:0/21
> 1 handling Ready
Ready MustSync=false:
HardState Term:8 Vote:1 Commit:21
CommittedEntries:
6/19 EntryNormal "prop_6_19"
6/20 EntryNormal "prop_6_20"
8/21 EntryNormal ""
Messages:
1->2 MsgApp Term:8 Log:8/21 Commit:21
1->3 MsgApp Term:8 Log:8/21 Commit:21
1->4 MsgApp Term:8 Log:8/21 Commit:21
> 4 receiving messages
1->4 MsgApp Term:8 Log:8/21 Commit:21
> 4 handling Ready
Ready MustSync=false:
HardState Term:8 Commit:21
CommittedEntries:
6/19 EntryNormal "prop_6_19"
6/20 EntryNormal "prop_6_20"
8/21 EntryNormal ""
Messages:
4->1 MsgAppResp Term:8 Log:0/21
> 1 receiving messages
4->1 MsgAppResp Term:8 Log:0/21
stabilize 1 5
----
> 5 receiving messages
1->5 MsgApp Term:8 Log:6/20 Commit:18 Entries:[8/21 EntryNormal ""]
> 5 handling Ready
Ready MustSync=false:
Lead:1 State:StateFollower
Messages:
5->1 MsgAppResp Term:8 Log:6/20 Rejected (Hint: 18)
> 1 receiving messages
5->1 MsgAppResp Term:8 Log:6/20 Rejected (Hint: 18)
> 1 handling Ready
Ready MustSync=false:
Messages:
1->5 MsgApp Term:8 Log:6/18 Commit:21 Entries:[6/19 EntryNormal "prop_6_19", 6/20 EntryNormal "prop_6_20", 8/21 EntryNormal ""]
> 5 receiving messages
1->5 MsgApp Term:8 Log:6/18 Commit:21 Entries:[6/19 EntryNormal "prop_6_19", 6/20 EntryNormal "prop_6_20", 8/21 EntryNormal ""]
INFO found conflict at index 19 [existing term: 7, conflicting term: 6]
INFO replace the unstable entries from index 19
> 5 handling Ready
Ready MustSync=true:
HardState Term:8 Commit:21
Entries:
6/19 EntryNormal "prop_6_19"
6/20 EntryNormal "prop_6_20"
8/21 EntryNormal ""
CommittedEntries:
6/19 EntryNormal "prop_6_19"
6/20 EntryNormal "prop_6_20"
8/21 EntryNormal ""
Messages:
5->1 MsgAppResp Term:8 Log:0/21
> 1 receiving messages
5->1 MsgAppResp Term:8 Log:0/21
> 1 handling Ready
Ready MustSync=false:
Messages:
1->5 MsgApp Term:8 Log:8/21 Commit:21
> 5 receiving messages
1->5 MsgApp Term:8 Log:8/21 Commit:21
> 5 handling Ready
Ready MustSync=false:
Messages:
5->1 MsgAppResp Term:8 Log:0/21
> 1 receiving messages
5->1 MsgAppResp Term:8 Log:0/21
stabilize 1 6
----
> 6 receiving messages
1->6 MsgApp Term:8 Log:6/20 Commit:18 Entries:[8/21 EntryNormal ""]
> 6 handling Ready
Ready MustSync=false:
Lead:1 State:StateFollower
Messages:
6->1 MsgAppResp Term:8 Log:4/20 Rejected (Hint: 17)
> 1 receiving messages
6->1 MsgAppResp Term:8 Log:4/20 Rejected (Hint: 17)
> 1 handling Ready
Ready MustSync=false:
Messages:
1->6 MsgApp Term:8 Log:4/15 Commit:21 Entries:[5/16 EntryNormal "", 5/17 EntryNormal "prop_5_17", 6/18 EntryNormal "", 6/19 EntryNormal "prop_6_19", 6/20 EntryNormal "prop_6_20", 8/21 EntryNormal ""]
> 6 receiving messages
1->6 MsgApp Term:8 Log:4/15 Commit:21 Entries:[5/16 EntryNormal "", 5/17 EntryNormal "prop_5_17", 6/18 EntryNormal "", 6/19 EntryNormal "prop_6_19", 6/20 EntryNormal "prop_6_20", 8/21 EntryNormal ""]
INFO found conflict at index 16 [existing term: 4, conflicting term: 5]
INFO replace the unstable entries from index 16
> 6 handling Ready
Ready MustSync=true:
HardState Term:8 Vote:1 Commit:21
Entries:
5/16 EntryNormal ""
5/17 EntryNormal "prop_5_17"
6/18 EntryNormal ""
6/19 EntryNormal "prop_6_19"
6/20 EntryNormal "prop_6_20"
8/21 EntryNormal ""
CommittedEntries:
5/16 EntryNormal ""
5/17 EntryNormal "prop_5_17"
6/18 EntryNormal ""
6/19 EntryNormal "prop_6_19"
6/20 EntryNormal "prop_6_20"
8/21 EntryNormal ""
Messages:
6->1 MsgAppResp Term:8 Log:0/21
> 1 receiving messages
6->1 MsgAppResp Term:8 Log:0/21
> 1 handling Ready
Ready MustSync=false:
Messages:
1->6 MsgApp Term:8 Log:8/21 Commit:21
> 6 receiving messages
1->6 MsgApp Term:8 Log:8/21 Commit:21
> 6 handling Ready
Ready MustSync=false:
Messages:
6->1 MsgAppResp Term:8 Log:0/21
> 1 receiving messages
6->1 MsgAppResp Term:8 Log:0/21
stabilize 1 7
----
> 7 receiving messages
1->7 MsgApp Term:8 Log:6/20 Commit:18 Entries:[8/21 EntryNormal ""]
> 7 handling Ready
Ready MustSync=false:
Lead:1 State:StateFollower
Messages:
7->1 MsgAppResp Term:8 Log:3/20 Rejected (Hint: 20)
> 1 receiving messages
7->1 MsgAppResp Term:8 Log:3/20 Rejected (Hint: 20)
> 1 handling Ready
Ready MustSync=false:
Messages:
1->7 MsgApp Term:8 Log:1/13 Commit:21 Entries:[4/14 EntryNormal "", 4/15 EntryNormal "prop_4_15", 5/16 EntryNormal "", 5/17 EntryNormal "prop_5_17", 6/18 EntryNormal "", 6/19 EntryNormal "prop_6_19", 6/20 EntryNormal "prop_6_20", 8/21 EntryNormal ""]
> 7 receiving messages
1->7 MsgApp Term:8 Log:1/13 Commit:21 Entries:[4/14 EntryNormal "", 4/15 EntryNormal "prop_4_15", 5/16 EntryNormal "", 5/17 EntryNormal "prop_5_17", 6/18 EntryNormal "", 6/19 EntryNormal "prop_6_19", 6/20 EntryNormal "prop_6_20", 8/21 EntryNormal ""]
INFO found conflict at index 14 [existing term: 2, conflicting term: 4]
INFO replace the unstable entries from index 14
> 7 handling Ready
Ready MustSync=true:
HardState Term:8 Vote:1 Commit:21
Entries:
4/14 EntryNormal ""
4/15 EntryNormal "prop_4_15"
5/16 EntryNormal ""
5/17 EntryNormal "prop_5_17"
6/18 EntryNormal ""
6/19 EntryNormal "prop_6_19"
6/20 EntryNormal "prop_6_20"
8/21 EntryNormal ""
CommittedEntries:
4/14 EntryNormal ""
4/15 EntryNormal "prop_4_15"
5/16 EntryNormal ""
5/17 EntryNormal "prop_5_17"
6/18 EntryNormal ""
6/19 EntryNormal "prop_6_19"
6/20 EntryNormal "prop_6_20"
8/21 EntryNormal ""
Messages:
7->1 MsgAppResp Term:8 Log:0/21
> 1 receiving messages
7->1 MsgAppResp Term:8 Log:0/21
> 1 handling Ready
Ready MustSync=false:
Messages:
1->7 MsgApp Term:8 Log:8/21 Commit:21
> 7 receiving messages
1->7 MsgApp Term:8 Log:8/21 Commit:21
> 7 handling Ready
Ready MustSync=false:
Messages:
7->1 MsgAppResp Term:8 Log:0/21
> 1 receiving messages
7->1 MsgAppResp Term:8 Log:0/21

View File

@ -1,156 +0,0 @@
# TestSnapshotSucceedViaAppResp regression tests the situation in which a snap-
# shot is sent to a follower at the most recent index (i.e. the snapshot index
# is the leader's last index is the committed index). In that situation, a bug
# in the past left the follower in probing status until the next log entry was
# committed.
#
# See https://github.com/etcd-io/etcd/pull/10308 for additional background.
# Turn off output during the setup of the test.
log-level none
----
ok
# Start with two nodes, but the config already has a third.
add-nodes 2 voters=(1,2,3) index=10
----
ok
campaign 1
----
ok
# Fully replicate everything, including the leader's empty index.
stabilize
----
ok (quiet)
compact 1 11
----
ok (quiet)
# Drop inflight messages to n3.
deliver-msgs drop=(3)
----
ok (quiet)
# Show the Raft log messages from now on.
log-level debug
----
ok
status 1
----
1: StateReplicate match=11 next=12 inactive
2: StateReplicate match=11 next=12
3: StateProbe match=0 next=11 paused inactive
# Add the node that will receive a snapshot (it has no state at all, does not
# even have a config).
add-nodes 1
----
INFO 3 switched to configuration voters=()
INFO 3 became follower at term 0
INFO newRaft 3 [peers: [], term: 0, commit: 0, applied: 0, lastindex: 0, lastterm: 0]
# Time passes on the leader so that it will try the previously missing follower
# again.
tick-heartbeat 1
----
ok
process-ready 1
----
Ready MustSync=false:
Messages:
1->2 MsgHeartbeat Term:1 Log:0/0 Commit:11
1->3 MsgHeartbeat Term:1 Log:0/0
# Iterate until no more work is done by the new peer. It receives the heartbeat
# and responds.
stabilize 3
----
> 3 receiving messages
1->3 MsgHeartbeat Term:1 Log:0/0
INFO 3 [term: 0] received a MsgHeartbeat message with higher term from 1 [term: 1]
INFO 3 became follower at term 1
> 3 handling Ready
Ready MustSync=true:
Lead:1 State:StateFollower
HardState Term:1 Commit:0
Messages:
3->1 MsgHeartbeatResp Term:1 Log:0/0
# The leader in turn will realize that n3 needs a snapshot, which it initiates.
stabilize 1
----
> 1 receiving messages
3->1 MsgHeartbeatResp Term:1 Log:0/0
DEBUG 1 [firstindex: 12, commit: 11] sent snapshot[index: 11, term: 1] to 3 [StateProbe match=0 next=11]
DEBUG 1 paused sending replication messages to 3 [StateSnapshot match=0 next=11 paused pendingSnap=11]
> 1 handling Ready
Ready MustSync=false:
Messages:
1->3 MsgSnap Term:1 Log:0/0 Snapshot: Index:11 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
status 1
----
1: StateReplicate match=11 next=12 inactive
2: StateReplicate match=11 next=12
3: StateSnapshot match=0 next=11 paused pendingSnap=11
# Follower applies the snapshot. Note how it reacts with a MsgAppResp upon completion.
# The snapshot fully catches the follower up (i.e. there are no more log entries it
# needs to apply after). The bug was that the leader failed to realize that the follower
# was now fully caught up.
stabilize 3
----
> 3 receiving messages
1->3 MsgSnap Term:1 Log:0/0 Snapshot: Index:11 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
INFO log [committed=0, applied=0, unstable.offset=1, len(unstable.Entries)=0] starts to restore snapshot [index: 11, term: 1]
INFO 3 switched to configuration voters=(1 2 3)
INFO 3 [commit: 11, lastindex: 11, lastterm: 1] restored snapshot [index: 11, term: 1]
INFO 3 [commit: 11] restored snapshot [index: 11, term: 1]
> 3 handling Ready
Ready MustSync=false:
HardState Term:1 Commit:11
Snapshot Index:11 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
Messages:
3->1 MsgAppResp Term:1 Log:0/11
# The MsgAppResp lets the leader move the follower back to replicating state.
# Leader sends another MsgAppResp, to communicate the updated commit index.
stabilize 1
----
> 1 receiving messages
3->1 MsgAppResp Term:1 Log:0/11
DEBUG 1 recovered from needing snapshot, resumed sending replication messages to 3 [StateSnapshot match=11 next=12 paused pendingSnap=11]
> 1 handling Ready
Ready MustSync=false:
Messages:
1->3 MsgApp Term:1 Log:1/11 Commit:11
status 1
----
1: StateReplicate match=11 next=12 inactive
2: StateReplicate match=11 next=12
3: StateReplicate match=11 next=12
# Let things settle.
stabilize
----
> 2 receiving messages
1->2 MsgHeartbeat Term:1 Log:0/0 Commit:11
> 3 receiving messages
1->3 MsgApp Term:1 Log:1/11 Commit:11
> 2 handling Ready
Ready MustSync=false:
Messages:
2->1 MsgHeartbeatResp Term:1 Log:0/0
> 3 handling Ready
Ready MustSync=false:
Messages:
3->1 MsgAppResp Term:1 Log:0/11
> 1 receiving messages
2->1 MsgHeartbeatResp Term:1 Log:0/0
3->1 MsgAppResp Term:1 Log:0/11

View File

@ -1,189 +0,0 @@
// Copyright 2019 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tracker
import (
"reflect"
"testing"
)
func TestInflightsAdd(t *testing.T) {
// no rotating case
in := &Inflights{
size: 10,
buffer: make([]uint64, 10),
}
for i := 0; i < 5; i++ {
in.Add(uint64(i))
}
wantIn := &Inflights{
start: 0,
count: 5,
size: 10,
// ↓------------
buffer: []uint64{0, 1, 2, 3, 4, 0, 0, 0, 0, 0},
}
if !reflect.DeepEqual(in, wantIn) {
t.Fatalf("in = %+v, want %+v", in, wantIn)
}
for i := 5; i < 10; i++ {
in.Add(uint64(i))
}
wantIn2 := &Inflights{
start: 0,
count: 10,
size: 10,
// ↓---------------------------
buffer: []uint64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
}
if !reflect.DeepEqual(in, wantIn2) {
t.Fatalf("in = %+v, want %+v", in, wantIn2)
}
// rotating case
in2 := &Inflights{
start: 5,
size: 10,
buffer: make([]uint64, 10),
}
for i := 0; i < 5; i++ {
in2.Add(uint64(i))
}
wantIn21 := &Inflights{
start: 5,
count: 5,
size: 10,
// ↓------------
buffer: []uint64{0, 0, 0, 0, 0, 0, 1, 2, 3, 4},
}
if !reflect.DeepEqual(in2, wantIn21) {
t.Fatalf("in = %+v, want %+v", in2, wantIn21)
}
for i := 5; i < 10; i++ {
in2.Add(uint64(i))
}
wantIn22 := &Inflights{
start: 5,
count: 10,
size: 10,
// -------------- ↓------------
buffer: []uint64{5, 6, 7, 8, 9, 0, 1, 2, 3, 4},
}
if !reflect.DeepEqual(in2, wantIn22) {
t.Fatalf("in = %+v, want %+v", in2, wantIn22)
}
}
func TestInflightFreeTo(t *testing.T) {
// no rotating case
in := NewInflights(10)
for i := 0; i < 10; i++ {
in.Add(uint64(i))
}
in.FreeLE(4)
wantIn := &Inflights{
start: 5,
count: 5,
size: 10,
// ↓------------
buffer: []uint64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
}
if !reflect.DeepEqual(in, wantIn) {
t.Fatalf("in = %+v, want %+v", in, wantIn)
}
in.FreeLE(8)
wantIn2 := &Inflights{
start: 9,
count: 1,
size: 10,
// ↓
buffer: []uint64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
}
if !reflect.DeepEqual(in, wantIn2) {
t.Fatalf("in = %+v, want %+v", in, wantIn2)
}
// rotating case
for i := 10; i < 15; i++ {
in.Add(uint64(i))
}
in.FreeLE(12)
wantIn3 := &Inflights{
start: 3,
count: 2,
size: 10,
// ↓-----
buffer: []uint64{10, 11, 12, 13, 14, 5, 6, 7, 8, 9},
}
if !reflect.DeepEqual(in, wantIn3) {
t.Fatalf("in = %+v, want %+v", in, wantIn3)
}
in.FreeLE(14)
wantIn4 := &Inflights{
start: 0,
count: 0,
size: 10,
// ↓
buffer: []uint64{10, 11, 12, 13, 14, 5, 6, 7, 8, 9},
}
if !reflect.DeepEqual(in, wantIn4) {
t.Fatalf("in = %+v, want %+v", in, wantIn4)
}
}
func TestInflightFreeFirstOne(t *testing.T) {
in := NewInflights(10)
for i := 0; i < 10; i++ {
in.Add(uint64(i))
}
in.FreeFirstOne()
wantIn := &Inflights{
start: 1,
count: 9,
size: 10,
// ↓------------------------
buffer: []uint64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
}
if !reflect.DeepEqual(in, wantIn) {
t.Fatalf("in = %+v, want %+v", in, wantIn)
}
}

View File

@ -1,189 +0,0 @@
// Copyright 2019 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tracker
import (
"reflect"
"testing"
)
func TestInflightsAdd(t *testing.T) {
// no rotating case
in := &Inflights{
size: 10,
buffer: make([]uint64, 10),
}
for i := 0; i < 5; i++ {
in.Add(uint64(i))
}
wantIn := &Inflights{
start: 0,
count: 5,
size: 10,
// ↓------------
buffer: []uint64{0, 1, 2, 3, 4, 0, 0, 0, 0, 0},
}
if !reflect.DeepEqual(in, wantIn) {
t.Fatalf("in = %+v, want %+v", in, wantIn)
}
for i := 5; i < 10; i++ {
in.Add(uint64(i))
}
wantIn2 := &Inflights{
start: 0,
count: 10,
size: 10,
// ↓---------------------------
buffer: []uint64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
}
if !reflect.DeepEqual(in, wantIn2) {
t.Fatalf("in = %+v, want %+v", in, wantIn2)
}
// rotating case
in2 := &Inflights{
start: 5,
size: 10,
buffer: make([]uint64, 10),
}
for i := 0; i < 5; i++ {
in2.Add(uint64(i))
}
wantIn21 := &Inflights{
start: 5,
count: 5,
size: 10,
// ↓------------
buffer: []uint64{0, 0, 0, 0, 0, 0, 1, 2, 3, 4},
}
if !reflect.DeepEqual(in2, wantIn21) {
t.Fatalf("in = %+v, want %+v", in2, wantIn21)
}
for i := 5; i < 10; i++ {
in2.Add(uint64(i))
}
wantIn22 := &Inflights{
start: 5,
count: 10,
size: 10,
// -------------- ↓------------
buffer: []uint64{5, 6, 7, 8, 9, 0, 1, 2, 3, 4},
}
if !reflect.DeepEqual(in2, wantIn22) {
t.Fatalf("in = %+v, want %+v", in2, wantIn22)
}
}
func TestInflightFreeTo(t *testing.T) {
// no rotating case
in := NewInflights(10)
for i := 0; i < 10; i++ {
in.Add(uint64(i))
}
in.FreeLE(4)
wantIn := &Inflights{
start: 5,
count: 5,
size: 10,
// ↓------------
buffer: []uint64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
}
if !reflect.DeepEqual(in, wantIn) {
t.Fatalf("in = %+v, want %+v", in, wantIn)
}
in.FreeLE(8)
wantIn2 := &Inflights{
start: 9,
count: 1,
size: 10,
// ↓
buffer: []uint64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
}
if !reflect.DeepEqual(in, wantIn2) {
t.Fatalf("in = %+v, want %+v", in, wantIn2)
}
// rotating case
for i := 10; i < 15; i++ {
in.Add(uint64(i))
}
in.FreeLE(12)
wantIn3 := &Inflights{
start: 3,
count: 2,
size: 10,
// ↓-----
buffer: []uint64{10, 11, 12, 13, 14, 5, 6, 7, 8, 9},
}
if !reflect.DeepEqual(in, wantIn3) {
t.Fatalf("in = %+v, want %+v", in, wantIn3)
}
in.FreeLE(14)
wantIn4 := &Inflights{
start: 0,
count: 0,
size: 10,
// ↓
buffer: []uint64{10, 11, 12, 13, 14, 5, 6, 7, 8, 9},
}
if !reflect.DeepEqual(in, wantIn4) {
t.Fatalf("in = %+v, want %+v", in, wantIn4)
}
}
func TestInflightFreeFirstOne(t *testing.T) {
in := NewInflights(10)
for i := 0; i < 10; i++ {
in.Add(uint64(i))
}
in.FreeFirstOne()
wantIn := &Inflights{
start: 1,
count: 9,
size: 10,
// ↓------------------------
buffer: []uint64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
}
if !reflect.DeepEqual(in, wantIn) {
t.Fatalf("in = %+v, want %+v", in, wantIn)
}
}

View File

@ -1,106 +0,0 @@
// Copyright 2015 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package raft
import (
"math"
"reflect"
"strings"
"testing"
pb "go.etcd.io/etcd/raft/v3/raftpb"
)
var testFormatter EntryFormatter = func(data []byte) string {
return strings.ToUpper(string(data))
}
func TestDescribeEntry(t *testing.T) {
entry := pb.Entry{
Term: 1,
Index: 2,
Type: pb.EntryNormal,
Data: []byte("hello\x00world"),
}
defaultFormatted := DescribeEntry(entry, nil)
if defaultFormatted != "1/2 EntryNormal \"hello\\x00world\"" {
t.Errorf("unexpected default output: %s", defaultFormatted)
}
customFormatted := DescribeEntry(entry, testFormatter)
if customFormatted != "1/2 EntryNormal HELLO\x00WORLD" {
t.Errorf("unexpected custom output: %s", customFormatted)
}
}
func TestLimitSize(t *testing.T) {
ents := []pb.Entry{{Index: 4, Term: 4}, {Index: 5, Term: 5}, {Index: 6, Term: 6}}
tests := []struct {
maxsize uint64
wentries []pb.Entry
}{
{math.MaxUint64, []pb.Entry{{Index: 4, Term: 4}, {Index: 5, Term: 5}, {Index: 6, Term: 6}}},
// even if maxsize is zero, the first entry should be returned
{0, []pb.Entry{{Index: 4, Term: 4}}},
// limit to 2
{uint64(ents[0].Size() + ents[1].Size()), []pb.Entry{{Index: 4, Term: 4}, {Index: 5, Term: 5}}},
// limit to 2
{uint64(ents[0].Size() + ents[1].Size() + ents[2].Size()/2), []pb.Entry{{Index: 4, Term: 4}, {Index: 5, Term: 5}}},
{uint64(ents[0].Size() + ents[1].Size() + ents[2].Size() - 1), []pb.Entry{{Index: 4, Term: 4}, {Index: 5, Term: 5}}},
// all
{uint64(ents[0].Size() + ents[1].Size() + ents[2].Size()), []pb.Entry{{Index: 4, Term: 4}, {Index: 5, Term: 5}, {Index: 6, Term: 6}}},
}
for i, tt := range tests {
if !reflect.DeepEqual(limitSize(ents, tt.maxsize), tt.wentries) {
t.Errorf("#%d: entries = %v, want %v", i, limitSize(ents, tt.maxsize), tt.wentries)
}
}
}
func TestIsLocalMsg(t *testing.T) {
tests := []struct {
msgt pb.MessageType
isLocal bool
}{
{pb.MsgHup, true},
{pb.MsgBeat, true},
{pb.MsgUnreachable, true},
{pb.MsgSnapStatus, true},
{pb.MsgCheckQuorum, true},
{pb.MsgTransferLeader, false},
{pb.MsgProp, false},
{pb.MsgApp, false},
{pb.MsgAppResp, false},
{pb.MsgVote, false},
{pb.MsgVoteResp, false},
{pb.MsgSnap, false},
{pb.MsgHeartbeat, false},
{pb.MsgHeartbeatResp, false},
{pb.MsgTimeoutNow, false},
{pb.MsgReadIndex, false},
{pb.MsgReadIndexResp, false},
{pb.MsgPreVote, false},
{pb.MsgPreVoteResp, false},
}
for i, tt := range tests {
got := IsLocalMsg(tt.msgt)
if got != tt.isLocal {
t.Errorf("#%d: got %v, want %v", i, got, tt.isLocal)
}
}
}

View File

@ -6,7 +6,7 @@
# Go tests use this curl binary for integration tests.
#
FROM ubuntu:trusty@sha256:881afbae521c910f764f7187dbfbca3cc10c26f8bafa458c76dda009a901c29d
FROM ubuntu:trusty
RUN apt-get update && \
apt-get upgrade -y && \

6
vendor/modules.txt vendored
View File

@ -3,6 +3,7 @@
# github.com/Shopify/sarama v1.33.0
## explicit; go 1.16
github.com/Shopify/sarama
github.com/Shopify/sarama/mocks
# github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5
## explicit
github.com/afex/hystrix-go/hystrix
@ -364,7 +365,7 @@ github.com/spf13/cobra
## explicit; go 1.13
github.com/stretchr/testify/assert
github.com/stretchr/testify/require
# github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c
# github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c => github.com/Cloudstriff/gorocksdb v1.0.1
## explicit
github.com/tecbot/gorocksdb
# github.com/tklauser/go-sysconf v0.3.11
@ -414,9 +415,6 @@ golang.org/x/net/trace
## explicit
golang.org/x/sync/errgroup
golang.org/x/sync/singleflight
## explicit
golang.org/x/sync/errgroup
golang.org/x/sync/singleflight
# golang.org/x/sys v0.7.0
## explicit; go 1.17
golang.org/x/sys/internal/unsafeheader