mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
Merge pull request #15261 from ahrtr/memberlist_20230208
clientv3: support serializable `MemberList` operation
This commit is contained in:
commit
da4bf0f76f
@ -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`
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user