Merge pull request #15261 from ahrtr/memberlist_20230208

clientv3: support serializable `MemberList` operation
This commit is contained in:
Benjamin Wang 2023-02-28 06:26:38 +08:00 committed by GitHub
commit da4bf0f76f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 109 additions and 33 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

@ -418,9 +418,15 @@ func WithFromKey() OpOption {
}
}
// WithSerializable makes 'Get' request serializable. By default,
// it's linearizable. Serializable requests are better for lower latency
// requirement.
// WithSerializable makes `Get` and `MemberList` requests serializable.
// By default, they are linearizable. Serializable requests are better
// for lower latency requirement, but users should be aware that they
// could get stale data with serializable requests.
//
// In some situations users may want to use serializable requests. For
// example, when adding a new member to a one-node cluster, it's reasonable
// and safe to use serializable request before the new added member gets
// started.
func WithSerializable() OpOption {
return func(op *Op) { op.serializable = true }
}

View File

@ -119,15 +119,22 @@ RPC: Range
- print-value-only -- print only value when used with write-out=simple
- consistency -- Linearizable(l) or Serializable(s)
- consistency -- Linearizable(l) or Serializable(s), defaults to Linearizable(l).
- from-key -- Get keys that are greater than or equal to the given key using byte compare
- keys-only -- Get only the keys
#### Output
Prints the data in format below,
```
\<key\>\n\<value\>\n\<next_key\>\n\<next_value\>...
```
Note serializable requests are better for lower latency requirement, but
stale data might be returned if serializable option (`--consistency=s`)
is specified.
#### Examples
@ -711,10 +718,19 @@ MEMBER LIST prints the member details for all members associated with an etcd cl
RPC: MemberList
#### Options
- consistency -- Linearizable(l) or Serializable(s), defaults to Linearizable(l).
#### Output
Prints a humanized table of the member IDs, statuses, names, peer addresses, and client addresses.
Note serializable requests are better for lower latency requirement, but
stale member list might be returned if serializable option (`--consistency=s`)
is specified. In some situations users may want to use serializable requests.
For example, when adding a new member to a one-node cluster, it's reasonable
and safe to use serializable request before the new added member gets started.
#### Examples
```bash

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

@ -157,7 +157,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)