mirror of
https://github.com/etcd-io/etcd.git
synced 2026-02-05 15:46:51 +01:00
In this commit, we don't load v2 snapshot files (*.snap) anymore, instead we just pick the latest snapshot entry from WAL. We don't need the raftpb.Snapshot.Data anymore, so it's correct to just read the latest snapshot entry from WAL. The end goal is to completely get rid of v2 snapshot, which means we don't read v2 snapshot, nor write it. But in order to be compatible with 3.6, we still need to write v2 snapshot. This PR just stops reading v2 snapshot, so that it's safe to completely remove it in 3.8. Signed-off-by: Benjamin Wang <benjamin.ahrtr@gmail.com>
310 lines
9.1 KiB
Go
310 lines
9.1 KiB
Go
// 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 version implements etcd version parsing and contains latest version
|
|
// information.
|
|
|
|
package etcdserver
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap/zaptest"
|
|
|
|
bolt "go.etcd.io/bbolt"
|
|
"go.etcd.io/etcd/api/v3/etcdserverpb"
|
|
"go.etcd.io/etcd/api/v3/version"
|
|
"go.etcd.io/etcd/client/pkg/v3/types"
|
|
"go.etcd.io/etcd/server/v3/config"
|
|
"go.etcd.io/etcd/server/v3/etcdserver/api/membership"
|
|
"go.etcd.io/etcd/server/v3/etcdserver/api/snap"
|
|
serverstorage "go.etcd.io/etcd/server/v3/storage"
|
|
"go.etcd.io/etcd/server/v3/storage/datadir"
|
|
"go.etcd.io/etcd/server/v3/storage/schema"
|
|
"go.etcd.io/etcd/server/v3/storage/wal"
|
|
"go.etcd.io/etcd/server/v3/storage/wal/walpb"
|
|
"go.etcd.io/raft/v3/raftpb"
|
|
)
|
|
|
|
func TestBootstrapExistingClusterNoWALMaxLearner(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
members []etcdserverpb.Member
|
|
maxLearner int
|
|
hasError bool
|
|
expectedError error
|
|
}{
|
|
{
|
|
name: "bootstrap success: maxLearner gt learner count",
|
|
members: []etcdserverpb.Member{
|
|
{ID: 4512484362714696085, PeerURLs: []string{"http://localhost:2380"}},
|
|
{ID: 5321713336100798248, PeerURLs: []string{"http://localhost:2381"}},
|
|
{ID: 5670219998796287055, PeerURLs: []string{"http://localhost:2382"}},
|
|
},
|
|
maxLearner: 1,
|
|
hasError: false,
|
|
expectedError: nil,
|
|
},
|
|
{
|
|
name: "bootstrap success: maxLearner eq learner count",
|
|
members: []etcdserverpb.Member{
|
|
{ID: 4512484362714696085, PeerURLs: []string{"http://localhost:2380"}, IsLearner: true},
|
|
{ID: 5321713336100798248, PeerURLs: []string{"http://localhost:2381"}},
|
|
{ID: 5670219998796287055, PeerURLs: []string{"http://localhost:2382"}, IsLearner: true},
|
|
},
|
|
maxLearner: 2,
|
|
hasError: false,
|
|
expectedError: nil,
|
|
},
|
|
{
|
|
name: "bootstrap fail: maxLearner lt learner count",
|
|
members: []etcdserverpb.Member{
|
|
{ID: 4512484362714696085, PeerURLs: []string{"http://localhost:2380"}},
|
|
{ID: 5321713336100798248, PeerURLs: []string{"http://localhost:2381"}, IsLearner: true},
|
|
{ID: 5670219998796287055, PeerURLs: []string{"http://localhost:2382"}, IsLearner: true},
|
|
},
|
|
maxLearner: 1,
|
|
hasError: true,
|
|
expectedError: membership.ErrTooManyLearners,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cluster, err := types.NewURLsMap("node0=http://localhost:2380,node1=http://localhost:2381,node2=http://localhost:2382")
|
|
require.NoErrorf(t, err, "unexpected error: %v", err)
|
|
cfg := config.ServerConfig{
|
|
Name: "node0",
|
|
InitialPeerURLsMap: cluster,
|
|
Logger: zaptest.NewLogger(t),
|
|
MaxLearners: tt.maxLearner,
|
|
}
|
|
_, err = bootstrapExistingClusterNoWAL(cfg, mockBootstrapRoundTrip(tt.members))
|
|
hasError := err != nil
|
|
if hasError != tt.hasError {
|
|
t.Errorf("expected error: %v got: %v", tt.hasError, err)
|
|
}
|
|
if hasError {
|
|
require.Containsf(t, err.Error(), tt.expectedError.Error(), "expected error to contain: %q, got: %q", tt.expectedError.Error(), err.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type roundTripFunc func(r *http.Request) (*http.Response, error)
|
|
|
|
func (s roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
return s(r)
|
|
}
|
|
|
|
func mockBootstrapRoundTrip(members []etcdserverpb.Member) roundTripFunc {
|
|
return func(r *http.Request) (*http.Response, error) {
|
|
switch {
|
|
case strings.Contains(r.URL.String(), "/members"):
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(strings.NewReader(mockMembersJSON(members))),
|
|
Header: http.Header{"X-Etcd-Cluster-Id": []string{"f4588138892a16b0"}},
|
|
}, nil
|
|
case strings.Contains(r.URL.String(), "/version"):
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(strings.NewReader(mockVersionJSON())),
|
|
}, nil
|
|
case strings.Contains(r.URL.String(), DowngradeEnabledPath):
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(strings.NewReader(`false`)),
|
|
}, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
func mockVersionJSON() string {
|
|
v := version.Versions{Server: "3.7.0", Cluster: "3.7.0"}
|
|
version, _ := json.Marshal(v)
|
|
return string(version)
|
|
}
|
|
|
|
func mockMembersJSON(m []etcdserverpb.Member) string {
|
|
members, _ := json.Marshal(m)
|
|
return string(members)
|
|
}
|
|
|
|
func TestBootstrapBackend(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
prepareData func(config.ServerConfig) error
|
|
expectedConsistentIdx uint64
|
|
expectedError error
|
|
}{
|
|
{
|
|
name: "bootstrap backend success: no data files",
|
|
prepareData: nil,
|
|
expectedConsistentIdx: 0,
|
|
expectedError: nil,
|
|
},
|
|
{
|
|
name: "bootstrap backend success: have data files and snapshot db file",
|
|
prepareData: prepareData,
|
|
expectedConsistentIdx: 5,
|
|
expectedError: nil,
|
|
},
|
|
// TODO(ahrtr): add more test cases
|
|
// https://github.com/etcd-io/etcd/issues/13507
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dataDir, err := createDataDir(t)
|
|
require.NoErrorf(t, err, "Failed to create the data dir, unexpected error: %v", err)
|
|
|
|
cfg := config.ServerConfig{
|
|
Name: "demoNode",
|
|
DataDir: dataDir,
|
|
BackendFreelistType: bolt.FreelistArrayType,
|
|
Logger: zaptest.NewLogger(t),
|
|
}
|
|
|
|
if tt.prepareData != nil {
|
|
err = tt.prepareData(cfg)
|
|
require.NoErrorf(t, err, "failed to prepare data, unexpected error: %v", err)
|
|
}
|
|
|
|
haveWAL := wal.Exist(cfg.WALDir())
|
|
backend, err := bootstrapBackend(cfg, haveWAL)
|
|
defer t.Cleanup(func() {
|
|
backend.Close()
|
|
})
|
|
|
|
hasError := err != nil
|
|
expectedHasError := tt.expectedError != nil
|
|
if hasError != expectedHasError {
|
|
t.Errorf("expected error: %v got: %v", expectedHasError, err)
|
|
}
|
|
if hasError {
|
|
require.Containsf(t, err.Error(), tt.expectedError.Error(), "expected error to contain: %q, got: %q", tt.expectedError.Error(), err.Error())
|
|
}
|
|
|
|
if backend.ci.ConsistentIndex() != tt.expectedConsistentIdx {
|
|
t.Errorf("expected consistent index: %d, got: %d", tt.expectedConsistentIdx, backend.ci.ConsistentIndex())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func createDataDir(t *testing.T) (string, error) {
|
|
var err error
|
|
|
|
// create the temporary data dir
|
|
dataDir := t.TempDir()
|
|
|
|
// create ${dataDir}/member/snap
|
|
if err = os.MkdirAll(datadir.ToSnapDir(dataDir), 0o700); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// create ${dataDir}/member/wal
|
|
err = os.MkdirAll(datadir.ToWALDir(dataDir), 0o700)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return dataDir, nil
|
|
}
|
|
|
|
// prepare data for the test case
|
|
func prepareData(cfg config.ServerConfig) error {
|
|
var snapshotTerm, snapshotIndex uint64 = 2, 5
|
|
|
|
if err := createWALFileWithSnapshotRecord(cfg, snapshotTerm, snapshotIndex); err != nil {
|
|
return err
|
|
}
|
|
|
|
return createSnapshotAndBackendDB(cfg, snapshotTerm, snapshotIndex)
|
|
}
|
|
|
|
func createWALFileWithSnapshotRecord(cfg config.ServerConfig, snapshotTerm, snapshotIndex uint64) (err error) {
|
|
var w *wal.WAL
|
|
if w, err = wal.Create(cfg.Logger, cfg.WALDir(), []byte("somedata")); err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
err = w.Close()
|
|
}()
|
|
|
|
walSnap := walpb.Snapshot{
|
|
Index: snapshotIndex,
|
|
Term: snapshotTerm,
|
|
ConfState: &raftpb.ConfState{
|
|
Voters: []uint64{0x00ffca74},
|
|
AutoLeave: false,
|
|
},
|
|
}
|
|
|
|
if err = w.SaveSnapshot(walSnap); err != nil {
|
|
return err
|
|
}
|
|
|
|
return w.Save(raftpb.HardState{Term: snapshotTerm, Vote: 3, Commit: snapshotIndex}, nil)
|
|
}
|
|
|
|
func createSnapshotAndBackendDB(cfg config.ServerConfig, snapshotTerm, snapshotIndex uint64) error {
|
|
var err error
|
|
|
|
confState := raftpb.ConfState{
|
|
Voters: []uint64{1, 2, 3},
|
|
}
|
|
|
|
// create snapshot file
|
|
ss := snap.New(cfg.Logger, cfg.SnapDir())
|
|
if err = ss.SaveSnap(raftpb.Snapshot{
|
|
Data: []byte("{}"),
|
|
Metadata: raftpb.SnapshotMetadata{
|
|
ConfState: confState,
|
|
Index: snapshotIndex,
|
|
Term: snapshotTerm,
|
|
},
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
// create snapshot db file: "%016x.snap.db"
|
|
be := serverstorage.OpenBackend(cfg, nil)
|
|
schema.CreateMetaBucket(be.BatchTx())
|
|
schema.UnsafeUpdateConsistentIndex(be.BatchTx(), snapshotIndex, snapshotTerm)
|
|
schema.MustUnsafeSaveConfStateToBackend(cfg.Logger, be.BatchTx(), &confState)
|
|
if err = be.Close(); err != nil {
|
|
return err
|
|
}
|
|
sdb := filepath.Join(cfg.SnapDir(), fmt.Sprintf("%016x.snap.db", snapshotIndex))
|
|
if err = os.Rename(cfg.BackendPath(), sdb); err != nil {
|
|
return err
|
|
}
|
|
|
|
// create backend db file
|
|
be = serverstorage.OpenBackend(cfg, nil)
|
|
schema.CreateMetaBucket(be.BatchTx())
|
|
schema.UnsafeUpdateConsistentIndex(be.BatchTx(), 1, 1)
|
|
return be.Close()
|
|
}
|