clientv3: support serializable MemberList operation

Signed-off-by: Benjamin Wang <wachao@vmware.com>
dependabot/go_modules/go.uber.org/atomic-1.10.0
Benjamin Wang 2023-02-08 14:30:42 +08:00
parent 0a8cdddd26
commit 1f0d361848
17 changed files with 85 additions and 28 deletions

View File

@ -32,6 +32,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.5.0...v3.6.0).
- [Always print the raft_term in decimal](https://github.com/etcd-io/etcd/pull/13711) when displaying member list in json.
- [Add one more field `storageVersion`](https://github.com/etcd-io/etcd/pull/13773) into the response of command `etcdctl endpoint status`.
- Add [`--max-txn-ops`](https://github.com/etcd-io/etcd/pull/14340) flag to make-mirror command.
- Add [`--consistency`](https://github.com/etcd-io/etcd/pull/15261) flag to member list command.
- Display [field `hash_revision`](https://github.com/etcd-io/etcd/pull/14812) for `etcdctl endpoint hash` command.
### etcdutl v3
@ -39,6 +40,10 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.5.0...v3.6.0).
- Add command to generate [shell completion](https://github.com/etcd-io/etcd/pull/13142).
- Add `migrate` command for downgrading/upgrading etcd data dir files.
### Package `clientv3`
- [Support serializable `MemberList` operation](https://github.com/etcd-io/etcd/pull/15261).
### Package `server`
- Package `mvcc` was moved to `storage/mvcc`

View File

@ -426,7 +426,7 @@ type mockCluster struct {
members []*etcdserverpb.Member
}
func (mc *mockCluster) MemberList(ctx context.Context) (*MemberListResponse, error) {
func (mc *mockCluster) MemberList(ctx context.Context, opts ...OpOption) (*MemberListResponse, error) {
return &MemberListResponse{Members: mc.members}, nil
}

View File

@ -34,7 +34,7 @@ type (
type Cluster interface {
// MemberList lists the current cluster membership.
MemberList(ctx context.Context) (*MemberListResponse, error)
MemberList(ctx context.Context, opts ...OpOption) (*MemberListResponse, error)
// MemberAdd adds a new member into the cluster.
MemberAdd(ctx context.Context, peerAddrs []string) (*MemberAddResponse, error)
@ -122,9 +122,9 @@ func (c *cluster) MemberUpdate(ctx context.Context, id uint64, peerAddrs []strin
return nil, toErr(ctx, err)
}
func (c *cluster) MemberList(ctx context.Context) (*MemberListResponse, error) {
// it is safe to retry on list.
resp, err := c.remote.MemberList(ctx, &pb.MemberListRequest{Linearizable: true}, c.callOpts...)
func (c *cluster) MemberList(ctx context.Context, opts ...OpOption) (*MemberListResponse, error) {
opt := OpGet("", opts...)
resp, err := c.remote.MemberList(ctx, &pb.MemberListRequest{Linearizable: !opt.serializable}, c.callOpts...)
if err == nil {
return (*MemberListResponse)(resp), nil
}

View File

@ -711,6 +711,9 @@ MEMBER LIST prints the member details for all members associated with an etcd cl
RPC: MemberList
#### Options
- consistency -- Linearizable(l) or Serializable(s)
#### Output
Prints a humanized table of the member IDs, statuses, names, peer addresses, and client addresses.

View File

@ -109,12 +109,8 @@ func getGetOp(args []string) (string, []clientv3.OpOption) {
}
var opts []clientv3.OpOption
switch getConsistency {
case "s":
if IsSerializable(getConsistency) {
opts = append(opts, clientv3.WithSerializable())
case "l":
default:
cobrautl.ExitWithError(cobrautl.ExitBadFeature, fmt.Errorf("unknown consistency flag %q", getConsistency))
}
key := args[0]

View File

@ -27,8 +27,9 @@ import (
)
var (
memberPeerURLs string
isLearner bool
memberPeerURLs string
isLearner bool
memberConsistency string
)
// NewMemberCommand returns the cobra command for "member".
@ -100,6 +101,8 @@ The items in the lists are ID, Status, Name, Peer Addrs, Client Addrs, Is Learne
Run: memberListCommandFunc,
}
cc.Flags().StringVar(&memberConsistency, "consistency", "l", "Linearizable(l) or Serializable(s)")
return cc
}
@ -226,8 +229,12 @@ func memberUpdateCommandFunc(cmd *cobra.Command, args []string) {
// memberListCommandFunc executes the "member list" command.
func memberListCommandFunc(cmd *cobra.Command, args []string) {
var opts []clientv3.OpOption
if IsSerializable(memberConsistency) {
opts = append(opts, clientv3.WithSerializable())
}
ctx, cancel := commandCtx(cmd)
resp, err := mustClientFromCmd(cmd).MemberList(ctx)
resp, err := mustClientFromCmd(cmd).MemberList(ctx, opts...)
cancel()
if err != nil {
cobrautl.ExitWithError(cobrautl.ExitError, err)

View File

@ -166,3 +166,14 @@ func defrag(c *clientv3.Client, ep string) {
}
fmt.Printf("Defragmented %q\n", ep)
}
func IsSerializable(option string) bool {
switch option {
case "s":
return true
case "l":
default:
cobrautl.ExitWithError(cobrautl.ExitBadFeature, fmt.Errorf("unknown consistency flag %q", getConsistency))
}
return false
}

View File

@ -40,7 +40,7 @@ func TestMemberList(t *testing.T) {
cc := testutils.MustClient(clus.Client())
testutils.ExecuteUntil(ctx, t, func() {
resp, err := cc.MemberList(ctx)
resp, err := cc.MemberList(ctx, false)
if err != nil {
t.Fatalf("could not get member list, err: %s", err)
}
@ -237,7 +237,7 @@ func TestMemberRemove(t *testing.T) {
// Otherwise, return a member that client has not connected to.
// It ensures that `MemberRemove` function does not return an "etcdserver: server stopped" error.
func memberToRemove(ctx context.Context, t *testing.T, client intf.Client, clusterSize int) (memberId uint64, clusterId uint64) {
listResp, err := client.MemberList(ctx)
listResp, err := client.MemberList(ctx, false)
if err != nil {
t.Fatal(err)
}

View File

@ -124,7 +124,7 @@ func TestPeriodicCheckDetectsCorruption(t *testing.T) {
assert.NoError(t, err, "error on put")
}
members, err := cc.MemberList(ctx)
members, err := cc.MemberList(ctx, false)
assert.NoError(t, err, "error on member list")
var memberID uint64
for _, m := range members.Members {
@ -171,7 +171,7 @@ func TestCompactHashCheckDetectCorruption(t *testing.T) {
err := cc.Put(ctx, testutil.PickKey(int64(i)), fmt.Sprint(i), config.PutOptions{})
assert.NoError(t, err, "error on put")
}
members, err := cc.MemberList(ctx)
members, err := cc.MemberList(ctx, false)
assert.NoError(t, err, "error on member list")
var memberID uint64
for _, m := range members.Members {

View File

@ -202,7 +202,7 @@ func authTestMemberUpdate(cx ctlCtx) {
cx.user, cx.pass = "root", "root"
authSetupTestUser(cx)
mr, err := getMemberList(cx)
mr, err := getMemberList(cx, false)
if err != nil {
cx.t.Fatal(err)
}

View File

@ -22,12 +22,20 @@ import (
"strings"
"testing"
"github.com/stretchr/testify/require"
"go.etcd.io/etcd/api/v3/etcdserverpb"
"go.etcd.io/etcd/tests/v3/framework/e2e"
)
func TestCtlV3MemberList(t *testing.T) { testCtl(t, memberListTest) }
func TestCtlV3MemberListWithHex(t *testing.T) { testCtl(t, memberListWithHexTest) }
func TestCtlV3MemberListSerializable(t *testing.T) {
cfg := e2e.NewConfig(
e2e.WithClusterSize(1),
)
testCtl(t, memberListSerializableTest, withCfg(*cfg))
}
func TestCtlV3MemberAdd(t *testing.T) { testCtl(t, memberAddTest) }
func TestCtlV3MemberAddAsLearner(t *testing.T) { testCtl(t, memberAddAsLearnerTest) }
@ -52,6 +60,19 @@ func memberListTest(cx ctlCtx) {
}
}
func memberListSerializableTest(cx ctlCtx) {
resp, err := getMemberList(cx, false)
require.NoError(cx.t, err)
require.Equal(cx.t, 1, len(resp.Members))
peerURL := fmt.Sprintf("http://localhost:%d", e2e.EtcdProcessBasePort+11)
err = ctlV3MemberAdd(cx, peerURL, false)
require.NoError(cx.t, err)
resp, err = getMemberList(cx, true)
require.Equal(cx.t, 2, len(resp.Members))
}
func ctlV3MemberList(cx ctlCtx) error {
cmdArgs := append(cx.PrefixArgs(), "member", "list")
lines := make([]string, cx.cfg.ClusterSize)
@ -61,8 +82,11 @@ func ctlV3MemberList(cx ctlCtx) error {
return e2e.SpawnWithExpects(cmdArgs, cx.envMap, lines...)
}
func getMemberList(cx ctlCtx) (etcdserverpb.MemberListResponse, error) {
func getMemberList(cx ctlCtx, serializable bool) (etcdserverpb.MemberListResponse, error) {
cmdArgs := append(cx.PrefixArgs(), "--write-out", "json", "member", "list")
if serializable {
cmdArgs = append(cmdArgs, "--consistency", "s")
}
proc, err := e2e.SpawnCmd(cmdArgs, cx.envMap)
if err != nil {
@ -86,7 +110,7 @@ func getMemberList(cx ctlCtx) (etcdserverpb.MemberListResponse, error) {
}
func memberListWithHexTest(cx ctlCtx) {
resp, err := getMemberList(cx)
resp, err := getMemberList(cx, false)
if err != nil {
cx.t.Fatalf("getMemberList error (%v)", err)
}
@ -166,7 +190,7 @@ func ctlV3MemberAdd(cx ctlCtx, peerURL string, isLearner bool) error {
}
func memberUpdateTest(cx ctlCtx) {
mr, err := getMemberList(cx)
mr, err := getMemberList(cx, false)
if err != nil {
cx.t.Fatal(err)
}

View File

@ -350,7 +350,7 @@ func (cx *ctlCtx) memberToRemove() (ep string, memberID string, clusterID string
cx.t.Fatalf("%d-node is too small to test 'member remove'", n1)
}
resp, err := getMemberList(*cx)
resp, err := getMemberList(*cx, false)
if err != nil {
cx.t.Fatal(err)
}

View File

@ -158,7 +158,7 @@ func TestV2DeprecationSnapshotRecover(t *testing.T) {
lastReleaseGetResponse, err := cc.Get(ctx, "", config.GetOptions{Prefix: true})
assert.NoError(t, err)
lastReleaseMemberListResponse, err := cc.MemberList(ctx)
lastReleaseMemberListResponse, err := cc.MemberList(ctx, false)
assert.NoError(t, err)
assert.NoError(t, epc.Close())
@ -174,7 +174,7 @@ func TestV2DeprecationSnapshotRecover(t *testing.T) {
currentReleaseGetResponse, err := cc.Get(ctx, "", config.GetOptions{Prefix: true})
assert.NoError(t, err)
currentReleaseMemberListResponse, err := cc.MemberList(ctx)
currentReleaseMemberListResponse, err := cc.MemberList(ctx, false)
assert.NoError(t, err)
assert.Equal(t, lastReleaseGetResponse.Kvs, currentReleaseGetResponse.Kvs)

View File

@ -721,7 +721,7 @@ func (epc *EtcdProcessCluster) CloseProc(ctx context.Context, finder func(EtcdPr
// First remove member from the cluster
memberCtl := epc.Client(opts...)
memberList, err := memberCtl.MemberList(ctx)
memberList, err := memberCtl.MemberList(ctx, false)
if err != nil {
return fmt.Errorf("failed to get member list: %w", err)
}

View File

@ -274,9 +274,13 @@ func AddTxnResponse(resp *clientv3.TxnResponse, jsonData string) {
}
}
func (ctl *EtcdctlV3) MemberList(ctx context.Context) (*clientv3.MemberListResponse, error) {
func (ctl *EtcdctlV3) MemberList(ctx context.Context, serializable bool) (*clientv3.MemberListResponse, error) {
var resp clientv3.MemberListResponse
err := ctl.spawnJsonCmd(ctx, &resp, "member", "list")
args := []string{"member", "list"}
if serializable {
args = append(args, "--consistency", "s")
}
err := ctl.spawnJsonCmd(ctx, &resp, args...)
return &resp, err
}

View File

@ -418,3 +418,10 @@ func (c integrationClient) MemberAddAsLearner(ctx context.Context, _ string, pee
func (c integrationClient) MemberRemove(ctx context.Context, id uint64) (*clientv3.MemberRemoveResponse, error) {
return c.Client.MemberRemove(ctx, id)
}
func (c integrationClient) MemberList(ctx context.Context, serializable bool) (*clientv3.MemberListResponse, error) {
if serializable {
return c.Client.MemberList(ctx, clientv3.WithSerializable())
}
return c.Client.MemberList(ctx)
}

View File

@ -78,7 +78,7 @@ type Client interface {
Txn(context context.Context, compares, ifSucess, ifFail []string, o config.TxnOptions) (*clientv3.TxnResponse, error)
MemberList(context context.Context) (*clientv3.MemberListResponse, error)
MemberList(context context.Context, serializable bool) (*clientv3.MemberListResponse, error)
MemberAdd(context context.Context, name string, peerAddrs []string) (*clientv3.MemberAddResponse, error)
MemberAddAsLearner(context context.Context, name string, peerAddrs []string) (*clientv3.MemberAddResponse, error)
MemberRemove(ctx context.Context, id uint64) (*clientv3.MemberRemoveResponse, error)