Files
tidb/util/etcd/etcd_test.go

425 lines
11 KiB
Go

// Copyright 2018 PingCAP, Inc.
//
// 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 etcd
import (
"context"
"testing"
"time"
"github.com/pingcap/errors"
"github.com/stretchr/testify/require"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/tests/v3/integration"
)
var (
etcdMockCluster *integration.ClusterV3
etcdCli *Client
ctx context.Context
)
func TestCreate(t *testing.T) {
integration.BeforeTestExternal(t)
ctx, etcdCli, etcdMockCluster = testSetup(t)
defer etcdMockCluster.Terminate(t)
etcdClient := etcdMockCluster.RandClient()
key := "binlogcreate/testkey"
obj := "test"
// verify that kv pair is empty before set
getResp, err := etcdClient.KV.Get(ctx, key)
require.NoError(t, err)
require.Len(t, getResp.Kvs, 0)
_, err = etcdCli.Create(ctx, key, obj, nil)
require.NoError(t, err)
getResp, err = etcdClient.KV.Get(ctx, key)
require.NoError(t, err)
require.Len(t, getResp.Kvs, 1)
}
func TestCreateWithTTL(t *testing.T) {
integration.BeforeTestExternal(t)
ctx, etcdCli, etcdMockCluster = testSetup(t)
defer etcdMockCluster.Terminate(t)
key := "binlogttl/ttlkey"
obj := "ttltest"
lcr, err := etcdCli.client.Lease.Grant(ctx, 1)
require.NoError(t, err)
opts := []clientv3.OpOption{clientv3.WithLease(lcr.ID)}
_, err = etcdCli.Create(ctx, key, obj, opts)
require.NoError(t, err)
time.Sleep(2 * time.Second)
_, _, err = etcdCli.Get(ctx, key)
require.True(t, errors.IsNotFound(err))
}
func TestCreateWithKeyExist(t *testing.T) {
integration.BeforeTestExternal(t)
ctx, etcdCli, etcdMockCluster = testSetup(t)
defer etcdMockCluster.Terminate(t)
obj := "existtest"
key := "binlogexist/exist"
etcdClient := etcdMockCluster.RandClient()
_, err := etcdClient.KV.Put(ctx, key, obj, nil...)
require.NoError(t, err)
_, err = etcdCli.Create(ctx, key, obj, nil)
require.True(t, errors.IsAlreadyExists(err))
}
func TestUpdate(t *testing.T) {
integration.BeforeTestExternal(t)
ctx, etcdCli, etcdMockCluster = testSetup(t)
defer etcdMockCluster.Terminate(t)
obj1 := "updatetest"
obj2 := "updatetest2"
key := "binlogupdate/updatekey"
lcr, err := etcdCli.client.Lease.Grant(ctx, 2)
require.NoError(t, err)
opts := []clientv3.OpOption{clientv3.WithLease(lcr.ID)}
revision0, err := etcdCli.Create(ctx, key, obj1, opts)
require.NoError(t, err)
res, revision1, err := etcdCli.Get(ctx, key)
require.NoError(t, err)
require.Equal(t, obj1, string(res))
require.Equal(t, revision1, revision0)
time.Sleep(time.Second)
err = etcdCli.Update(ctx, key, obj2, 3)
require.NoError(t, err)
time.Sleep(2 * time.Second)
// the new revision should greater than the old
res, revision2, err := etcdCli.Get(ctx, key)
require.NoError(t, err)
require.Equal(t, obj2, string(res))
require.Greater(t, revision2, revision1)
time.Sleep(2 * time.Second)
_, _, err = etcdCli.Get(ctx, key)
require.True(t, errors.IsNotFound(err))
}
func TestUpdateOrCreate(t *testing.T) {
integration.BeforeTestExternal(t)
ctx, etcdCli, etcdMockCluster = testSetup(t)
defer etcdMockCluster.Terminate(t)
obj := "updatetest"
key := "binlogupdatecreate/updatekey"
err := etcdCli.UpdateOrCreate(ctx, key, obj, 3)
require.NoError(t, err)
}
func TestList(t *testing.T) {
integration.BeforeTestExternal(t)
ctx, etcdCli, etcdMockCluster = testSetup(t)
defer etcdMockCluster.Terminate(t)
key := "binloglist/testkey"
k1 := key + "/level1"
k2 := key + "/level2"
k3 := key + "/level3"
k11 := key + "/level1/level1"
revision1, err := etcdCli.Create(ctx, k1, k1, nil)
require.NoError(t, err)
revision2, err := etcdCli.Create(ctx, k2, k2, nil)
require.NoError(t, err)
require.True(t, revision2 > revision1)
revision3, err := etcdCli.Create(ctx, k3, k3, nil)
require.NoError(t, err)
require.True(t, revision3 > revision2)
revision4, err := etcdCli.Create(ctx, k11, k11, nil)
require.NoError(t, err)
require.True(t, revision4 > revision3)
root, revision5, err := etcdCli.List(ctx, key)
require.NoError(t, err)
require.Equal(t, k1, string(root.Childs["level1"].Value))
require.Equal(t, k11, string(root.Childs["level1"].Childs["level1"].Value))
require.Equal(t, k2, string(root.Childs["level2"].Value))
require.Equal(t, k3, string(root.Childs["level3"].Value))
// the revision of list should equal to the latest update's revision
_, revision6, err := etcdCli.Get(ctx, k11)
require.NoError(t, err)
require.Equal(t, revision6, revision5)
}
func TestDelete(t *testing.T) {
integration.BeforeTestExternal(t)
ctx, etcdCli, etcdMockCluster = testSetup(t)
defer etcdMockCluster.Terminate(t)
key := "binlogdelete/testkey"
keys := []string{key + "/level1", key + "/level2", key + "/level1" + "/level1"}
for _, k := range keys {
_, err := etcdCli.Create(ctx, k, k, nil)
require.NoError(t, err)
}
root, _, err := etcdCli.List(ctx, key)
require.NoError(t, err)
require.Len(t, root.Childs, 2)
err = etcdCli.Delete(ctx, keys[1], false)
require.NoError(t, err)
root, _, err = etcdCli.List(ctx, key)
require.NoError(t, err)
require.Len(t, root.Childs, 1)
err = etcdCli.Delete(ctx, key, true)
require.NoError(t, err)
root, _, err = etcdCli.List(ctx, key)
require.NoError(t, err)
require.Len(t, root.Childs, 0)
}
func TestDoTxn(t *testing.T) {
integration.BeforeTestExternal(t)
ctx, etcdCli, etcdMockCluster = testSetup(t)
defer etcdMockCluster.Terminate(t)
// case1: create two keys in one transaction
ops := []*Operation{
{
Tp: CreateOp,
Key: "test1",
Value: "1",
}, {
Tp: CreateOp,
Key: "test2",
Value: "2",
},
}
revision, err := etcdCli.DoTxn(context.Background(), ops)
require.NoError(t, err)
value1, revision1, err := etcdCli.Get(context.Background(), "test1")
require.NoError(t, err)
require.Equal(t, "1", string(value1))
require.Equal(t, revision, revision1)
value2, revision2, err := etcdCli.Get(context.Background(), "test2")
require.NoError(t, err)
require.Equal(t, "2", string(value2))
require.Equal(t, revision, revision2)
// case2: delete, update and create in one transaction
ops = []*Operation{
{
Tp: DeleteOp,
Key: "test1",
}, {
Tp: UpdateOp,
Key: "test2",
Value: "22",
}, {
Tp: CreateOp,
Key: "test3",
Value: "3",
},
}
revision, err = etcdCli.DoTxn(context.Background(), ops)
require.NoError(t, err)
_, _, err = etcdCli.Get(context.Background(), "test1")
require.Regexp(t, ".* not found", err)
value2, revision2, err = etcdCli.Get(context.Background(), "test2")
require.NoError(t, err)
require.Equal(t, "22", string(value2))
require.Equal(t, revision, revision2)
value3, revision3, err := etcdCli.Get(context.Background(), "test3")
require.NoError(t, err)
require.Equal(t, "3", string(value3))
require.Equal(t, revision, revision3)
// case3: create keys with TTL
ops = []*Operation{
{
Tp: CreateOp,
Key: "test4",
Value: "4",
TTL: 1,
}, {
Tp: CreateOp,
Key: "test5",
Value: "5",
},
}
revision, err = etcdCli.DoTxn(context.Background(), ops)
require.NoError(t, err)
value4, revision4, err := etcdCli.Get(context.Background(), "test4")
require.NoError(t, err)
require.Equal(t, "4", string(value4))
require.Equal(t, revision, revision4)
value5, revision5, err := etcdCli.Get(context.Background(), "test5")
require.NoError(t, err)
require.Equal(t, "5", string(value5))
require.Equal(t, revision, revision5)
// sleep 2 seconds and this key will be deleted
time.Sleep(2 * time.Second)
_, _, err = etcdCli.Get(context.Background(), "test4")
require.Regexp(t, ".* not found", err)
// case4: do transaction failed because key is deleted, so can't update
ops = []*Operation{
{
Tp: CreateOp,
Key: "test4",
Value: "4",
}, {
Tp: UpdateOp, // key test1 is deleted, so will update failed
Key: "test1",
Value: "11",
},
}
_, err = etcdCli.DoTxn(context.Background(), ops)
require.Regexp(t, "do transaction failed.*", err)
_, _, err = etcdCli.Get(context.Background(), "test4")
require.Regexp(t, ".* not found", err)
// case5: do transaction failed because can't operate one key in one transaction
ops = []*Operation{
{
Tp: CreateOp,
Key: "test6",
Value: "6",
}, {
Tp: UpdateOp,
Key: "test6",
Value: "66",
},
}
_, err = etcdCli.DoTxn(context.Background(), ops)
require.Regexp(t, "etcdserver: duplicate key given in txn request", err)
_, _, err = etcdCli.Get(context.Background(), "test6")
require.Regexp(t, ".* not found", err)
// case6: do transaction failed because can't create an existing key
ops = []*Operation{
{
Tp: CreateOp,
Key: "test2", // already exist
Value: "222",
}, {
Tp: UpdateOp,
Key: "test5",
Value: "555",
},
}
_, err = etcdCli.DoTxn(context.Background(), ops)
require.Regexp(t, "do transaction failed.*", err)
value2, _, err = etcdCli.Get(context.Background(), "test2")
require.NoError(t, err)
require.Equal(t, "22", string(value2))
value5, _, err = etcdCli.Get(context.Background(), "test5")
require.NoError(t, err)
require.Equal(t, "5", string(value5))
// case7: delete not exist key but will do transaction success
ops = []*Operation{
{
Tp: DeleteOp,
Key: "test7", // not exist
}, {
Tp: CreateOp,
Key: "test8",
Value: "8",
},
}
_, err = etcdCli.DoTxn(context.Background(), ops)
require.NoError(t, err)
value8, _, err := etcdCli.Get(context.Background(), "test8")
require.NoError(t, err)
require.Equal(t, "8", string(value8))
// case8: do transaction failed because can't set TTL for delete operation
ops = []*Operation{
{
Tp: DeleteOp,
Key: "test8",
TTL: 1,
},
}
_, err = etcdCli.DoTxn(context.Background(), ops)
require.Regexp(t, "unexpected TTL in delete operation", err)
}
func testSetup(t *testing.T) (context.Context, *Client, *integration.ClusterV3) {
cluster := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
etcd := NewClient(cluster.RandClient(), "binlog")
return context.Background(), etcd, cluster
}
func testSetupOriginal(t *testing.T) (context.Context, *clientv3.Client, *integration.ClusterV3) {
cluster := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
return context.Background(), cluster.RandClient(), cluster
}
func TestSetEtcdCliByNamespace(t *testing.T) {
integration.BeforeTest(t)
ctx, origEtcdCli, etcdMockCluster := testSetupOriginal(t)
defer etcdMockCluster.Terminate(t)
namespacePrefix := "testNamespace/"
key := "testkey"
obj := "test"
unprefixedKV := origEtcdCli.KV
cliNamespace := origEtcdCli
SetEtcdCliByNamespace(cliNamespace, namespacePrefix)
_, err := cliNamespace.Put(ctx, key, obj)
require.NoError(t, err)
// verify that kv pair is empty before set
getResp, err := unprefixedKV.Get(ctx, namespacePrefix+key)
require.NoError(t, err)
require.Len(t, getResp.Kvs, 1)
}