mirror of https://github.com/etcd-io/dbtester.git
vendor: new gRPC
Signed-off-by: Gyuho Lee <gyuhox@gmail.com>
This commit is contained in:
parent
82d8634536
commit
b7411b790b
|
|
@ -394,9 +394,14 @@
|
|||
packages = [
|
||||
".",
|
||||
"balancer",
|
||||
"balancer/base",
|
||||
"balancer/roundrobin",
|
||||
"channelz",
|
||||
"codes",
|
||||
"connectivity",
|
||||
"credentials",
|
||||
"encoding",
|
||||
"encoding/proto",
|
||||
"grpclb/grpc_lb_v1/messages",
|
||||
"grpclog",
|
||||
"health/grpc_health_v1",
|
||||
|
|
@ -406,12 +411,14 @@
|
|||
"naming",
|
||||
"peer",
|
||||
"resolver",
|
||||
"resolver/dns",
|
||||
"resolver/passthrough",
|
||||
"stats",
|
||||
"status",
|
||||
"tap",
|
||||
"transport"
|
||||
]
|
||||
revision = "5b3c4e850e90a4cf6a20ebd46c8b32a0a3afcb9e"
|
||||
revision = "7a6a684ca69eb4cae85ad0a484f2e531598c047b"
|
||||
source = "https://github.com/grpc/grpc-go"
|
||||
|
||||
[[projects]]
|
||||
|
|
@ -423,6 +430,6 @@
|
|||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "359a18695d247c68e0bfc25e394013eb702e10cbfb10caf5a616f23c8298fe64"
|
||||
inputs-digest = "f37f5d03bc73604d19ad1c629ca401a4f3abe7003f4577e5794319c36c2c881a"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
[[constraint]]
|
||||
name = "google.golang.org/grpc"
|
||||
source = "https://github.com/grpc/grpc-go"
|
||||
revision = "5b3c4e850e90a4cf6a20ebd46c8b32a0a3afcb9e"
|
||||
revision = "7a6a684ca69eb4cae85ad0a484f2e531598c047b"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2016 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 integration implements tests built upon embedded etcd, focusing on
|
||||
// the correctness of the etcd v2 client.
|
||||
package integration
|
||||
|
|
@ -21,7 +21,6 @@ import (
|
|||
|
||||
"github.com/coreos/etcd/auth/authpb"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
|
|
@ -216,8 +215,8 @@ func (auth *authenticator) close() {
|
|||
auth.conn.Close()
|
||||
}
|
||||
|
||||
func newAuthenticator(endpoint string, opts []grpc.DialOption, c *Client) (*authenticator, error) {
|
||||
conn, err := grpc.Dial(endpoint, opts...)
|
||||
func newAuthenticator(ctx context.Context, target string, opts []grpc.DialOption, c *Client) (*authenticator, error) {
|
||||
conn, err := grpc.DialContext(ctx, target, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,275 @@
|
|||
// Copyright 2018 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 balancer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/clientv3/balancer/picker"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/resolver"
|
||||
_ "google.golang.org/grpc/resolver/dns" // register DNS resolver
|
||||
_ "google.golang.org/grpc/resolver/passthrough" // register passthrough resolver
|
||||
)
|
||||
|
||||
// RegisterBuilder creates and registers a builder. Since this function calls balancer.Register, it
|
||||
// must be invoked at initialization time.
|
||||
func RegisterBuilder(cfg Config) {
|
||||
bb := &builder{cfg}
|
||||
balancer.Register(bb)
|
||||
|
||||
bb.cfg.Logger.Info(
|
||||
"registered balancer",
|
||||
zap.String("policy", bb.cfg.Policy.String()),
|
||||
zap.String("name", bb.cfg.Name),
|
||||
)
|
||||
}
|
||||
|
||||
type builder struct {
|
||||
cfg Config
|
||||
}
|
||||
|
||||
// Build is called initially when creating "ccBalancerWrapper".
|
||||
// "grpc.Dial" is called to this client connection.
|
||||
// Then, resolved addresses will be handled via "HandleResolvedAddrs".
|
||||
func (b *builder) Build(cc balancer.ClientConn, opt balancer.BuildOptions) balancer.Balancer {
|
||||
bb := &baseBalancer{
|
||||
id: strconv.FormatInt(time.Now().UnixNano(), 36),
|
||||
policy: b.cfg.Policy,
|
||||
name: b.cfg.Policy.String(),
|
||||
lg: b.cfg.Logger,
|
||||
|
||||
addrToSc: make(map[resolver.Address]balancer.SubConn),
|
||||
scToAddr: make(map[balancer.SubConn]resolver.Address),
|
||||
scToSt: make(map[balancer.SubConn]connectivity.State),
|
||||
|
||||
currentConn: nil,
|
||||
csEvltr: &connectivityStateEvaluator{},
|
||||
|
||||
// initialize picker always returns "ErrNoSubConnAvailable"
|
||||
Picker: picker.NewErr(balancer.ErrNoSubConnAvailable),
|
||||
}
|
||||
if b.cfg.Name != "" {
|
||||
bb.name = b.cfg.Name
|
||||
}
|
||||
if bb.lg == nil {
|
||||
bb.lg = zap.NewNop()
|
||||
}
|
||||
|
||||
// TODO: support multiple connections
|
||||
bb.mu.Lock()
|
||||
bb.currentConn = cc
|
||||
bb.mu.Unlock()
|
||||
|
||||
bb.lg.Info(
|
||||
"built balancer",
|
||||
zap.String("balancer-id", bb.id),
|
||||
zap.String("policy", bb.policy.String()),
|
||||
zap.String("resolver-target", cc.Target()),
|
||||
)
|
||||
return bb
|
||||
}
|
||||
|
||||
// Name implements "grpc/balancer.Builder" interface.
|
||||
func (b *builder) Name() string { return b.cfg.Name }
|
||||
|
||||
// Balancer defines client balancer interface.
|
||||
type Balancer interface {
|
||||
// Balancer is called on specified client connection. Client initiates gRPC
|
||||
// connection with "grpc.Dial(addr, grpc.WithBalancerName)", and then those resolved
|
||||
// addresses are passed to "grpc/balancer.Balancer.HandleResolvedAddrs".
|
||||
// For each resolved address, balancer calls "balancer.ClientConn.NewSubConn".
|
||||
// "grpc/balancer.Balancer.HandleSubConnStateChange" is called when connectivity state
|
||||
// changes, thus requires failover logic in this method.
|
||||
balancer.Balancer
|
||||
|
||||
// Picker calls "Pick" for every client request.
|
||||
picker.Picker
|
||||
}
|
||||
|
||||
type baseBalancer struct {
|
||||
id string
|
||||
policy picker.Policy
|
||||
name string
|
||||
lg *zap.Logger
|
||||
|
||||
mu sync.RWMutex
|
||||
|
||||
addrToSc map[resolver.Address]balancer.SubConn
|
||||
scToAddr map[balancer.SubConn]resolver.Address
|
||||
scToSt map[balancer.SubConn]connectivity.State
|
||||
|
||||
currentConn balancer.ClientConn
|
||||
currentState connectivity.State
|
||||
csEvltr *connectivityStateEvaluator
|
||||
|
||||
picker.Picker
|
||||
}
|
||||
|
||||
// HandleResolvedAddrs implements "grpc/balancer.Balancer" interface.
|
||||
// gRPC sends initial or updated resolved addresses from "Build".
|
||||
func (bb *baseBalancer) HandleResolvedAddrs(addrs []resolver.Address, err error) {
|
||||
if err != nil {
|
||||
bb.lg.Warn("HandleResolvedAddrs called with error", zap.String("balancer-id", bb.id), zap.Error(err))
|
||||
return
|
||||
}
|
||||
bb.lg.Info("resolved", zap.String("balancer-id", bb.id), zap.Strings("addresses", addrsToStrings(addrs)))
|
||||
|
||||
bb.mu.Lock()
|
||||
defer bb.mu.Unlock()
|
||||
|
||||
resolved := make(map[resolver.Address]struct{})
|
||||
for _, addr := range addrs {
|
||||
resolved[addr] = struct{}{}
|
||||
if _, ok := bb.addrToSc[addr]; !ok {
|
||||
sc, err := bb.currentConn.NewSubConn([]resolver.Address{addr}, balancer.NewSubConnOptions{})
|
||||
if err != nil {
|
||||
bb.lg.Warn("NewSubConn failed", zap.String("balancer-id", bb.id), zap.Error(err), zap.String("address", addr.Addr))
|
||||
continue
|
||||
}
|
||||
bb.addrToSc[addr] = sc
|
||||
bb.scToAddr[sc] = addr
|
||||
bb.scToSt[sc] = connectivity.Idle
|
||||
sc.Connect()
|
||||
}
|
||||
}
|
||||
|
||||
for addr, sc := range bb.addrToSc {
|
||||
if _, ok := resolved[addr]; !ok {
|
||||
// was removed by resolver or failed to create subconn
|
||||
bb.currentConn.RemoveSubConn(sc)
|
||||
delete(bb.addrToSc, addr)
|
||||
|
||||
bb.lg.Info(
|
||||
"removed subconn",
|
||||
zap.String("balancer-id", bb.id),
|
||||
zap.String("address", addr.Addr),
|
||||
zap.String("subconn", scToString(sc)),
|
||||
)
|
||||
|
||||
// Keep the state of this sc in bb.scToSt until sc's state becomes Shutdown.
|
||||
// The entry will be deleted in HandleSubConnStateChange.
|
||||
// (DO NOT) delete(bb.scToAddr, sc)
|
||||
// (DO NOT) delete(bb.scToSt, sc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleSubConnStateChange implements "grpc/balancer.Balancer" interface.
|
||||
func (bb *baseBalancer) HandleSubConnStateChange(sc balancer.SubConn, s connectivity.State) {
|
||||
bb.mu.Lock()
|
||||
defer bb.mu.Unlock()
|
||||
|
||||
old, ok := bb.scToSt[sc]
|
||||
if !ok {
|
||||
bb.lg.Warn(
|
||||
"state change for an unknown subconn",
|
||||
zap.String("balancer-id", bb.id),
|
||||
zap.String("subconn", scToString(sc)),
|
||||
zap.String("state", s.String()),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
bb.lg.Info(
|
||||
"state changed",
|
||||
zap.String("balancer-id", bb.id),
|
||||
zap.Bool("connected", s == connectivity.Ready),
|
||||
zap.String("subconn", scToString(sc)),
|
||||
zap.String("address", bb.scToAddr[sc].Addr),
|
||||
zap.String("old-state", old.String()),
|
||||
zap.String("new-state", s.String()),
|
||||
)
|
||||
|
||||
bb.scToSt[sc] = s
|
||||
switch s {
|
||||
case connectivity.Idle:
|
||||
sc.Connect()
|
||||
case connectivity.Shutdown:
|
||||
// When an address was removed by resolver, b called RemoveSubConn but
|
||||
// kept the sc's state in scToSt. Remove state for this sc here.
|
||||
delete(bb.scToAddr, sc)
|
||||
delete(bb.scToSt, sc)
|
||||
}
|
||||
|
||||
oldAggrState := bb.currentState
|
||||
bb.currentState = bb.csEvltr.recordTransition(old, s)
|
||||
|
||||
// Regenerate picker when one of the following happens:
|
||||
// - this sc became ready from not-ready
|
||||
// - this sc became not-ready from ready
|
||||
// - the aggregated state of balancer became TransientFailure from non-TransientFailure
|
||||
// - the aggregated state of balancer became non-TransientFailure from TransientFailure
|
||||
if (s == connectivity.Ready) != (old == connectivity.Ready) ||
|
||||
(bb.currentState == connectivity.TransientFailure) != (oldAggrState == connectivity.TransientFailure) {
|
||||
bb.regeneratePicker()
|
||||
}
|
||||
|
||||
bb.currentConn.UpdateBalancerState(bb.currentState, bb.Picker)
|
||||
return
|
||||
}
|
||||
|
||||
func (bb *baseBalancer) regeneratePicker() {
|
||||
if bb.currentState == connectivity.TransientFailure {
|
||||
bb.lg.Info(
|
||||
"generated transient error picker",
|
||||
zap.String("balancer-id", bb.id),
|
||||
zap.String("policy", bb.policy.String()),
|
||||
)
|
||||
bb.Picker = picker.NewErr(balancer.ErrTransientFailure)
|
||||
return
|
||||
}
|
||||
|
||||
// only pass ready subconns to picker
|
||||
scs := make([]balancer.SubConn, 0)
|
||||
addrToSc := make(map[resolver.Address]balancer.SubConn)
|
||||
scToAddr := make(map[balancer.SubConn]resolver.Address)
|
||||
for addr, sc := range bb.addrToSc {
|
||||
if st, ok := bb.scToSt[sc]; ok && st == connectivity.Ready {
|
||||
scs = append(scs, sc)
|
||||
addrToSc[addr] = sc
|
||||
scToAddr[sc] = addr
|
||||
}
|
||||
}
|
||||
|
||||
switch bb.policy {
|
||||
case picker.RoundrobinBalanced:
|
||||
bb.Picker = picker.NewRoundrobinBalanced(bb.lg, scs, addrToSc, scToAddr)
|
||||
|
||||
default:
|
||||
panic(fmt.Errorf("invalid balancer picker policy (%d)", bb.policy))
|
||||
}
|
||||
|
||||
bb.lg.Info(
|
||||
"generated picker",
|
||||
zap.String("balancer-id", bb.id),
|
||||
zap.String("policy", bb.policy.String()),
|
||||
zap.Strings("subconn-ready", scsToStrings(addrToSc)),
|
||||
zap.Int("subconn-size", len(addrToSc)),
|
||||
)
|
||||
}
|
||||
|
||||
// Close implements "grpc/balancer.Balancer" interface.
|
||||
// Close is a nop because base balancer doesn't have internal state to clean up,
|
||||
// and it doesn't need to call RemoveSubConn for the SubConns.
|
||||
func (bb *baseBalancer) Close() {
|
||||
// TODO
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2018 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 balancer
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/clientv3/balancer/picker"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Config defines balancer configurations.
|
||||
type Config struct {
|
||||
// Policy configures balancer policy.
|
||||
Policy picker.Policy
|
||||
|
||||
// Name defines an additional name for balancer.
|
||||
// Useful for balancer testing to avoid register conflicts.
|
||||
// If empty, defaults to policy name.
|
||||
Name string
|
||||
|
||||
// Logger configures balancer logging.
|
||||
// If nil, logs are discarded.
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright 2018 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 balancer
|
||||
|
||||
import "google.golang.org/grpc/connectivity"
|
||||
|
||||
// connectivityStateEvaluator gets updated by addrConns when their
|
||||
// states transition, based on which it evaluates the state of
|
||||
// ClientConn.
|
||||
type connectivityStateEvaluator struct {
|
||||
numReady uint64 // Number of addrConns in ready state.
|
||||
numConnecting uint64 // Number of addrConns in connecting state.
|
||||
numTransientFailure uint64 // Number of addrConns in transientFailure.
|
||||
}
|
||||
|
||||
// recordTransition records state change happening in every subConn and based on
|
||||
// that it evaluates what aggregated state should be.
|
||||
// It can only transition between Ready, Connecting and TransientFailure. Other states,
|
||||
// Idle and Shutdown are transitioned into by ClientConn; in the beginning of the connection
|
||||
// before any subConn is created ClientConn is in idle state. In the end when ClientConn
|
||||
// closes it is in Shutdown state.
|
||||
//
|
||||
// recordTransition should only be called synchronously from the same goroutine.
|
||||
func (cse *connectivityStateEvaluator) recordTransition(oldState, newState connectivity.State) connectivity.State {
|
||||
// Update counters.
|
||||
for idx, state := range []connectivity.State{oldState, newState} {
|
||||
updateVal := 2*uint64(idx) - 1 // -1 for oldState and +1 for new.
|
||||
switch state {
|
||||
case connectivity.Ready:
|
||||
cse.numReady += updateVal
|
||||
case connectivity.Connecting:
|
||||
cse.numConnecting += updateVal
|
||||
case connectivity.TransientFailure:
|
||||
cse.numTransientFailure += updateVal
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate.
|
||||
if cse.numReady > 0 {
|
||||
return connectivity.Ready
|
||||
}
|
||||
if cse.numConnecting > 0 {
|
||||
return connectivity.Connecting
|
||||
}
|
||||
return connectivity.TransientFailure
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2018 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 picker defines/implements client balancer picker policy.
|
||||
package picker
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2018 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 picker
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
)
|
||||
|
||||
// NewErr returns a picker that always returns err on "Pick".
|
||||
func NewErr(err error) Picker {
|
||||
return &errPicker{err: err}
|
||||
}
|
||||
|
||||
type errPicker struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (p *errPicker) Pick(context.Context, balancer.PickOptions) (balancer.SubConn, func(balancer.DoneInfo), error) {
|
||||
return nil, nil, p.err
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2018 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 picker
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc/balancer"
|
||||
)
|
||||
|
||||
// Picker defines balancer Picker methods.
|
||||
type Picker interface {
|
||||
balancer.Picker
|
||||
}
|
||||
49
vendor/github.com/coreos/etcd/clientv3/balancer/picker/picker_policy.go
generated
vendored
Normal file
49
vendor/github.com/coreos/etcd/clientv3/balancer/picker/picker_policy.go
generated
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright 2018 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 picker
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Policy defines balancer picker policy.
|
||||
type Policy uint8
|
||||
|
||||
const (
|
||||
// TODO: custom picker is not supported yet.
|
||||
// custom defines custom balancer picker.
|
||||
custom Policy = iota
|
||||
|
||||
// RoundrobinBalanced balance loads over multiple endpoints
|
||||
// and implements failover in roundrobin fashion.
|
||||
RoundrobinBalanced Policy = iota
|
||||
|
||||
// TODO: only send loads to pinned address "RoundrobinFailover"
|
||||
// just like how 3.3 client works
|
||||
//
|
||||
// TODO: priotize leader
|
||||
// TODO: health-check
|
||||
// TODO: weighted roundrobin
|
||||
// TODO: power of two random choice
|
||||
)
|
||||
|
||||
func (p Policy) String() string {
|
||||
switch p {
|
||||
case custom:
|
||||
panic("'custom' picker policy is not supported yet")
|
||||
case RoundrobinBalanced:
|
||||
return "etcd-client-roundrobin-balanced"
|
||||
default:
|
||||
panic(fmt.Errorf("invalid balancer picker policy (%d)", p))
|
||||
}
|
||||
}
|
||||
92
vendor/github.com/coreos/etcd/clientv3/balancer/picker/roundrobin_balanced.go
generated
vendored
Normal file
92
vendor/github.com/coreos/etcd/clientv3/balancer/picker/roundrobin_balanced.go
generated
vendored
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
// Copyright 2018 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 picker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
// NewRoundrobinBalanced returns a new roundrobin balanced picker.
|
||||
func NewRoundrobinBalanced(
|
||||
lg *zap.Logger,
|
||||
scs []balancer.SubConn,
|
||||
addrToSc map[resolver.Address]balancer.SubConn,
|
||||
scToAddr map[balancer.SubConn]resolver.Address,
|
||||
) Picker {
|
||||
return &rrBalanced{
|
||||
lg: lg,
|
||||
scs: scs,
|
||||
addrToSc: addrToSc,
|
||||
scToAddr: scToAddr,
|
||||
}
|
||||
}
|
||||
|
||||
type rrBalanced struct {
|
||||
lg *zap.Logger
|
||||
|
||||
mu sync.RWMutex
|
||||
next int
|
||||
scs []balancer.SubConn
|
||||
|
||||
addrToSc map[resolver.Address]balancer.SubConn
|
||||
scToAddr map[balancer.SubConn]resolver.Address
|
||||
}
|
||||
|
||||
// Pick is called for every client request.
|
||||
func (rb *rrBalanced) Pick(ctx context.Context, opts balancer.PickOptions) (balancer.SubConn, func(balancer.DoneInfo), error) {
|
||||
rb.mu.RLock()
|
||||
n := len(rb.scs)
|
||||
rb.mu.RUnlock()
|
||||
if n == 0 {
|
||||
return nil, nil, balancer.ErrNoSubConnAvailable
|
||||
}
|
||||
|
||||
rb.mu.Lock()
|
||||
cur := rb.next
|
||||
sc := rb.scs[cur]
|
||||
picked := rb.scToAddr[sc].Addr
|
||||
rb.next = (rb.next + 1) % len(rb.scs)
|
||||
rb.mu.Unlock()
|
||||
|
||||
rb.lg.Debug(
|
||||
"picked",
|
||||
zap.String("address", picked),
|
||||
zap.Int("subconn-index", cur),
|
||||
zap.Int("subconn-size", n),
|
||||
)
|
||||
|
||||
doneFunc := func(info balancer.DoneInfo) {
|
||||
// TODO: error handling?
|
||||
fss := []zapcore.Field{
|
||||
zap.Error(info.Err),
|
||||
zap.String("address", picked),
|
||||
zap.Bool("success", info.Err == nil),
|
||||
zap.Bool("bytes-sent", info.BytesSent),
|
||||
zap.Bool("bytes-received", info.BytesReceived),
|
||||
}
|
||||
if info.Err == nil {
|
||||
rb.lg.Debug("balancer done", fss...)
|
||||
} else {
|
||||
rb.lg.Warn("balancer failed", fss...)
|
||||
}
|
||||
}
|
||||
return sc, doneFunc, nil
|
||||
}
|
||||
229
vendor/github.com/coreos/etcd/clientv3/balancer/resolver/endpoint/endpoint.go
generated
vendored
Normal file
229
vendor/github.com/coreos/etcd/clientv3/balancer/resolver/endpoint/endpoint.go
generated
vendored
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
// Copyright 2018 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 endpoint resolves etcd entpoints using grpc targets of the form 'endpoint://<id>/<endpoint>'.
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
const scheme = "endpoint"
|
||||
|
||||
var (
|
||||
targetPrefix = fmt.Sprintf("%s://", scheme)
|
||||
|
||||
bldr *builder
|
||||
)
|
||||
|
||||
func init() {
|
||||
bldr = &builder{
|
||||
resolverGroups: make(map[string]*ResolverGroup),
|
||||
}
|
||||
resolver.Register(bldr)
|
||||
}
|
||||
|
||||
type builder struct {
|
||||
mu sync.RWMutex
|
||||
resolverGroups map[string]*ResolverGroup
|
||||
}
|
||||
|
||||
// NewResolverGroup creates a new ResolverGroup with the given id.
|
||||
func NewResolverGroup(id string) (*ResolverGroup, error) {
|
||||
return bldr.newResolverGroup(id)
|
||||
}
|
||||
|
||||
// ResolverGroup keeps all endpoints of resolvers using a common endpoint://<id>/ target
|
||||
// up-to-date.
|
||||
type ResolverGroup struct {
|
||||
mu sync.RWMutex
|
||||
id string
|
||||
endpoints []string
|
||||
resolvers []*Resolver
|
||||
}
|
||||
|
||||
func (e *ResolverGroup) addResolver(r *Resolver) {
|
||||
e.mu.Lock()
|
||||
addrs := epsToAddrs(e.endpoints...)
|
||||
e.resolvers = append(e.resolvers, r)
|
||||
e.mu.Unlock()
|
||||
r.cc.NewAddress(addrs)
|
||||
}
|
||||
|
||||
func (e *ResolverGroup) removeResolver(r *Resolver) {
|
||||
e.mu.Lock()
|
||||
for i, er := range e.resolvers {
|
||||
if er == r {
|
||||
e.resolvers = append(e.resolvers[:i], e.resolvers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetEndpoints updates the endpoints for ResolverGroup. All registered resolver are updated
|
||||
// immediately with the new endpoints.
|
||||
func (e *ResolverGroup) SetEndpoints(endpoints []string) {
|
||||
addrs := epsToAddrs(endpoints...)
|
||||
e.mu.Lock()
|
||||
e.endpoints = endpoints
|
||||
for _, r := range e.resolvers {
|
||||
r.cc.NewAddress(addrs)
|
||||
}
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
// Target constructs a endpoint target using the endpoint id of the ResolverGroup.
|
||||
func (e *ResolverGroup) Target(endpoint string) string {
|
||||
return Target(e.id, endpoint)
|
||||
}
|
||||
|
||||
// Target constructs a endpoint resolver target.
|
||||
func Target(id, endpoint string) string {
|
||||
return fmt.Sprintf("%s://%s/%s", scheme, id, endpoint)
|
||||
}
|
||||
|
||||
// IsTarget checks if a given target string in an endpoint resolver target.
|
||||
func IsTarget(target string) bool {
|
||||
return strings.HasPrefix(target, "endpoint://")
|
||||
}
|
||||
|
||||
func (e *ResolverGroup) Close() {
|
||||
bldr.close(e.id)
|
||||
}
|
||||
|
||||
// Build creates or reuses an etcd resolver for the etcd cluster name identified by the authority part of the target.
|
||||
func (b *builder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOption) (resolver.Resolver, error) {
|
||||
if len(target.Authority) < 1 {
|
||||
return nil, fmt.Errorf("'etcd' target scheme requires non-empty authority identifying etcd cluster being routed to")
|
||||
}
|
||||
id := target.Authority
|
||||
es, err := b.getResolverGroup(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build resolver: %v", err)
|
||||
}
|
||||
r := &Resolver{
|
||||
endpointID: id,
|
||||
cc: cc,
|
||||
}
|
||||
es.addResolver(r)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (b *builder) newResolverGroup(id string) (*ResolverGroup, error) {
|
||||
b.mu.RLock()
|
||||
_, ok := b.resolverGroups[id]
|
||||
b.mu.RUnlock()
|
||||
if ok {
|
||||
return nil, fmt.Errorf("Endpoint already exists for id: %s", id)
|
||||
}
|
||||
|
||||
es := &ResolverGroup{id: id}
|
||||
b.mu.Lock()
|
||||
b.resolverGroups[id] = es
|
||||
b.mu.Unlock()
|
||||
return es, nil
|
||||
}
|
||||
|
||||
func (b *builder) getResolverGroup(id string) (*ResolverGroup, error) {
|
||||
b.mu.RLock()
|
||||
es, ok := b.resolverGroups[id]
|
||||
b.mu.RUnlock()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ResolverGroup not found for id: %s", id)
|
||||
}
|
||||
return es, nil
|
||||
}
|
||||
|
||||
func (b *builder) close(id string) {
|
||||
b.mu.Lock()
|
||||
delete(b.resolverGroups, id)
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *builder) Scheme() string {
|
||||
return scheme
|
||||
}
|
||||
|
||||
// Resolver provides a resolver for a single etcd cluster, identified by name.
|
||||
type Resolver struct {
|
||||
endpointID string
|
||||
cc resolver.ClientConn
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// TODO: use balancer.epsToAddrs
|
||||
func epsToAddrs(eps ...string) (addrs []resolver.Address) {
|
||||
addrs = make([]resolver.Address, 0, len(eps))
|
||||
for _, ep := range eps {
|
||||
addrs = append(addrs, resolver.Address{Addr: ep})
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
|
||||
func (*Resolver) ResolveNow(o resolver.ResolveNowOption) {}
|
||||
|
||||
func (r *Resolver) Close() {
|
||||
es, err := bldr.getResolverGroup(r.endpointID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
es.removeResolver(r)
|
||||
}
|
||||
|
||||
// ParseEndpoint endpoint parses an endpoint of the form
|
||||
// (http|https)://<host>*|(unix|unixs)://<path>)
|
||||
// and returns a protocol ('tcp' or 'unix'),
|
||||
// host (or filepath if a unix socket),
|
||||
// scheme (http, https, unix, unixs).
|
||||
func ParseEndpoint(endpoint string) (proto string, host string, scheme string) {
|
||||
proto = "tcp"
|
||||
host = endpoint
|
||||
url, uerr := url.Parse(endpoint)
|
||||
if uerr != nil || !strings.Contains(endpoint, "://") {
|
||||
return proto, host, scheme
|
||||
}
|
||||
scheme = url.Scheme
|
||||
|
||||
// strip scheme:// prefix since grpc dials by host
|
||||
host = url.Host
|
||||
switch url.Scheme {
|
||||
case "http", "https":
|
||||
case "unix", "unixs":
|
||||
proto = "unix"
|
||||
host = url.Host + url.Path
|
||||
default:
|
||||
proto, host = "", ""
|
||||
}
|
||||
return proto, host, scheme
|
||||
}
|
||||
|
||||
// ParseTarget parses a endpoint://<id>/<endpoint> string and returns the parsed id and endpoint.
|
||||
// If the target is malformed, an error is returned.
|
||||
func ParseTarget(target string) (string, string, error) {
|
||||
noPrefix := strings.TrimPrefix(target, targetPrefix)
|
||||
if noPrefix == target {
|
||||
return "", "", fmt.Errorf("malformed target, %s prefix is required: %s", targetPrefix, target)
|
||||
}
|
||||
parts := strings.SplitN(noPrefix, "/", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", "", fmt.Errorf("malformed target, expected %s://<id>/<endpoint>, but got %s", scheme, target)
|
||||
}
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright 2018 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 balancer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sort"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
func scToString(sc balancer.SubConn) string {
|
||||
return fmt.Sprintf("%p", sc)
|
||||
}
|
||||
|
||||
func scsToStrings(scs map[resolver.Address]balancer.SubConn) (ss []string) {
|
||||
ss = make([]string, 0, len(scs))
|
||||
for a, sc := range scs {
|
||||
ss = append(ss, fmt.Sprintf("%s (%s)", a.Addr, scToString(sc)))
|
||||
}
|
||||
sort.Strings(ss)
|
||||
return ss
|
||||
}
|
||||
|
||||
func addrsToStrings(addrs []resolver.Address) (ss []string) {
|
||||
ss = make([]string, len(addrs))
|
||||
for i := range addrs {
|
||||
ss[i] = addrs[i].Addr
|
||||
}
|
||||
sort.Strings(ss)
|
||||
return ss
|
||||
}
|
||||
|
||||
func epsToAddrs(eps ...string) (addrs []resolver.Address) {
|
||||
addrs = make([]resolver.Address, 0, len(eps))
|
||||
for _, ep := range eps {
|
||||
u, err := url.Parse(ep)
|
||||
if err != nil {
|
||||
addrs = append(addrs, resolver.Address{Addr: ep, Type: resolver.Backend})
|
||||
continue
|
||||
}
|
||||
addrs = append(addrs, resolver.Address{Addr: u.Host, Type: resolver.Backend})
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
|
||||
var genN = new(uint32)
|
||||
|
||||
func genName() string {
|
||||
now := time.Now().UnixNano()
|
||||
return fmt.Sprintf("%X%X", now, atomic.AddUint32(genN, 1))
|
||||
}
|
||||
|
|
@ -27,7 +27,11 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/coreos/etcd/clientv3/balancer"
|
||||
"github.com/coreos/etcd/clientv3/balancer/picker"
|
||||
"github.com/coreos/etcd/clientv3/balancer/resolver/endpoint"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/util/backoffutils"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
|
|
@ -40,8 +44,21 @@ import (
|
|||
var (
|
||||
ErrNoAvailableEndpoints = errors.New("etcdclient: no available endpoints")
|
||||
ErrOldCluster = errors.New("etcdclient: old cluster version")
|
||||
|
||||
roundRobinBalancerName = fmt.Sprintf("etcd-%s", picker.RoundrobinBalanced.String())
|
||||
)
|
||||
|
||||
func init() {
|
||||
balancer.RegisterBuilder(balancer.Config{
|
||||
Policy: picker.RoundrobinBalanced,
|
||||
Name: roundRobinBalancerName,
|
||||
|
||||
// TODO: configure from clientv3.Config
|
||||
Logger: zap.NewNop(),
|
||||
// Logger: zap.NewExample(),
|
||||
})
|
||||
}
|
||||
|
||||
// Client provides and manages an etcd v3 client session.
|
||||
type Client struct {
|
||||
Cluster
|
||||
|
|
@ -51,13 +68,13 @@ type Client struct {
|
|||
Auth
|
||||
Maintenance
|
||||
|
||||
conn *grpc.ClientConn
|
||||
dialerrc chan error
|
||||
conn *grpc.ClientConn
|
||||
|
||||
cfg Config
|
||||
creds *credentials.TransportCredentials
|
||||
balancer *balancer.GRPC17Health
|
||||
mu *sync.Mutex
|
||||
cfg Config
|
||||
creds *credentials.TransportCredentials
|
||||
balancer balancer.Balancer
|
||||
resolverGroup *endpoint.ResolverGroup
|
||||
mu *sync.Mutex
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
|
@ -70,6 +87,8 @@ type Client struct {
|
|||
tokenCred *authTokenCredential
|
||||
|
||||
callOpts []grpc.CallOption
|
||||
|
||||
lg *zap.Logger
|
||||
}
|
||||
|
||||
// New creates a new etcdv3 client from a given configuration.
|
||||
|
|
@ -104,6 +123,9 @@ func (c *Client) Close() error {
|
|||
c.cancel()
|
||||
c.Watcher.Close()
|
||||
c.Lease.Close()
|
||||
if c.resolverGroup != nil {
|
||||
c.resolverGroup.Close()
|
||||
}
|
||||
if c.conn != nil {
|
||||
return toErr(c.ctx, c.conn.Close())
|
||||
}
|
||||
|
|
@ -126,16 +148,9 @@ func (c *Client) Endpoints() (eps []string) {
|
|||
// SetEndpoints updates client's endpoints.
|
||||
func (c *Client) SetEndpoints(eps ...string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.cfg.Endpoints = eps
|
||||
c.mu.Unlock()
|
||||
c.balancer.UpdateAddrs(eps...)
|
||||
|
||||
if c.balancer.NeedUpdate() {
|
||||
select {
|
||||
case c.balancer.UpdateAddrsC() <- balancer.NotifyNext:
|
||||
case <-c.balancer.StopC():
|
||||
}
|
||||
}
|
||||
c.resolverGroup.SetEndpoints(eps)
|
||||
}
|
||||
|
||||
// Sync synchronizes client's endpoints with the known endpoints from the etcd membership.
|
||||
|
|
@ -189,28 +204,6 @@ func (cred authTokenCredential) GetRequestMetadata(ctx context.Context, s ...str
|
|||
}, nil
|
||||
}
|
||||
|
||||
func parseEndpoint(endpoint string) (proto string, host string, scheme string) {
|
||||
proto = "tcp"
|
||||
host = endpoint
|
||||
url, uerr := url.Parse(endpoint)
|
||||
if uerr != nil || !strings.Contains(endpoint, "://") {
|
||||
return proto, host, scheme
|
||||
}
|
||||
scheme = url.Scheme
|
||||
|
||||
// strip scheme:// prefix since grpc dials by host
|
||||
host = url.Host
|
||||
switch url.Scheme {
|
||||
case "http", "https":
|
||||
case "unix", "unixs":
|
||||
proto = "unix"
|
||||
host = url.Host + url.Path
|
||||
default:
|
||||
proto, host = "", ""
|
||||
}
|
||||
return proto, host, scheme
|
||||
}
|
||||
|
||||
func (c *Client) processCreds(scheme string) (creds *credentials.TransportCredentials) {
|
||||
creds = c.creds
|
||||
switch scheme {
|
||||
|
|
@ -231,10 +224,12 @@ func (c *Client) processCreds(scheme string) (creds *credentials.TransportCreden
|
|||
}
|
||||
|
||||
// dialSetupOpts gives the dial opts prior to any authentication
|
||||
func (c *Client) dialSetupOpts(endpoint string, dopts ...grpc.DialOption) (opts []grpc.DialOption) {
|
||||
if c.cfg.DialTimeout > 0 {
|
||||
opts = []grpc.DialOption{grpc.WithTimeout(c.cfg.DialTimeout)}
|
||||
func (c *Client) dialSetupOpts(target string, dopts ...grpc.DialOption) (opts []grpc.DialOption, err error) {
|
||||
_, ep, err := endpoint.ParseTarget(target)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse target: %v", err)
|
||||
}
|
||||
|
||||
if c.cfg.DialKeepAliveTime > 0 {
|
||||
params := keepalive.ClientParameters{
|
||||
Time: c.cfg.DialKeepAliveTime,
|
||||
|
|
@ -244,12 +239,12 @@ func (c *Client) dialSetupOpts(endpoint string, dopts ...grpc.DialOption) (opts
|
|||
}
|
||||
opts = append(opts, dopts...)
|
||||
|
||||
f := func(host string, t time.Duration) (net.Conn, error) {
|
||||
proto, host, _ := parseEndpoint(c.balancer.Endpoint(host))
|
||||
if host == "" && endpoint != "" {
|
||||
f := func(dialEp string, t time.Duration) (net.Conn, error) {
|
||||
proto, host, _ := endpoint.ParseEndpoint(dialEp)
|
||||
if host == "" && ep != "" {
|
||||
// dialing an endpoint not in the balancer; use
|
||||
// endpoint passed into dial
|
||||
proto, host, _ = parseEndpoint(endpoint)
|
||||
proto, host, _ = endpoint.ParseEndpoint(ep)
|
||||
}
|
||||
if proto == "" {
|
||||
return nil, fmt.Errorf("unknown scheme for %q", host)
|
||||
|
|
@ -260,19 +255,12 @@ func (c *Client) dialSetupOpts(endpoint string, dopts ...grpc.DialOption) (opts
|
|||
default:
|
||||
}
|
||||
dialer := &net.Dialer{Timeout: t}
|
||||
conn, err := dialer.DialContext(c.ctx, proto, host)
|
||||
if err != nil {
|
||||
select {
|
||||
case c.dialerrc <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
return conn, err
|
||||
return dialer.DialContext(c.ctx, proto, host)
|
||||
}
|
||||
opts = append(opts, grpc.WithDialer(f))
|
||||
|
||||
creds := c.creds
|
||||
if _, _, scheme := parseEndpoint(endpoint); len(scheme) != 0 {
|
||||
if _, _, scheme := endpoint.ParseEndpoint(ep); len(scheme) != 0 {
|
||||
creds = c.processCreds(scheme)
|
||||
}
|
||||
if creds != nil {
|
||||
|
|
@ -281,7 +269,19 @@ func (c *Client) dialSetupOpts(endpoint string, dopts ...grpc.DialOption) (opts
|
|||
opts = append(opts, grpc.WithInsecure())
|
||||
}
|
||||
|
||||
return opts
|
||||
// Interceptor retry and backoff.
|
||||
// TODO: Replace all of clientv3/retry.go with interceptor based retry, or with
|
||||
// https://github.com/grpc/proposal/blob/master/A6-client-retries.md#retry-policy
|
||||
// once it is available.
|
||||
rrBackoff := withBackoff(c.roundRobinQuorumBackoff(defaultBackoffWaitBetween, defaultBackoffJitterFraction))
|
||||
opts = append(opts,
|
||||
// Disable stream retry by default since go-grpc-middleware/retry does not support client streams.
|
||||
// Streams that are safe to retry are enabled individually.
|
||||
grpc.WithStreamInterceptor(c.streamClientInterceptor(c.lg, withMax(0), rrBackoff)),
|
||||
grpc.WithUnaryInterceptor(c.unaryClientInterceptor(c.lg, withMax(defaultUnaryMaxRetries), rrBackoff)),
|
||||
)
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// Dial connects to a single endpoint using the client's config.
|
||||
|
|
@ -294,10 +294,18 @@ func (c *Client) getToken(ctx context.Context) error {
|
|||
var auth *authenticator
|
||||
|
||||
for i := 0; i < len(c.cfg.Endpoints); i++ {
|
||||
endpoint := c.cfg.Endpoints[i]
|
||||
host := getHost(endpoint)
|
||||
ep := c.cfg.Endpoints[i]
|
||||
// use dial options without dopts to avoid reusing the client balancer
|
||||
auth, err = newAuthenticator(host, c.dialSetupOpts(endpoint), c)
|
||||
var dOpts []grpc.DialOption
|
||||
_, host, _ := endpoint.ParseEndpoint(ep)
|
||||
target := c.resolverGroup.Target(host)
|
||||
dOpts, err = c.dialSetupOpts(target, c.cfg.DialOptions...)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to configure auth dialer: %v", err)
|
||||
continue
|
||||
}
|
||||
dOpts = append(dOpts, grpc.WithBalancerName(roundRobinBalancerName))
|
||||
auth, err = newAuthenticator(ctx, target, dOpts, c)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -319,37 +327,52 @@ func (c *Client) getToken(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (c *Client) dial(endpoint string, dopts ...grpc.DialOption) (*grpc.ClientConn, error) {
|
||||
opts := c.dialSetupOpts(endpoint, dopts...)
|
||||
host := getHost(endpoint)
|
||||
func (c *Client) dial(ep string, dopts ...grpc.DialOption) (*grpc.ClientConn, error) {
|
||||
// We pass a target to DialContext of the form: endpoint://<clusterName>/<host-part> that
|
||||
// does not include scheme (http/https/unix/unixs) or path parts.
|
||||
_, host, _ := endpoint.ParseEndpoint(ep)
|
||||
target := c.resolverGroup.Target(host)
|
||||
|
||||
opts, err := c.dialSetupOpts(target, dopts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to configure dialer: %v", err)
|
||||
}
|
||||
|
||||
if c.Username != "" && c.Password != "" {
|
||||
c.tokenCred = &authTokenCredential{
|
||||
tokenMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
ctx := c.ctx
|
||||
ctx, cancel := c.ctx, func() {}
|
||||
if c.cfg.DialTimeout > 0 {
|
||||
cctx, cancel := context.WithTimeout(ctx, c.cfg.DialTimeout)
|
||||
defer cancel()
|
||||
ctx = cctx
|
||||
ctx, cancel = context.WithTimeout(ctx, c.cfg.DialTimeout)
|
||||
}
|
||||
|
||||
err := c.getToken(ctx)
|
||||
err = c.getToken(ctx)
|
||||
if err != nil {
|
||||
if toErr(ctx, err) != rpctypes.ErrAuthNotEnabled {
|
||||
if err == ctx.Err() && ctx.Err() != c.ctx.Err() {
|
||||
err = context.DeadlineExceeded
|
||||
}
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
opts = append(opts, grpc.WithPerRPCCredentials(c.tokenCred))
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
|
||||
opts = append(opts, c.cfg.DialOptions...)
|
||||
|
||||
conn, err := grpc.DialContext(c.ctx, host, opts...)
|
||||
dctx := c.ctx
|
||||
if c.cfg.DialTimeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
dctx, cancel = context.WithTimeout(c.ctx, c.cfg.DialTimeout)
|
||||
defer cancel() // TODO: Is this right for cases where grpc.WithBlock() is not set on the dial options?
|
||||
}
|
||||
|
||||
conn, err := grpc.DialContext(dctx, target, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -382,7 +405,6 @@ func newClient(cfg *Config) (*Client, error) {
|
|||
ctx, cancel := context.WithCancel(baseCtx)
|
||||
client := &Client{
|
||||
conn: nil,
|
||||
dialerrc: make(chan error, 1),
|
||||
cfg: *cfg,
|
||||
creds: creds,
|
||||
ctx: ctx,
|
||||
|
|
@ -390,6 +412,17 @@ func newClient(cfg *Config) (*Client, error) {
|
|||
mu: new(sync.Mutex),
|
||||
callOpts: defaultCallOpts,
|
||||
}
|
||||
|
||||
lcfg := DefaultLogConfig
|
||||
if cfg.LogConfig != nil {
|
||||
lcfg = *cfg.LogConfig
|
||||
}
|
||||
var err error
|
||||
client.lg, err = lcfg.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cfg.Username != "" && cfg.Password != "" {
|
||||
client.Username = cfg.Username
|
||||
client.Password = cfg.Password
|
||||
|
|
@ -412,40 +445,31 @@ func newClient(cfg *Config) (*Client, error) {
|
|||
client.callOpts = callOpts
|
||||
}
|
||||
|
||||
client.balancer = balancer.NewGRPC17Health(cfg.Endpoints, cfg.DialTimeout, client.dial)
|
||||
|
||||
// use Endpoints[0] so that for https:// without any tls config given, then
|
||||
// grpc will assume the certificate server name is the endpoint host.
|
||||
conn, err := client.dial(cfg.Endpoints[0], grpc.WithBalancer(client.balancer))
|
||||
// Prepare a 'endpoint://<unique-client-id>/' resolver for the client and create a endpoint target to pass
|
||||
// to dial so the client knows to use this resolver.
|
||||
client.resolverGroup, err = endpoint.NewResolverGroup(fmt.Sprintf("client-%s", strconv.FormatInt(time.Now().UnixNano(), 36)))
|
||||
if err != nil {
|
||||
client.cancel()
|
||||
client.balancer.Close()
|
||||
return nil, err
|
||||
}
|
||||
client.conn = conn
|
||||
client.resolverGroup.SetEndpoints(cfg.Endpoints)
|
||||
|
||||
// wait for a connection
|
||||
if cfg.DialTimeout > 0 {
|
||||
hasConn := false
|
||||
waitc := time.After(cfg.DialTimeout)
|
||||
select {
|
||||
case <-client.balancer.Ready():
|
||||
hasConn = true
|
||||
case <-ctx.Done():
|
||||
case <-waitc:
|
||||
}
|
||||
if !hasConn {
|
||||
err := context.DeadlineExceeded
|
||||
select {
|
||||
case err = <-client.dialerrc:
|
||||
default:
|
||||
}
|
||||
client.cancel()
|
||||
client.balancer.Close()
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if len(cfg.Endpoints) < 1 {
|
||||
return nil, fmt.Errorf("at least one Endpoint must is required in client config")
|
||||
}
|
||||
dialEndpoint := cfg.Endpoints[0]
|
||||
|
||||
// Use an provided endpoint target so that for https:// without any tls config given, then
|
||||
// grpc will assume the certificate server name is the endpoint host.
|
||||
conn, err := client.dial(dialEndpoint, grpc.WithBalancerName(roundRobinBalancerName))
|
||||
if err != nil {
|
||||
client.cancel()
|
||||
client.resolverGroup.Close()
|
||||
return nil, err
|
||||
}
|
||||
// TODO: With the old grpc balancer interface, we waited until the dial timeout
|
||||
// for the balancer to be ready. Is there an equivalent wait we should do with the new grpc balancer interface?
|
||||
client.conn = conn
|
||||
|
||||
client.Cluster = NewCluster(client)
|
||||
client.KV = NewKV(client)
|
||||
|
|
@ -465,6 +489,22 @@ func newClient(cfg *Config) (*Client, error) {
|
|||
return client, nil
|
||||
}
|
||||
|
||||
// roundRobinQuorumBackoff retries against quorum between each backoff.
|
||||
// This is intended for use with a round robin load balancer.
|
||||
func (c *Client) roundRobinQuorumBackoff(waitBetween time.Duration, jitterFraction float64) backoffFunc {
|
||||
return func(attempt uint) time.Duration {
|
||||
// after each round robin across quorum, backoff for our wait between duration
|
||||
n := uint(len(c.Endpoints()))
|
||||
quorum := (n/2 + 1)
|
||||
if attempt%quorum == 0 {
|
||||
c.lg.Info("backoff", zap.Uint("attempt", attempt), zap.Uint("quorum", quorum), zap.Duration("waitBetween", waitBetween), zap.Float64("jitterFraction", jitterFraction))
|
||||
return backoffutils.JitterUp(waitBetween, jitterFraction)
|
||||
}
|
||||
c.lg.Info("backoff skipped", zap.Uint("attempt", attempt), zap.Uint("quorum", quorum))
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) checkVersion() (err error) {
|
||||
var wg sync.WaitGroup
|
||||
errc := make(chan error, len(c.cfg.Endpoints))
|
||||
|
|
@ -574,6 +614,26 @@ func canceledByCaller(stopCtx context.Context, err error) bool {
|
|||
return err == context.Canceled || err == context.DeadlineExceeded
|
||||
}
|
||||
|
||||
// IsConnCanceled returns true, if error is from a closed gRPC connection.
|
||||
// ref. https://github.com/grpc/grpc-go/pull/1854
|
||||
func IsConnCanceled(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
// >= gRPC v1.10.x
|
||||
s, ok := status.FromError(err)
|
||||
if ok {
|
||||
// connection is canceled or server has already closed the connection
|
||||
return s.Code() == codes.Canceled || s.Message() == "transport is closing"
|
||||
}
|
||||
// >= gRPC v1.10.x
|
||||
if err == context.Canceled {
|
||||
return true
|
||||
}
|
||||
// <= gRPC v1.7.x returns 'errors.New("grpc: the client connection is closing")'
|
||||
return strings.Contains(err.Error(), "grpc: the client connection is closing")
|
||||
}
|
||||
|
||||
func getHost(ep string) string {
|
||||
url, uerr := url.Parse(ep)
|
||||
if uerr != nil || !strings.Contains(ep, "://") {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2017 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 clientv3util contains utility functions derived from clientv3.
|
||||
package clientv3util
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
)
|
||||
|
||||
// KeyExists returns a comparison operation that evaluates to true iff the given
|
||||
// key exists. It does this by checking if the key `Version` is greater than 0.
|
||||
// It is a useful guard in transaction delete operations.
|
||||
func KeyExists(key string) clientv3.Cmp {
|
||||
return clientv3.Compare(clientv3.Version(key), ">", 0)
|
||||
}
|
||||
|
||||
// KeyMissing returns a comparison operation that evaluates to true iff the
|
||||
// given key does not exist.
|
||||
func KeyMissing(key string) clientv3.Cmp {
|
||||
return clientv3.Compare(clientv3.Version(key), "=", 0)
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2016 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 concurrency implements concurrency operations on top of
|
||||
// etcd such as distributed locks, barriers, and elections.
|
||||
package concurrency
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
// Copyright 2016 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 concurrency
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/mvcc/mvccpb"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrElectionNotLeader = errors.New("election: not leader")
|
||||
ErrElectionNoLeader = errors.New("election: no leader")
|
||||
)
|
||||
|
||||
type Election struct {
|
||||
session *Session
|
||||
|
||||
keyPrefix string
|
||||
|
||||
leaderKey string
|
||||
leaderRev int64
|
||||
leaderSession *Session
|
||||
hdr *pb.ResponseHeader
|
||||
}
|
||||
|
||||
// NewElection returns a new election on a given key prefix.
|
||||
func NewElection(s *Session, pfx string) *Election {
|
||||
return &Election{session: s, keyPrefix: pfx + "/"}
|
||||
}
|
||||
|
||||
// ResumeElection initializes an election with a known leader.
|
||||
func ResumeElection(s *Session, pfx string, leaderKey string, leaderRev int64) *Election {
|
||||
return &Election{
|
||||
session: s,
|
||||
leaderKey: leaderKey,
|
||||
leaderRev: leaderRev,
|
||||
leaderSession: s,
|
||||
}
|
||||
}
|
||||
|
||||
// Campaign puts a value as eligible for the election. It blocks until
|
||||
// it is elected, an error occurs, or the context is cancelled.
|
||||
func (e *Election) Campaign(ctx context.Context, val string) error {
|
||||
s := e.session
|
||||
client := e.session.Client()
|
||||
|
||||
k := fmt.Sprintf("%s%x", e.keyPrefix, s.Lease())
|
||||
txn := client.Txn(ctx).If(v3.Compare(v3.CreateRevision(k), "=", 0))
|
||||
txn = txn.Then(v3.OpPut(k, val, v3.WithLease(s.Lease())))
|
||||
txn = txn.Else(v3.OpGet(k))
|
||||
resp, err := txn.Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.leaderKey, e.leaderRev, e.leaderSession = k, resp.Header.Revision, s
|
||||
if !resp.Succeeded {
|
||||
kv := resp.Responses[0].GetResponseRange().Kvs[0]
|
||||
e.leaderRev = kv.CreateRevision
|
||||
if string(kv.Value) != val {
|
||||
if err = e.Proclaim(ctx, val); err != nil {
|
||||
e.Resign(ctx)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = waitDeletes(ctx, client, e.keyPrefix, e.leaderRev-1)
|
||||
if err != nil {
|
||||
// clean up in case of context cancel
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
e.Resign(client.Ctx())
|
||||
default:
|
||||
e.leaderSession = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
e.hdr = resp.Header
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Proclaim lets the leader announce a new value without another election.
|
||||
func (e *Election) Proclaim(ctx context.Context, val string) error {
|
||||
if e.leaderSession == nil {
|
||||
return ErrElectionNotLeader
|
||||
}
|
||||
client := e.session.Client()
|
||||
cmp := v3.Compare(v3.CreateRevision(e.leaderKey), "=", e.leaderRev)
|
||||
txn := client.Txn(ctx).If(cmp)
|
||||
txn = txn.Then(v3.OpPut(e.leaderKey, val, v3.WithLease(e.leaderSession.Lease())))
|
||||
tresp, terr := txn.Commit()
|
||||
if terr != nil {
|
||||
return terr
|
||||
}
|
||||
if !tresp.Succeeded {
|
||||
e.leaderKey = ""
|
||||
return ErrElectionNotLeader
|
||||
}
|
||||
|
||||
e.hdr = tresp.Header
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resign lets a leader start a new election.
|
||||
func (e *Election) Resign(ctx context.Context) (err error) {
|
||||
if e.leaderSession == nil {
|
||||
return nil
|
||||
}
|
||||
client := e.session.Client()
|
||||
cmp := v3.Compare(v3.CreateRevision(e.leaderKey), "=", e.leaderRev)
|
||||
resp, err := client.Txn(ctx).If(cmp).Then(v3.OpDelete(e.leaderKey)).Commit()
|
||||
if err == nil {
|
||||
e.hdr = resp.Header
|
||||
}
|
||||
e.leaderKey = ""
|
||||
e.leaderSession = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// Leader returns the leader value for the current election.
|
||||
func (e *Election) Leader(ctx context.Context) (*v3.GetResponse, error) {
|
||||
client := e.session.Client()
|
||||
resp, err := client.Get(ctx, e.keyPrefix, v3.WithFirstCreate()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if len(resp.Kvs) == 0 {
|
||||
// no leader currently elected
|
||||
return nil, ErrElectionNoLeader
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Observe returns a channel that reliably observes ordered leader proposals
|
||||
// as GetResponse values on every current elected leader key. It will not
|
||||
// necessarily fetch all historical leader updates, but will always post the
|
||||
// most recent leader value.
|
||||
//
|
||||
// The channel closes when the context is canceled or the underlying watcher
|
||||
// is otherwise disrupted.
|
||||
func (e *Election) Observe(ctx context.Context) <-chan v3.GetResponse {
|
||||
retc := make(chan v3.GetResponse)
|
||||
go e.observe(ctx, retc)
|
||||
return retc
|
||||
}
|
||||
|
||||
func (e *Election) observe(ctx context.Context, ch chan<- v3.GetResponse) {
|
||||
client := e.session.Client()
|
||||
|
||||
defer close(ch)
|
||||
for {
|
||||
resp, err := client.Get(ctx, e.keyPrefix, v3.WithFirstCreate()...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var kv *mvccpb.KeyValue
|
||||
var hdr *pb.ResponseHeader
|
||||
|
||||
if len(resp.Kvs) == 0 {
|
||||
cctx, cancel := context.WithCancel(ctx)
|
||||
// wait for first key put on prefix
|
||||
opts := []v3.OpOption{v3.WithRev(resp.Header.Revision), v3.WithPrefix()}
|
||||
wch := client.Watch(cctx, e.keyPrefix, opts...)
|
||||
for kv == nil {
|
||||
wr, ok := <-wch
|
||||
if !ok || wr.Err() != nil {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
// only accept puts; a delete will make observe() spin
|
||||
for _, ev := range wr.Events {
|
||||
if ev.Type == mvccpb.PUT {
|
||||
hdr, kv = &wr.Header, ev.Kv
|
||||
// may have multiple revs; hdr.rev = the last rev
|
||||
// set to kv's rev in case batch has multiple Puts
|
||||
hdr.Revision = kv.ModRevision
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
} else {
|
||||
hdr, kv = resp.Header, resp.Kvs[0]
|
||||
}
|
||||
|
||||
select {
|
||||
case ch <- v3.GetResponse{Header: hdr, Kvs: []*mvccpb.KeyValue{kv}}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
cctx, cancel := context.WithCancel(ctx)
|
||||
wch := client.Watch(cctx, string(kv.Key), v3.WithRev(hdr.Revision+1))
|
||||
keyDeleted := false
|
||||
for !keyDeleted {
|
||||
wr, ok := <-wch
|
||||
if !ok {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
for _, ev := range wr.Events {
|
||||
if ev.Type == mvccpb.DELETE {
|
||||
keyDeleted = true
|
||||
break
|
||||
}
|
||||
resp.Header = &wr.Header
|
||||
resp.Kvs = []*mvccpb.KeyValue{ev.Kv}
|
||||
select {
|
||||
case ch <- *resp:
|
||||
case <-cctx.Done():
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// Key returns the leader key if elected, empty string otherwise.
|
||||
func (e *Election) Key() string { return e.leaderKey }
|
||||
|
||||
// Rev returns the leader key's creation revision, if elected.
|
||||
func (e *Election) Rev() int64 { return e.leaderRev }
|
||||
|
||||
// Header is the response header from the last successful election proposal.
|
||||
func (e *Election) Header() *pb.ResponseHeader { return e.hdr }
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright 2016 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 concurrency
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/mvcc/mvccpb"
|
||||
)
|
||||
|
||||
func waitDelete(ctx context.Context, client *v3.Client, key string, rev int64) error {
|
||||
cctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
var wr v3.WatchResponse
|
||||
wch := client.Watch(cctx, key, v3.WithRev(rev))
|
||||
for wr = range wch {
|
||||
for _, ev := range wr.Events {
|
||||
if ev.Type == mvccpb.DELETE {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := wr.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("lost watcher waiting for delete")
|
||||
}
|
||||
|
||||
// waitDeletes efficiently waits until all keys matching the prefix and no greater
|
||||
// than the create revision.
|
||||
func waitDeletes(ctx context.Context, client *v3.Client, pfx string, maxCreateRev int64) (*pb.ResponseHeader, error) {
|
||||
getOpts := append(v3.WithLastCreate(), v3.WithMaxCreateRev(maxCreateRev))
|
||||
for {
|
||||
resp, err := client.Get(ctx, pfx, getOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(resp.Kvs) == 0 {
|
||||
return resp.Header, nil
|
||||
}
|
||||
lastKey := string(resp.Kvs[0].Key)
|
||||
if err = waitDelete(ctx, client, lastKey, resp.Header.Revision); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
// Copyright 2016 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 concurrency
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
)
|
||||
|
||||
// Mutex implements the sync Locker interface with etcd
|
||||
type Mutex struct {
|
||||
s *Session
|
||||
|
||||
pfx string
|
||||
myKey string
|
||||
myRev int64
|
||||
hdr *pb.ResponseHeader
|
||||
}
|
||||
|
||||
func NewMutex(s *Session, pfx string) *Mutex {
|
||||
return &Mutex{s, pfx + "/", "", -1, nil}
|
||||
}
|
||||
|
||||
// Lock locks the mutex with a cancelable context. If the context is canceled
|
||||
// while trying to acquire the lock, the mutex tries to clean its stale lock entry.
|
||||
func (m *Mutex) Lock(ctx context.Context) error {
|
||||
s := m.s
|
||||
client := m.s.Client()
|
||||
|
||||
m.myKey = fmt.Sprintf("%s%x", m.pfx, s.Lease())
|
||||
cmp := v3.Compare(v3.CreateRevision(m.myKey), "=", 0)
|
||||
// put self in lock waiters via myKey; oldest waiter holds lock
|
||||
put := v3.OpPut(m.myKey, "", v3.WithLease(s.Lease()))
|
||||
// reuse key in case this session already holds the lock
|
||||
get := v3.OpGet(m.myKey)
|
||||
// fetch current holder to complete uncontended path with only one RPC
|
||||
getOwner := v3.OpGet(m.pfx, v3.WithFirstCreate()...)
|
||||
resp, err := client.Txn(ctx).If(cmp).Then(put, getOwner).Else(get, getOwner).Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.myRev = resp.Header.Revision
|
||||
if !resp.Succeeded {
|
||||
m.myRev = resp.Responses[0].GetResponseRange().Kvs[0].CreateRevision
|
||||
}
|
||||
// if no key on prefix / the minimum rev is key, already hold the lock
|
||||
ownerKey := resp.Responses[1].GetResponseRange().Kvs
|
||||
if len(ownerKey) == 0 || ownerKey[0].CreateRevision == m.myRev {
|
||||
m.hdr = resp.Header
|
||||
return nil
|
||||
}
|
||||
|
||||
// wait for deletion revisions prior to myKey
|
||||
hdr, werr := waitDeletes(ctx, client, m.pfx, m.myRev-1)
|
||||
// release lock key if cancelled
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
m.Unlock(client.Ctx())
|
||||
default:
|
||||
m.hdr = hdr
|
||||
}
|
||||
return werr
|
||||
}
|
||||
|
||||
func (m *Mutex) Unlock(ctx context.Context) error {
|
||||
client := m.s.Client()
|
||||
if _, err := client.Delete(ctx, m.myKey); err != nil {
|
||||
return err
|
||||
}
|
||||
m.myKey = "\x00"
|
||||
m.myRev = -1
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Mutex) IsOwner() v3.Cmp {
|
||||
return v3.Compare(v3.CreateRevision(m.myKey), "=", m.myRev)
|
||||
}
|
||||
|
||||
func (m *Mutex) Key() string { return m.myKey }
|
||||
|
||||
// Header is the response header received from etcd on acquiring the lock.
|
||||
func (m *Mutex) Header() *pb.ResponseHeader { return m.hdr }
|
||||
|
||||
type lockerMutex struct{ *Mutex }
|
||||
|
||||
func (lm *lockerMutex) Lock() {
|
||||
client := lm.s.Client()
|
||||
if err := lm.Mutex.Lock(client.Ctx()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
func (lm *lockerMutex) Unlock() {
|
||||
client := lm.s.Client()
|
||||
if err := lm.Mutex.Unlock(client.Ctx()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// NewLocker creates a sync.Locker backed by an etcd mutex.
|
||||
func NewLocker(s *Session, pfx string) sync.Locker {
|
||||
return &lockerMutex{NewMutex(s, pfx)}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
// Copyright 2016 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 concurrency
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
)
|
||||
|
||||
const defaultSessionTTL = 60
|
||||
|
||||
// Session represents a lease kept alive for the lifetime of a client.
|
||||
// Fault-tolerant applications may use sessions to reason about liveness.
|
||||
type Session struct {
|
||||
client *v3.Client
|
||||
opts *sessionOptions
|
||||
id v3.LeaseID
|
||||
|
||||
cancel context.CancelFunc
|
||||
donec <-chan struct{}
|
||||
}
|
||||
|
||||
// NewSession gets the leased session for a client.
|
||||
func NewSession(client *v3.Client, opts ...SessionOption) (*Session, error) {
|
||||
ops := &sessionOptions{ttl: defaultSessionTTL, ctx: client.Ctx()}
|
||||
for _, opt := range opts {
|
||||
opt(ops)
|
||||
}
|
||||
|
||||
id := ops.leaseID
|
||||
if id == v3.NoLease {
|
||||
resp, err := client.Grant(ops.ctx, int64(ops.ttl))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id = v3.LeaseID(resp.ID)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ops.ctx)
|
||||
keepAlive, err := client.KeepAlive(ctx, id)
|
||||
if err != nil || keepAlive == nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
donec := make(chan struct{})
|
||||
s := &Session{client: client, opts: ops, id: id, cancel: cancel, donec: donec}
|
||||
|
||||
// keep the lease alive until client error or cancelled context
|
||||
go func() {
|
||||
defer close(donec)
|
||||
for range keepAlive {
|
||||
// eat messages until keep alive channel closes
|
||||
}
|
||||
}()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Client is the etcd client that is attached to the session.
|
||||
func (s *Session) Client() *v3.Client {
|
||||
return s.client
|
||||
}
|
||||
|
||||
// Lease is the lease ID for keys bound to the session.
|
||||
func (s *Session) Lease() v3.LeaseID { return s.id }
|
||||
|
||||
// Done returns a channel that closes when the lease is orphaned, expires, or
|
||||
// is otherwise no longer being refreshed.
|
||||
func (s *Session) Done() <-chan struct{} { return s.donec }
|
||||
|
||||
// Orphan ends the refresh for the session lease. This is useful
|
||||
// in case the state of the client connection is indeterminate (revoke
|
||||
// would fail) or when transferring lease ownership.
|
||||
func (s *Session) Orphan() {
|
||||
s.cancel()
|
||||
<-s.donec
|
||||
}
|
||||
|
||||
// Close orphans the session and revokes the session lease.
|
||||
func (s *Session) Close() error {
|
||||
s.Orphan()
|
||||
// if revoke takes longer than the ttl, lease is expired anyway
|
||||
ctx, cancel := context.WithTimeout(s.opts.ctx, time.Duration(s.opts.ttl)*time.Second)
|
||||
_, err := s.client.Revoke(ctx, s.id)
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
type sessionOptions struct {
|
||||
ttl int
|
||||
leaseID v3.LeaseID
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// SessionOption configures Session.
|
||||
type SessionOption func(*sessionOptions)
|
||||
|
||||
// WithTTL configures the session's TTL in seconds.
|
||||
// If TTL is <= 0, the default 60 seconds TTL will be used.
|
||||
func WithTTL(ttl int) SessionOption {
|
||||
return func(so *sessionOptions) {
|
||||
if ttl > 0 {
|
||||
so.ttl = ttl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithLease specifies the existing leaseID to be used for the session.
|
||||
// This is useful in process restart scenario, for example, to reclaim
|
||||
// leadership from an election prior to restart.
|
||||
func WithLease(leaseID v3.LeaseID) SessionOption {
|
||||
return func(so *sessionOptions) {
|
||||
so.leaseID = leaseID
|
||||
}
|
||||
}
|
||||
|
||||
// WithContext assigns a context to the session instead of defaulting to
|
||||
// using the client context. This is useful for canceling NewSession and
|
||||
// Close operations immediately without having to close the client. If the
|
||||
// context is canceled before Close() completes, the session's lease will be
|
||||
// abandoned and left to expire instead of being revoked.
|
||||
func WithContext(ctx context.Context) SessionOption {
|
||||
return func(so *sessionOptions) {
|
||||
so.ctx = ctx
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,387 @@
|
|||
// Copyright 2016 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 concurrency
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
)
|
||||
|
||||
// STM is an interface for software transactional memory.
|
||||
type STM interface {
|
||||
// Get returns the value for a key and inserts the key in the txn's read set.
|
||||
// If Get fails, it aborts the transaction with an error, never returning.
|
||||
Get(key ...string) string
|
||||
// Put adds a value for a key to the write set.
|
||||
Put(key, val string, opts ...v3.OpOption)
|
||||
// Rev returns the revision of a key in the read set.
|
||||
Rev(key string) int64
|
||||
// Del deletes a key.
|
||||
Del(key string)
|
||||
|
||||
// commit attempts to apply the txn's changes to the server.
|
||||
commit() *v3.TxnResponse
|
||||
reset()
|
||||
}
|
||||
|
||||
// Isolation is an enumeration of transactional isolation levels which
|
||||
// describes how transactions should interfere and conflict.
|
||||
type Isolation int
|
||||
|
||||
const (
|
||||
// SerializableSnapshot provides serializable isolation and also checks
|
||||
// for write conflicts.
|
||||
SerializableSnapshot Isolation = iota
|
||||
// Serializable reads within the same transaction attempt return data
|
||||
// from the at the revision of the first read.
|
||||
Serializable
|
||||
// RepeatableReads reads within the same transaction attempt always
|
||||
// return the same data.
|
||||
RepeatableReads
|
||||
// ReadCommitted reads keys from any committed revision.
|
||||
ReadCommitted
|
||||
)
|
||||
|
||||
// stmError safely passes STM errors through panic to the STM error channel.
|
||||
type stmError struct{ err error }
|
||||
|
||||
type stmOptions struct {
|
||||
iso Isolation
|
||||
ctx context.Context
|
||||
prefetch []string
|
||||
}
|
||||
|
||||
type stmOption func(*stmOptions)
|
||||
|
||||
// WithIsolation specifies the transaction isolation level.
|
||||
func WithIsolation(lvl Isolation) stmOption {
|
||||
return func(so *stmOptions) { so.iso = lvl }
|
||||
}
|
||||
|
||||
// WithAbortContext specifies the context for permanently aborting the transaction.
|
||||
func WithAbortContext(ctx context.Context) stmOption {
|
||||
return func(so *stmOptions) { so.ctx = ctx }
|
||||
}
|
||||
|
||||
// WithPrefetch is a hint to prefetch a list of keys before trying to apply.
|
||||
// If an STM transaction will unconditionally fetch a set of keys, prefetching
|
||||
// those keys will save the round-trip cost from requesting each key one by one
|
||||
// with Get().
|
||||
func WithPrefetch(keys ...string) stmOption {
|
||||
return func(so *stmOptions) { so.prefetch = append(so.prefetch, keys...) }
|
||||
}
|
||||
|
||||
// NewSTM initiates a new STM instance, using serializable snapshot isolation by default.
|
||||
func NewSTM(c *v3.Client, apply func(STM) error, so ...stmOption) (*v3.TxnResponse, error) {
|
||||
opts := &stmOptions{ctx: c.Ctx()}
|
||||
for _, f := range so {
|
||||
f(opts)
|
||||
}
|
||||
if len(opts.prefetch) != 0 {
|
||||
f := apply
|
||||
apply = func(s STM) error {
|
||||
s.Get(opts.prefetch...)
|
||||
return f(s)
|
||||
}
|
||||
}
|
||||
return runSTM(mkSTM(c, opts), apply)
|
||||
}
|
||||
|
||||
func mkSTM(c *v3.Client, opts *stmOptions) STM {
|
||||
switch opts.iso {
|
||||
case SerializableSnapshot:
|
||||
s := &stmSerializable{
|
||||
stm: stm{client: c, ctx: opts.ctx},
|
||||
prefetch: make(map[string]*v3.GetResponse),
|
||||
}
|
||||
s.conflicts = func() []v3.Cmp {
|
||||
return append(s.rset.cmps(), s.wset.cmps(s.rset.first()+1)...)
|
||||
}
|
||||
return s
|
||||
case Serializable:
|
||||
s := &stmSerializable{
|
||||
stm: stm{client: c, ctx: opts.ctx},
|
||||
prefetch: make(map[string]*v3.GetResponse),
|
||||
}
|
||||
s.conflicts = func() []v3.Cmp { return s.rset.cmps() }
|
||||
return s
|
||||
case RepeatableReads:
|
||||
s := &stm{client: c, ctx: opts.ctx, getOpts: []v3.OpOption{v3.WithSerializable()}}
|
||||
s.conflicts = func() []v3.Cmp { return s.rset.cmps() }
|
||||
return s
|
||||
case ReadCommitted:
|
||||
s := &stm{client: c, ctx: opts.ctx, getOpts: []v3.OpOption{v3.WithSerializable()}}
|
||||
s.conflicts = func() []v3.Cmp { return nil }
|
||||
return s
|
||||
default:
|
||||
panic("unsupported stm")
|
||||
}
|
||||
}
|
||||
|
||||
type stmResponse struct {
|
||||
resp *v3.TxnResponse
|
||||
err error
|
||||
}
|
||||
|
||||
func runSTM(s STM, apply func(STM) error) (*v3.TxnResponse, error) {
|
||||
outc := make(chan stmResponse, 1)
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
e, ok := r.(stmError)
|
||||
if !ok {
|
||||
// client apply panicked
|
||||
panic(r)
|
||||
}
|
||||
outc <- stmResponse{nil, e.err}
|
||||
}
|
||||
}()
|
||||
var out stmResponse
|
||||
for {
|
||||
s.reset()
|
||||
if out.err = apply(s); out.err != nil {
|
||||
break
|
||||
}
|
||||
if out.resp = s.commit(); out.resp != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
outc <- out
|
||||
}()
|
||||
r := <-outc
|
||||
return r.resp, r.err
|
||||
}
|
||||
|
||||
// stm implements repeatable-read software transactional memory over etcd
|
||||
type stm struct {
|
||||
client *v3.Client
|
||||
ctx context.Context
|
||||
// rset holds read key values and revisions
|
||||
rset readSet
|
||||
// wset holds overwritten keys and their values
|
||||
wset writeSet
|
||||
// getOpts are the opts used for gets
|
||||
getOpts []v3.OpOption
|
||||
// conflicts computes the current conflicts on the txn
|
||||
conflicts func() []v3.Cmp
|
||||
}
|
||||
|
||||
type stmPut struct {
|
||||
val string
|
||||
op v3.Op
|
||||
}
|
||||
|
||||
type readSet map[string]*v3.GetResponse
|
||||
|
||||
func (rs readSet) add(keys []string, txnresp *v3.TxnResponse) {
|
||||
for i, resp := range txnresp.Responses {
|
||||
rs[keys[i]] = (*v3.GetResponse)(resp.GetResponseRange())
|
||||
}
|
||||
}
|
||||
|
||||
// first returns the store revision from the first fetch
|
||||
func (rs readSet) first() int64 {
|
||||
ret := int64(math.MaxInt64 - 1)
|
||||
for _, resp := range rs {
|
||||
if rev := resp.Header.Revision; rev < ret {
|
||||
ret = rev
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// cmps guards the txn from updates to read set
|
||||
func (rs readSet) cmps() []v3.Cmp {
|
||||
cmps := make([]v3.Cmp, 0, len(rs))
|
||||
for k, rk := range rs {
|
||||
cmps = append(cmps, isKeyCurrent(k, rk))
|
||||
}
|
||||
return cmps
|
||||
}
|
||||
|
||||
type writeSet map[string]stmPut
|
||||
|
||||
func (ws writeSet) get(keys ...string) *stmPut {
|
||||
for _, key := range keys {
|
||||
if wv, ok := ws[key]; ok {
|
||||
return &wv
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cmps returns a cmp list testing no writes have happened past rev
|
||||
func (ws writeSet) cmps(rev int64) []v3.Cmp {
|
||||
cmps := make([]v3.Cmp, 0, len(ws))
|
||||
for key := range ws {
|
||||
cmps = append(cmps, v3.Compare(v3.ModRevision(key), "<", rev))
|
||||
}
|
||||
return cmps
|
||||
}
|
||||
|
||||
// puts is the list of ops for all pending writes
|
||||
func (ws writeSet) puts() []v3.Op {
|
||||
puts := make([]v3.Op, 0, len(ws))
|
||||
for _, v := range ws {
|
||||
puts = append(puts, v.op)
|
||||
}
|
||||
return puts
|
||||
}
|
||||
|
||||
func (s *stm) Get(keys ...string) string {
|
||||
if wv := s.wset.get(keys...); wv != nil {
|
||||
return wv.val
|
||||
}
|
||||
return respToValue(s.fetch(keys...))
|
||||
}
|
||||
|
||||
func (s *stm) Put(key, val string, opts ...v3.OpOption) {
|
||||
s.wset[key] = stmPut{val, v3.OpPut(key, val, opts...)}
|
||||
}
|
||||
|
||||
func (s *stm) Del(key string) { s.wset[key] = stmPut{"", v3.OpDelete(key)} }
|
||||
|
||||
func (s *stm) Rev(key string) int64 {
|
||||
if resp := s.fetch(key); resp != nil && len(resp.Kvs) != 0 {
|
||||
return resp.Kvs[0].ModRevision
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *stm) commit() *v3.TxnResponse {
|
||||
txnresp, err := s.client.Txn(s.ctx).If(s.conflicts()...).Then(s.wset.puts()...).Commit()
|
||||
if err != nil {
|
||||
panic(stmError{err})
|
||||
}
|
||||
if txnresp.Succeeded {
|
||||
return txnresp
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stm) fetch(keys ...string) *v3.GetResponse {
|
||||
if len(keys) == 0 {
|
||||
return nil
|
||||
}
|
||||
ops := make([]v3.Op, len(keys))
|
||||
for i, key := range keys {
|
||||
if resp, ok := s.rset[key]; ok {
|
||||
return resp
|
||||
}
|
||||
ops[i] = v3.OpGet(key, s.getOpts...)
|
||||
}
|
||||
txnresp, err := s.client.Txn(s.ctx).Then(ops...).Commit()
|
||||
if err != nil {
|
||||
panic(stmError{err})
|
||||
}
|
||||
s.rset.add(keys, txnresp)
|
||||
return (*v3.GetResponse)(txnresp.Responses[0].GetResponseRange())
|
||||
}
|
||||
|
||||
func (s *stm) reset() {
|
||||
s.rset = make(map[string]*v3.GetResponse)
|
||||
s.wset = make(map[string]stmPut)
|
||||
}
|
||||
|
||||
type stmSerializable struct {
|
||||
stm
|
||||
prefetch map[string]*v3.GetResponse
|
||||
}
|
||||
|
||||
func (s *stmSerializable) Get(keys ...string) string {
|
||||
if wv := s.wset.get(keys...); wv != nil {
|
||||
return wv.val
|
||||
}
|
||||
firstRead := len(s.rset) == 0
|
||||
for _, key := range keys {
|
||||
if resp, ok := s.prefetch[key]; ok {
|
||||
delete(s.prefetch, key)
|
||||
s.rset[key] = resp
|
||||
}
|
||||
}
|
||||
resp := s.stm.fetch(keys...)
|
||||
if firstRead {
|
||||
// txn's base revision is defined by the first read
|
||||
s.getOpts = []v3.OpOption{
|
||||
v3.WithRev(resp.Header.Revision),
|
||||
v3.WithSerializable(),
|
||||
}
|
||||
}
|
||||
return respToValue(resp)
|
||||
}
|
||||
|
||||
func (s *stmSerializable) Rev(key string) int64 {
|
||||
s.Get(key)
|
||||
return s.stm.Rev(key)
|
||||
}
|
||||
|
||||
func (s *stmSerializable) gets() ([]string, []v3.Op) {
|
||||
keys := make([]string, 0, len(s.rset))
|
||||
ops := make([]v3.Op, 0, len(s.rset))
|
||||
for k := range s.rset {
|
||||
keys = append(keys, k)
|
||||
ops = append(ops, v3.OpGet(k))
|
||||
}
|
||||
return keys, ops
|
||||
}
|
||||
|
||||
func (s *stmSerializable) commit() *v3.TxnResponse {
|
||||
keys, getops := s.gets()
|
||||
txn := s.client.Txn(s.ctx).If(s.conflicts()...).Then(s.wset.puts()...)
|
||||
// use Else to prefetch keys in case of conflict to save a round trip
|
||||
txnresp, err := txn.Else(getops...).Commit()
|
||||
if err != nil {
|
||||
panic(stmError{err})
|
||||
}
|
||||
if txnresp.Succeeded {
|
||||
return txnresp
|
||||
}
|
||||
// load prefetch with Else data
|
||||
s.rset.add(keys, txnresp)
|
||||
s.prefetch = s.rset
|
||||
s.getOpts = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func isKeyCurrent(k string, r *v3.GetResponse) v3.Cmp {
|
||||
if len(r.Kvs) != 0 {
|
||||
return v3.Compare(v3.ModRevision(k), "=", r.Kvs[0].ModRevision)
|
||||
}
|
||||
return v3.Compare(v3.ModRevision(k), "=", 0)
|
||||
}
|
||||
|
||||
func respToValue(resp *v3.GetResponse) string {
|
||||
if resp == nil || len(resp.Kvs) == 0 {
|
||||
return ""
|
||||
}
|
||||
return string(resp.Kvs[0].Value)
|
||||
}
|
||||
|
||||
// NewSTMRepeatable is deprecated.
|
||||
func NewSTMRepeatable(ctx context.Context, c *v3.Client, apply func(STM) error) (*v3.TxnResponse, error) {
|
||||
return NewSTM(c, apply, WithAbortContext(ctx), WithIsolation(RepeatableReads))
|
||||
}
|
||||
|
||||
// NewSTMSerializable is deprecated.
|
||||
func NewSTMSerializable(ctx context.Context, c *v3.Client, apply func(STM) error) (*v3.TxnResponse, error) {
|
||||
return NewSTM(c, apply, WithAbortContext(ctx), WithIsolation(Serializable))
|
||||
}
|
||||
|
||||
// NewSTMReadCommitted is deprecated.
|
||||
func NewSTMReadCommitted(ctx context.Context, c *v3.Client, apply func(STM) error) (*v3.TxnResponse, error) {
|
||||
return NewSTM(c, apply, WithAbortContext(ctx), WithIsolation(ReadCommitted))
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import (
|
|||
"crypto/tls"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
|
|
@ -72,4 +73,27 @@ type Config struct {
|
|||
// Context is the default client context; it can be used to cancel grpc dial out and
|
||||
// other operations that do not have an explicit context.
|
||||
Context context.Context
|
||||
|
||||
// LogConfig configures client-side logger.
|
||||
// If nil, use the default logger.
|
||||
// TODO: configure balancer and gRPC logger
|
||||
LogConfig *zap.Config
|
||||
}
|
||||
|
||||
// DefaultLogConfig is the default client logging configuration.
|
||||
// Default log level is "Warn". Use "zap.InfoLevel" for debugging.
|
||||
// Use "/dev/null" for output paths, to discard all logs.
|
||||
var DefaultLogConfig = zap.Config{
|
||||
Level: zap.NewAtomicLevelAt(zap.WarnLevel),
|
||||
Development: false,
|
||||
Sampling: &zap.SamplingConfig{
|
||||
Initial: 100,
|
||||
Thereafter: 100,
|
||||
},
|
||||
Encoding: "json",
|
||||
EncoderConfig: zap.NewProductionEncoderConfig(),
|
||||
|
||||
// Use "/dev/null" to discard all
|
||||
OutputPaths: []string{"stderr"},
|
||||
ErrorOutputPaths: []string{"stderr"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,11 +87,16 @@
|
|||
// go func() { cli.Close() }()
|
||||
// _, err := kvc.Get(ctx, "a")
|
||||
// if err != nil {
|
||||
// // with etcd clientv3 <= v3.3
|
||||
// if err == context.Canceled {
|
||||
// // grpc balancer calls 'Get' with an inflight client.Close
|
||||
// } else if err == grpc.ErrClientConnClosing {
|
||||
// // grpc balancer calls 'Get' after client.Close.
|
||||
// }
|
||||
// // with etcd clientv3 >= v3.4
|
||||
// if clientv3.IsConnCanceled(err) {
|
||||
// // gRPC client connection is closed
|
||||
// }
|
||||
// }
|
||||
//
|
||||
package clientv3
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2016 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 integration implements tests built upon embedded etcd, and focuses on
|
||||
// correctness of etcd client.
|
||||
package integration
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright 2017 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 integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
)
|
||||
|
||||
// mustWaitPinReady waits up to 3-second until connection is up (pin endpoint).
|
||||
// Fatal on time-out.
|
||||
func mustWaitPinReady(t *testing.T, cli *clientv3.Client) {
|
||||
// TODO: decrease timeout after balancer rewrite!!!
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
_, err := cli.Get(ctx, "foo")
|
||||
cancel()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -466,7 +466,7 @@ func (l *lessor) recvKeepAliveLoop() (gerr error) {
|
|||
// resetRecv opens a new lease stream and starts sending keep alive requests.
|
||||
func (l *lessor) resetRecv() (pb.Lease_LeaseKeepAliveClient, error) {
|
||||
sctx, cancel := context.WithCancel(l.stopCtx)
|
||||
stream, err := l.remote.LeaseKeepAlive(sctx, l.callOpts...)
|
||||
stream, err := l.remote.LeaseKeepAlive(sctx, append(l.callOpts, withMax(0))...)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -0,0 +1,306 @@
|
|||
// Copyright 2017 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 leasing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
v3pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/mvcc/mvccpb"
|
||||
)
|
||||
|
||||
const revokeBackoff = 2 * time.Second
|
||||
|
||||
type leaseCache struct {
|
||||
mu sync.RWMutex
|
||||
entries map[string]*leaseKey
|
||||
revokes map[string]time.Time
|
||||
header *v3pb.ResponseHeader
|
||||
}
|
||||
|
||||
type leaseKey struct {
|
||||
response *v3.GetResponse
|
||||
// rev is the leasing key revision.
|
||||
rev int64
|
||||
waitc chan struct{}
|
||||
}
|
||||
|
||||
func (lc *leaseCache) Rev(key string) int64 {
|
||||
lc.mu.RLock()
|
||||
defer lc.mu.RUnlock()
|
||||
if li := lc.entries[key]; li != nil {
|
||||
return li.rev
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (lc *leaseCache) Lock(key string) (chan<- struct{}, int64) {
|
||||
lc.mu.Lock()
|
||||
defer lc.mu.Unlock()
|
||||
if li := lc.entries[key]; li != nil {
|
||||
li.waitc = make(chan struct{})
|
||||
return li.waitc, li.rev
|
||||
}
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
func (lc *leaseCache) LockRange(begin, end string) (ret []chan<- struct{}) {
|
||||
lc.mu.Lock()
|
||||
defer lc.mu.Unlock()
|
||||
for k, li := range lc.entries {
|
||||
if inRange(k, begin, end) {
|
||||
li.waitc = make(chan struct{})
|
||||
ret = append(ret, li.waitc)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func inRange(k, begin, end string) bool {
|
||||
if strings.Compare(k, begin) < 0 {
|
||||
return false
|
||||
}
|
||||
if end != "\x00" && strings.Compare(k, end) >= 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (lc *leaseCache) LockWriteOps(ops []v3.Op) (ret []chan<- struct{}) {
|
||||
for _, op := range ops {
|
||||
if op.IsGet() {
|
||||
continue
|
||||
}
|
||||
key := string(op.KeyBytes())
|
||||
if end := string(op.RangeBytes()); end == "" {
|
||||
if wc, _ := lc.Lock(key); wc != nil {
|
||||
ret = append(ret, wc)
|
||||
}
|
||||
} else {
|
||||
for k := range lc.entries {
|
||||
if !inRange(k, key, end) {
|
||||
continue
|
||||
}
|
||||
if wc, _ := lc.Lock(k); wc != nil {
|
||||
ret = append(ret, wc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (lc *leaseCache) NotifyOps(ops []v3.Op) (wcs []<-chan struct{}) {
|
||||
for _, op := range ops {
|
||||
if op.IsGet() {
|
||||
if _, wc := lc.notify(string(op.KeyBytes())); wc != nil {
|
||||
wcs = append(wcs, wc)
|
||||
}
|
||||
}
|
||||
}
|
||||
return wcs
|
||||
}
|
||||
|
||||
func (lc *leaseCache) MayAcquire(key string) bool {
|
||||
lc.mu.RLock()
|
||||
lr, ok := lc.revokes[key]
|
||||
lc.mu.RUnlock()
|
||||
return !ok || time.Since(lr) > revokeBackoff
|
||||
}
|
||||
|
||||
func (lc *leaseCache) Add(key string, resp *v3.GetResponse, op v3.Op) *v3.GetResponse {
|
||||
lk := &leaseKey{resp, resp.Header.Revision, closedCh}
|
||||
lc.mu.Lock()
|
||||
if lc.header == nil || lc.header.Revision < resp.Header.Revision {
|
||||
lc.header = resp.Header
|
||||
}
|
||||
lc.entries[key] = lk
|
||||
ret := lk.get(op)
|
||||
lc.mu.Unlock()
|
||||
return ret
|
||||
}
|
||||
|
||||
func (lc *leaseCache) Update(key, val []byte, respHeader *v3pb.ResponseHeader) {
|
||||
li := lc.entries[string(key)]
|
||||
if li == nil {
|
||||
return
|
||||
}
|
||||
cacheResp := li.response
|
||||
if len(cacheResp.Kvs) == 0 {
|
||||
kv := &mvccpb.KeyValue{
|
||||
Key: key,
|
||||
CreateRevision: respHeader.Revision,
|
||||
}
|
||||
cacheResp.Kvs = append(cacheResp.Kvs, kv)
|
||||
cacheResp.Count = 1
|
||||
}
|
||||
cacheResp.Kvs[0].Version++
|
||||
if cacheResp.Kvs[0].ModRevision < respHeader.Revision {
|
||||
cacheResp.Header = respHeader
|
||||
cacheResp.Kvs[0].ModRevision = respHeader.Revision
|
||||
cacheResp.Kvs[0].Value = val
|
||||
}
|
||||
}
|
||||
|
||||
func (lc *leaseCache) Delete(key string, hdr *v3pb.ResponseHeader) {
|
||||
lc.mu.Lock()
|
||||
defer lc.mu.Unlock()
|
||||
lc.delete(key, hdr)
|
||||
}
|
||||
|
||||
func (lc *leaseCache) delete(key string, hdr *v3pb.ResponseHeader) {
|
||||
if li := lc.entries[key]; li != nil && hdr.Revision >= li.response.Header.Revision {
|
||||
li.response.Kvs = nil
|
||||
li.response.Header = copyHeader(hdr)
|
||||
}
|
||||
}
|
||||
|
||||
func (lc *leaseCache) Evict(key string) (rev int64) {
|
||||
lc.mu.Lock()
|
||||
defer lc.mu.Unlock()
|
||||
if li := lc.entries[key]; li != nil {
|
||||
rev = li.rev
|
||||
delete(lc.entries, key)
|
||||
lc.revokes[key] = time.Now()
|
||||
}
|
||||
return rev
|
||||
}
|
||||
|
||||
func (lc *leaseCache) EvictRange(key, end string) {
|
||||
lc.mu.Lock()
|
||||
defer lc.mu.Unlock()
|
||||
for k := range lc.entries {
|
||||
if inRange(k, key, end) {
|
||||
delete(lc.entries, key)
|
||||
lc.revokes[key] = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isBadOp(op v3.Op) bool { return op.Rev() > 0 || len(op.RangeBytes()) > 0 }
|
||||
|
||||
func (lc *leaseCache) Get(ctx context.Context, op v3.Op) (*v3.GetResponse, bool) {
|
||||
if isBadOp(op) {
|
||||
return nil, false
|
||||
}
|
||||
key := string(op.KeyBytes())
|
||||
li, wc := lc.notify(key)
|
||||
if li == nil {
|
||||
return nil, true
|
||||
}
|
||||
select {
|
||||
case <-wc:
|
||||
case <-ctx.Done():
|
||||
return nil, true
|
||||
}
|
||||
lc.mu.RLock()
|
||||
lk := *li
|
||||
ret := lk.get(op)
|
||||
lc.mu.RUnlock()
|
||||
return ret, true
|
||||
}
|
||||
|
||||
func (lk *leaseKey) get(op v3.Op) *v3.GetResponse {
|
||||
ret := *lk.response
|
||||
ret.Header = copyHeader(ret.Header)
|
||||
empty := len(ret.Kvs) == 0 || op.IsCountOnly()
|
||||
empty = empty || (op.MinModRev() > ret.Kvs[0].ModRevision)
|
||||
empty = empty || (op.MaxModRev() != 0 && op.MaxModRev() < ret.Kvs[0].ModRevision)
|
||||
empty = empty || (op.MinCreateRev() > ret.Kvs[0].CreateRevision)
|
||||
empty = empty || (op.MaxCreateRev() != 0 && op.MaxCreateRev() < ret.Kvs[0].CreateRevision)
|
||||
if empty {
|
||||
ret.Kvs = nil
|
||||
} else {
|
||||
kv := *ret.Kvs[0]
|
||||
kv.Key = make([]byte, len(kv.Key))
|
||||
copy(kv.Key, ret.Kvs[0].Key)
|
||||
if !op.IsKeysOnly() {
|
||||
kv.Value = make([]byte, len(kv.Value))
|
||||
copy(kv.Value, ret.Kvs[0].Value)
|
||||
}
|
||||
ret.Kvs = []*mvccpb.KeyValue{&kv}
|
||||
}
|
||||
return &ret
|
||||
}
|
||||
|
||||
func (lc *leaseCache) notify(key string) (*leaseKey, <-chan struct{}) {
|
||||
lc.mu.RLock()
|
||||
defer lc.mu.RUnlock()
|
||||
if li := lc.entries[key]; li != nil {
|
||||
return li, li.waitc
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (lc *leaseCache) clearOldRevokes(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
lc.mu.Lock()
|
||||
for k, lr := range lc.revokes {
|
||||
if time.Now().Sub(lr.Add(revokeBackoff)) > 0 {
|
||||
delete(lc.revokes, k)
|
||||
}
|
||||
}
|
||||
lc.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (lc *leaseCache) evalCmp(cmps []v3.Cmp) (cmpVal bool, ok bool) {
|
||||
for _, cmp := range cmps {
|
||||
if len(cmp.RangeEnd) > 0 {
|
||||
return false, false
|
||||
}
|
||||
lk := lc.entries[string(cmp.Key)]
|
||||
if lk == nil {
|
||||
return false, false
|
||||
}
|
||||
if !evalCmp(lk.response, cmp) {
|
||||
return false, true
|
||||
}
|
||||
}
|
||||
return true, true
|
||||
}
|
||||
|
||||
func (lc *leaseCache) evalOps(ops []v3.Op) ([]*v3pb.ResponseOp, bool) {
|
||||
resps := make([]*v3pb.ResponseOp, len(ops))
|
||||
for i, op := range ops {
|
||||
if !op.IsGet() || isBadOp(op) {
|
||||
// TODO: support read-only Txn
|
||||
return nil, false
|
||||
}
|
||||
lk := lc.entries[string(op.KeyBytes())]
|
||||
if lk == nil {
|
||||
return nil, false
|
||||
}
|
||||
resp := lk.get(op)
|
||||
if resp == nil {
|
||||
return nil, false
|
||||
}
|
||||
resps[i] = &v3pb.ResponseOp{
|
||||
Response: &v3pb.ResponseOp_ResponseRange{
|
||||
ResponseRange: (*v3pb.RangeResponse)(resp),
|
||||
},
|
||||
}
|
||||
}
|
||||
return resps, true
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2017 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 leasing serves linearizable reads from a local cache by acquiring
|
||||
// exclusive write access to keys through a client-side leasing protocol. This
|
||||
// leasing layer can either directly wrap the etcd client or it can be exposed
|
||||
// through the etcd grpc proxy server, granting multiple clients write access.
|
||||
//
|
||||
// First, create a leasing KV from a clientv3.Client 'cli':
|
||||
//
|
||||
// lkv, err := leasing.NewKV(cli, "leasing-prefix")
|
||||
// if err != nil {
|
||||
// // handle error
|
||||
// }
|
||||
//
|
||||
// A range request for a key "abc" tries to acquire a leasing key so it can cache the range's
|
||||
// key locally. On the server, the leasing key is stored to "leasing-prefix/abc":
|
||||
//
|
||||
// resp, err := lkv.Get(context.TODO(), "abc")
|
||||
//
|
||||
// Future linearized read requests using 'lkv' will be served locally for the lease's lifetime:
|
||||
//
|
||||
// resp, err = lkv.Get(context.TODO(), "abc")
|
||||
//
|
||||
// If another leasing client writes to a leased key, then the owner relinquishes its exclusive
|
||||
// access, permitting the writer to modify the key:
|
||||
//
|
||||
// lkv2, err := leasing.NewKV(cli, "leasing-prefix")
|
||||
// if err != nil {
|
||||
// // handle error
|
||||
// }
|
||||
// lkv2.Put(context.TODO(), "abc", "456")
|
||||
// resp, err = lkv.Get("abc")
|
||||
//
|
||||
package leasing
|
||||
|
|
@ -0,0 +1,479 @@
|
|||
// Copyright 2017 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 leasing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/clientv3/concurrency"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/mvcc/mvccpb"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type leasingKV struct {
|
||||
cl *v3.Client
|
||||
kv v3.KV
|
||||
pfx string
|
||||
leases leaseCache
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
|
||||
sessionOpts []concurrency.SessionOption
|
||||
session *concurrency.Session
|
||||
sessionc chan struct{}
|
||||
}
|
||||
|
||||
var closedCh chan struct{}
|
||||
|
||||
func init() {
|
||||
closedCh = make(chan struct{})
|
||||
close(closedCh)
|
||||
}
|
||||
|
||||
// NewKV wraps a KV instance so that all requests are wired through a leasing protocol.
|
||||
func NewKV(cl *v3.Client, pfx string, opts ...concurrency.SessionOption) (v3.KV, func(), error) {
|
||||
cctx, cancel := context.WithCancel(cl.Ctx())
|
||||
lkv := &leasingKV{
|
||||
cl: cl,
|
||||
kv: cl.KV,
|
||||
pfx: pfx,
|
||||
leases: leaseCache{revokes: make(map[string]time.Time)},
|
||||
ctx: cctx,
|
||||
cancel: cancel,
|
||||
sessionOpts: opts,
|
||||
sessionc: make(chan struct{}),
|
||||
}
|
||||
lkv.wg.Add(2)
|
||||
go func() {
|
||||
defer lkv.wg.Done()
|
||||
lkv.monitorSession()
|
||||
}()
|
||||
go func() {
|
||||
defer lkv.wg.Done()
|
||||
lkv.leases.clearOldRevokes(cctx)
|
||||
}()
|
||||
return lkv, lkv.Close, lkv.waitSession(cctx)
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) Close() {
|
||||
lkv.cancel()
|
||||
lkv.wg.Wait()
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) Get(ctx context.Context, key string, opts ...v3.OpOption) (*v3.GetResponse, error) {
|
||||
return lkv.get(ctx, v3.OpGet(key, opts...))
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) Put(ctx context.Context, key, val string, opts ...v3.OpOption) (*v3.PutResponse, error) {
|
||||
return lkv.put(ctx, v3.OpPut(key, val, opts...))
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) Delete(ctx context.Context, key string, opts ...v3.OpOption) (*v3.DeleteResponse, error) {
|
||||
return lkv.delete(ctx, v3.OpDelete(key, opts...))
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) Do(ctx context.Context, op v3.Op) (v3.OpResponse, error) {
|
||||
switch {
|
||||
case op.IsGet():
|
||||
resp, err := lkv.get(ctx, op)
|
||||
return resp.OpResponse(), err
|
||||
case op.IsPut():
|
||||
resp, err := lkv.put(ctx, op)
|
||||
return resp.OpResponse(), err
|
||||
case op.IsDelete():
|
||||
resp, err := lkv.delete(ctx, op)
|
||||
return resp.OpResponse(), err
|
||||
case op.IsTxn():
|
||||
cmps, thenOps, elseOps := op.Txn()
|
||||
resp, err := lkv.Txn(ctx).If(cmps...).Then(thenOps...).Else(elseOps...).Commit()
|
||||
return resp.OpResponse(), err
|
||||
}
|
||||
return v3.OpResponse{}, nil
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) Compact(ctx context.Context, rev int64, opts ...v3.CompactOption) (*v3.CompactResponse, error) {
|
||||
return lkv.kv.Compact(ctx, rev, opts...)
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) Txn(ctx context.Context) v3.Txn {
|
||||
return &txnLeasing{Txn: lkv.kv.Txn(ctx), lkv: lkv, ctx: ctx}
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) monitorSession() {
|
||||
for lkv.ctx.Err() == nil {
|
||||
if lkv.session != nil {
|
||||
select {
|
||||
case <-lkv.session.Done():
|
||||
case <-lkv.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
lkv.leases.mu.Lock()
|
||||
select {
|
||||
case <-lkv.sessionc:
|
||||
lkv.sessionc = make(chan struct{})
|
||||
default:
|
||||
}
|
||||
lkv.leases.entries = make(map[string]*leaseKey)
|
||||
lkv.leases.mu.Unlock()
|
||||
|
||||
s, err := concurrency.NewSession(lkv.cl, lkv.sessionOpts...)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
lkv.leases.mu.Lock()
|
||||
lkv.session = s
|
||||
close(lkv.sessionc)
|
||||
lkv.leases.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) monitorLease(ctx context.Context, key string, rev int64) {
|
||||
cctx, cancel := context.WithCancel(lkv.ctx)
|
||||
defer cancel()
|
||||
for cctx.Err() == nil {
|
||||
if rev == 0 {
|
||||
resp, err := lkv.kv.Get(ctx, lkv.pfx+key)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
rev = resp.Header.Revision
|
||||
if len(resp.Kvs) == 0 || string(resp.Kvs[0].Value) == "REVOKE" {
|
||||
lkv.rescind(cctx, key, rev)
|
||||
return
|
||||
}
|
||||
}
|
||||
wch := lkv.cl.Watch(cctx, lkv.pfx+key, v3.WithRev(rev+1))
|
||||
for resp := range wch {
|
||||
for _, ev := range resp.Events {
|
||||
if string(ev.Kv.Value) != "REVOKE" {
|
||||
continue
|
||||
}
|
||||
if v3.LeaseID(ev.Kv.Lease) == lkv.leaseID() {
|
||||
lkv.rescind(cctx, key, ev.Kv.ModRevision)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
rev = 0
|
||||
}
|
||||
}
|
||||
|
||||
// rescind releases a lease from this client.
|
||||
func (lkv *leasingKV) rescind(ctx context.Context, key string, rev int64) {
|
||||
if lkv.leases.Evict(key) > rev {
|
||||
return
|
||||
}
|
||||
cmp := v3.Compare(v3.CreateRevision(lkv.pfx+key), "<", rev)
|
||||
op := v3.OpDelete(lkv.pfx + key)
|
||||
for ctx.Err() == nil {
|
||||
if _, err := lkv.kv.Txn(ctx).If(cmp).Then(op).Commit(); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) waitRescind(ctx context.Context, key string, rev int64) error {
|
||||
cctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
wch := lkv.cl.Watch(cctx, lkv.pfx+key, v3.WithRev(rev+1))
|
||||
for resp := range wch {
|
||||
for _, ev := range resp.Events {
|
||||
if ev.Type == v3.EventTypeDelete {
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) tryModifyOp(ctx context.Context, op v3.Op) (*v3.TxnResponse, chan<- struct{}, error) {
|
||||
key := string(op.KeyBytes())
|
||||
wc, rev := lkv.leases.Lock(key)
|
||||
cmp := v3.Compare(v3.CreateRevision(lkv.pfx+key), "<", rev+1)
|
||||
resp, err := lkv.kv.Txn(ctx).If(cmp).Then(op).Commit()
|
||||
switch {
|
||||
case err != nil:
|
||||
lkv.leases.Evict(key)
|
||||
fallthrough
|
||||
case !resp.Succeeded:
|
||||
if wc != nil {
|
||||
close(wc)
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
return resp, wc, nil
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) put(ctx context.Context, op v3.Op) (pr *v3.PutResponse, err error) {
|
||||
if err := lkv.waitSession(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for ctx.Err() == nil {
|
||||
resp, wc, err := lkv.tryModifyOp(ctx, op)
|
||||
if err != nil || wc == nil {
|
||||
resp, err = lkv.revoke(ctx, string(op.KeyBytes()), op)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Succeeded {
|
||||
lkv.leases.mu.Lock()
|
||||
lkv.leases.Update(op.KeyBytes(), op.ValueBytes(), resp.Header)
|
||||
lkv.leases.mu.Unlock()
|
||||
pr = (*v3.PutResponse)(resp.Responses[0].GetResponsePut())
|
||||
pr.Header = resp.Header
|
||||
}
|
||||
if wc != nil {
|
||||
close(wc)
|
||||
}
|
||||
if resp.Succeeded {
|
||||
return pr, nil
|
||||
}
|
||||
}
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) acquire(ctx context.Context, key string, op v3.Op) (*v3.TxnResponse, error) {
|
||||
for ctx.Err() == nil {
|
||||
if err := lkv.waitSession(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lcmp := v3.Cmp{Key: []byte(key), Target: pb.Compare_LEASE}
|
||||
resp, err := lkv.kv.Txn(ctx).If(
|
||||
v3.Compare(v3.CreateRevision(lkv.pfx+key), "=", 0),
|
||||
v3.Compare(lcmp, "=", 0)).
|
||||
Then(
|
||||
op,
|
||||
v3.OpPut(lkv.pfx+key, "", v3.WithLease(lkv.leaseID()))).
|
||||
Else(
|
||||
op,
|
||||
v3.OpGet(lkv.pfx+key),
|
||||
).Commit()
|
||||
if err == nil {
|
||||
if !resp.Succeeded {
|
||||
kvs := resp.Responses[1].GetResponseRange().Kvs
|
||||
// if txn failed since already owner, lease is acquired
|
||||
resp.Succeeded = len(kvs) > 0 && v3.LeaseID(kvs[0].Lease) == lkv.leaseID()
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
// retry if transient error
|
||||
if _, ok := err.(rpctypes.EtcdError); ok {
|
||||
return nil, err
|
||||
}
|
||||
if ev, ok := status.FromError(err); ok && ev.Code() != codes.Unavailable {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) get(ctx context.Context, op v3.Op) (*v3.GetResponse, error) {
|
||||
do := func() (*v3.GetResponse, error) {
|
||||
r, err := lkv.kv.Do(ctx, op)
|
||||
return r.Get(), err
|
||||
}
|
||||
if !lkv.readySession() {
|
||||
return do()
|
||||
}
|
||||
|
||||
if resp, ok := lkv.leases.Get(ctx, op); resp != nil {
|
||||
return resp, nil
|
||||
} else if !ok || op.IsSerializable() {
|
||||
// must be handled by server or can skip linearization
|
||||
return do()
|
||||
}
|
||||
|
||||
key := string(op.KeyBytes())
|
||||
if !lkv.leases.MayAcquire(key) {
|
||||
resp, err := lkv.kv.Do(ctx, op)
|
||||
return resp.Get(), err
|
||||
}
|
||||
|
||||
resp, err := lkv.acquire(ctx, key, v3.OpGet(key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
getResp := (*v3.GetResponse)(resp.Responses[0].GetResponseRange())
|
||||
getResp.Header = resp.Header
|
||||
if resp.Succeeded {
|
||||
getResp = lkv.leases.Add(key, getResp, op)
|
||||
lkv.wg.Add(1)
|
||||
go func() {
|
||||
defer lkv.wg.Done()
|
||||
lkv.monitorLease(ctx, key, resp.Header.Revision)
|
||||
}()
|
||||
}
|
||||
return getResp, nil
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) deleteRangeRPC(ctx context.Context, maxLeaseRev int64, key, end string) (*v3.DeleteResponse, error) {
|
||||
lkey, lend := lkv.pfx+key, lkv.pfx+end
|
||||
resp, err := lkv.kv.Txn(ctx).If(
|
||||
v3.Compare(v3.CreateRevision(lkey).WithRange(lend), "<", maxLeaseRev+1),
|
||||
).Then(
|
||||
v3.OpGet(key, v3.WithRange(end), v3.WithKeysOnly()),
|
||||
v3.OpDelete(key, v3.WithRange(end)),
|
||||
).Commit()
|
||||
if err != nil {
|
||||
lkv.leases.EvictRange(key, end)
|
||||
return nil, err
|
||||
}
|
||||
if !resp.Succeeded {
|
||||
return nil, nil
|
||||
}
|
||||
for _, kv := range resp.Responses[0].GetResponseRange().Kvs {
|
||||
lkv.leases.Delete(string(kv.Key), resp.Header)
|
||||
}
|
||||
delResp := (*v3.DeleteResponse)(resp.Responses[1].GetResponseDeleteRange())
|
||||
delResp.Header = resp.Header
|
||||
return delResp, nil
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) deleteRange(ctx context.Context, op v3.Op) (*v3.DeleteResponse, error) {
|
||||
key, end := string(op.KeyBytes()), string(op.RangeBytes())
|
||||
for ctx.Err() == nil {
|
||||
maxLeaseRev, err := lkv.revokeRange(ctx, key, end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wcs := lkv.leases.LockRange(key, end)
|
||||
delResp, err := lkv.deleteRangeRPC(ctx, maxLeaseRev, key, end)
|
||||
closeAll(wcs)
|
||||
if err != nil || delResp != nil {
|
||||
return delResp, err
|
||||
}
|
||||
}
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) delete(ctx context.Context, op v3.Op) (dr *v3.DeleteResponse, err error) {
|
||||
if err := lkv.waitSession(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(op.RangeBytes()) > 0 {
|
||||
return lkv.deleteRange(ctx, op)
|
||||
}
|
||||
key := string(op.KeyBytes())
|
||||
for ctx.Err() == nil {
|
||||
resp, wc, err := lkv.tryModifyOp(ctx, op)
|
||||
if err != nil || wc == nil {
|
||||
resp, err = lkv.revoke(ctx, key, op)
|
||||
}
|
||||
if err != nil {
|
||||
// don't know if delete was processed
|
||||
lkv.leases.Evict(key)
|
||||
return nil, err
|
||||
}
|
||||
if resp.Succeeded {
|
||||
dr = (*v3.DeleteResponse)(resp.Responses[0].GetResponseDeleteRange())
|
||||
dr.Header = resp.Header
|
||||
lkv.leases.Delete(key, dr.Header)
|
||||
}
|
||||
if wc != nil {
|
||||
close(wc)
|
||||
}
|
||||
if resp.Succeeded {
|
||||
return dr, nil
|
||||
}
|
||||
}
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) revoke(ctx context.Context, key string, op v3.Op) (*v3.TxnResponse, error) {
|
||||
rev := lkv.leases.Rev(key)
|
||||
txn := lkv.kv.Txn(ctx).If(v3.Compare(v3.CreateRevision(lkv.pfx+key), "<", rev+1)).Then(op)
|
||||
resp, err := txn.Else(v3.OpPut(lkv.pfx+key, "REVOKE", v3.WithIgnoreLease())).Commit()
|
||||
if err != nil || resp.Succeeded {
|
||||
return resp, err
|
||||
}
|
||||
return resp, lkv.waitRescind(ctx, key, resp.Header.Revision)
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) revokeRange(ctx context.Context, begin, end string) (int64, error) {
|
||||
lkey, lend := lkv.pfx+begin, ""
|
||||
if len(end) > 0 {
|
||||
lend = lkv.pfx + end
|
||||
}
|
||||
leaseKeys, err := lkv.kv.Get(ctx, lkey, v3.WithRange(lend))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return lkv.revokeLeaseKvs(ctx, leaseKeys.Kvs)
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) revokeLeaseKvs(ctx context.Context, kvs []*mvccpb.KeyValue) (int64, error) {
|
||||
maxLeaseRev := int64(0)
|
||||
for _, kv := range kvs {
|
||||
if rev := kv.CreateRevision; rev > maxLeaseRev {
|
||||
maxLeaseRev = rev
|
||||
}
|
||||
if v3.LeaseID(kv.Lease) == lkv.leaseID() {
|
||||
// don't revoke own keys
|
||||
continue
|
||||
}
|
||||
key := strings.TrimPrefix(string(kv.Key), lkv.pfx)
|
||||
if _, err := lkv.revoke(ctx, key, v3.OpGet(key)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return maxLeaseRev, nil
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) waitSession(ctx context.Context) error {
|
||||
lkv.leases.mu.RLock()
|
||||
sessionc := lkv.sessionc
|
||||
lkv.leases.mu.RUnlock()
|
||||
select {
|
||||
case <-sessionc:
|
||||
return nil
|
||||
case <-lkv.ctx.Done():
|
||||
return lkv.ctx.Err()
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) readySession() bool {
|
||||
lkv.leases.mu.RLock()
|
||||
defer lkv.leases.mu.RUnlock()
|
||||
if lkv.session == nil {
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case <-lkv.session.Done():
|
||||
default:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (lkv *leasingKV) leaseID() v3.LeaseID {
|
||||
lkv.leases.mu.RLock()
|
||||
defer lkv.leases.mu.RUnlock()
|
||||
return lkv.session.Lease()
|
||||
}
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
// Copyright 2017 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 leasing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
v3pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
)
|
||||
|
||||
type txnLeasing struct {
|
||||
v3.Txn
|
||||
lkv *leasingKV
|
||||
ctx context.Context
|
||||
cs []v3.Cmp
|
||||
opst []v3.Op
|
||||
opse []v3.Op
|
||||
}
|
||||
|
||||
func (txn *txnLeasing) If(cs ...v3.Cmp) v3.Txn {
|
||||
txn.cs = append(txn.cs, cs...)
|
||||
txn.Txn = txn.Txn.If(cs...)
|
||||
return txn
|
||||
}
|
||||
|
||||
func (txn *txnLeasing) Then(ops ...v3.Op) v3.Txn {
|
||||
txn.opst = append(txn.opst, ops...)
|
||||
txn.Txn = txn.Txn.Then(ops...)
|
||||
return txn
|
||||
}
|
||||
|
||||
func (txn *txnLeasing) Else(ops ...v3.Op) v3.Txn {
|
||||
txn.opse = append(txn.opse, ops...)
|
||||
txn.Txn = txn.Txn.Else(ops...)
|
||||
return txn
|
||||
}
|
||||
|
||||
func (txn *txnLeasing) Commit() (*v3.TxnResponse, error) {
|
||||
if resp, err := txn.eval(); resp != nil || err != nil {
|
||||
return resp, err
|
||||
}
|
||||
return txn.serverTxn()
|
||||
}
|
||||
|
||||
func (txn *txnLeasing) eval() (*v3.TxnResponse, error) {
|
||||
// TODO: wait on keys in comparisons
|
||||
thenOps, elseOps := gatherOps(txn.opst), gatherOps(txn.opse)
|
||||
ops := make([]v3.Op, 0, len(thenOps)+len(elseOps))
|
||||
ops = append(ops, thenOps...)
|
||||
ops = append(ops, elseOps...)
|
||||
|
||||
for _, ch := range txn.lkv.leases.NotifyOps(ops) {
|
||||
select {
|
||||
case <-ch:
|
||||
case <-txn.ctx.Done():
|
||||
return nil, txn.ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
txn.lkv.leases.mu.RLock()
|
||||
defer txn.lkv.leases.mu.RUnlock()
|
||||
succeeded, ok := txn.lkv.leases.evalCmp(txn.cs)
|
||||
if !ok || txn.lkv.leases.header == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if ops = txn.opst; !succeeded {
|
||||
ops = txn.opse
|
||||
}
|
||||
|
||||
resps, ok := txn.lkv.leases.evalOps(ops)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return &v3.TxnResponse{Header: copyHeader(txn.lkv.leases.header), Succeeded: succeeded, Responses: resps}, nil
|
||||
}
|
||||
|
||||
// fallback computes the ops to fetch all possible conflicting
|
||||
// leasing keys for a list of ops.
|
||||
func (txn *txnLeasing) fallback(ops []v3.Op) (fbOps []v3.Op) {
|
||||
for _, op := range ops {
|
||||
if op.IsGet() {
|
||||
continue
|
||||
}
|
||||
lkey, lend := txn.lkv.pfx+string(op.KeyBytes()), ""
|
||||
if len(op.RangeBytes()) > 0 {
|
||||
lend = txn.lkv.pfx + string(op.RangeBytes())
|
||||
}
|
||||
fbOps = append(fbOps, v3.OpGet(lkey, v3.WithRange(lend)))
|
||||
}
|
||||
return fbOps
|
||||
}
|
||||
|
||||
func (txn *txnLeasing) guardKeys(ops []v3.Op) (cmps []v3.Cmp) {
|
||||
seen := make(map[string]bool)
|
||||
for _, op := range ops {
|
||||
key := string(op.KeyBytes())
|
||||
if op.IsGet() || len(op.RangeBytes()) != 0 || seen[key] {
|
||||
continue
|
||||
}
|
||||
rev := txn.lkv.leases.Rev(key)
|
||||
cmps = append(cmps, v3.Compare(v3.CreateRevision(txn.lkv.pfx+key), "<", rev+1))
|
||||
seen[key] = true
|
||||
}
|
||||
return cmps
|
||||
}
|
||||
|
||||
func (txn *txnLeasing) guardRanges(ops []v3.Op) (cmps []v3.Cmp, err error) {
|
||||
for _, op := range ops {
|
||||
if op.IsGet() || len(op.RangeBytes()) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
key, end := string(op.KeyBytes()), string(op.RangeBytes())
|
||||
maxRevLK, err := txn.lkv.revokeRange(txn.ctx, key, end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts := append(v3.WithLastRev(), v3.WithRange(end))
|
||||
getResp, err := txn.lkv.kv.Get(txn.ctx, key, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxModRev := int64(0)
|
||||
if len(getResp.Kvs) > 0 {
|
||||
maxModRev = getResp.Kvs[0].ModRevision
|
||||
}
|
||||
|
||||
noKeyUpdate := v3.Compare(v3.ModRevision(key).WithRange(end), "<", maxModRev+1)
|
||||
noLeaseUpdate := v3.Compare(
|
||||
v3.CreateRevision(txn.lkv.pfx+key).WithRange(txn.lkv.pfx+end),
|
||||
"<",
|
||||
maxRevLK+1)
|
||||
cmps = append(cmps, noKeyUpdate, noLeaseUpdate)
|
||||
}
|
||||
return cmps, nil
|
||||
}
|
||||
|
||||
func (txn *txnLeasing) guard(ops []v3.Op) ([]v3.Cmp, error) {
|
||||
cmps := txn.guardKeys(ops)
|
||||
rangeCmps, err := txn.guardRanges(ops)
|
||||
return append(cmps, rangeCmps...), err
|
||||
}
|
||||
|
||||
func (txn *txnLeasing) commitToCache(txnResp *v3pb.TxnResponse, userTxn v3.Op) {
|
||||
ops := gatherResponseOps(txnResp.Responses, []v3.Op{userTxn})
|
||||
txn.lkv.leases.mu.Lock()
|
||||
for _, op := range ops {
|
||||
key := string(op.KeyBytes())
|
||||
if op.IsDelete() && len(op.RangeBytes()) > 0 {
|
||||
end := string(op.RangeBytes())
|
||||
for k := range txn.lkv.leases.entries {
|
||||
if inRange(k, key, end) {
|
||||
txn.lkv.leases.delete(k, txnResp.Header)
|
||||
}
|
||||
}
|
||||
} else if op.IsDelete() {
|
||||
txn.lkv.leases.delete(key, txnResp.Header)
|
||||
}
|
||||
if op.IsPut() {
|
||||
txn.lkv.leases.Update(op.KeyBytes(), op.ValueBytes(), txnResp.Header)
|
||||
}
|
||||
}
|
||||
txn.lkv.leases.mu.Unlock()
|
||||
}
|
||||
|
||||
func (txn *txnLeasing) revokeFallback(fbResps []*v3pb.ResponseOp) error {
|
||||
for _, resp := range fbResps {
|
||||
_, err := txn.lkv.revokeLeaseKvs(txn.ctx, resp.GetResponseRange().Kvs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txn *txnLeasing) serverTxn() (*v3.TxnResponse, error) {
|
||||
if err := txn.lkv.waitSession(txn.ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userOps := gatherOps(append(txn.opst, txn.opse...))
|
||||
userTxn := v3.OpTxn(txn.cs, txn.opst, txn.opse)
|
||||
fbOps := txn.fallback(userOps)
|
||||
|
||||
defer closeAll(txn.lkv.leases.LockWriteOps(userOps))
|
||||
for {
|
||||
cmps, err := txn.guard(userOps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := txn.lkv.kv.Txn(txn.ctx).If(cmps...).Then(userTxn).Else(fbOps...).Commit()
|
||||
if err != nil {
|
||||
for _, cmp := range cmps {
|
||||
txn.lkv.leases.Evict(strings.TrimPrefix(string(cmp.Key), txn.lkv.pfx))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if resp.Succeeded {
|
||||
txn.commitToCache((*v3pb.TxnResponse)(resp), userTxn)
|
||||
userResp := resp.Responses[0].GetResponseTxn()
|
||||
userResp.Header = resp.Header
|
||||
return (*v3.TxnResponse)(userResp), nil
|
||||
}
|
||||
if err := txn.revokeFallback(resp.Responses); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
// Copyright 2017 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 leasing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
v3pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
)
|
||||
|
||||
func compareInt64(a, b int64) int {
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func evalCmp(resp *v3.GetResponse, tcmp v3.Cmp) bool {
|
||||
var result int
|
||||
if len(resp.Kvs) != 0 {
|
||||
kv := resp.Kvs[0]
|
||||
switch tcmp.Target {
|
||||
case v3pb.Compare_VALUE:
|
||||
if tv, _ := tcmp.TargetUnion.(*v3pb.Compare_Value); tv != nil {
|
||||
result = bytes.Compare(kv.Value, tv.Value)
|
||||
}
|
||||
case v3pb.Compare_CREATE:
|
||||
if tv, _ := tcmp.TargetUnion.(*v3pb.Compare_CreateRevision); tv != nil {
|
||||
result = compareInt64(kv.CreateRevision, tv.CreateRevision)
|
||||
}
|
||||
case v3pb.Compare_MOD:
|
||||
if tv, _ := tcmp.TargetUnion.(*v3pb.Compare_ModRevision); tv != nil {
|
||||
result = compareInt64(kv.ModRevision, tv.ModRevision)
|
||||
}
|
||||
case v3pb.Compare_VERSION:
|
||||
if tv, _ := tcmp.TargetUnion.(*v3pb.Compare_Version); tv != nil {
|
||||
result = compareInt64(kv.Version, tv.Version)
|
||||
}
|
||||
}
|
||||
}
|
||||
switch tcmp.Result {
|
||||
case v3pb.Compare_EQUAL:
|
||||
return result == 0
|
||||
case v3pb.Compare_NOT_EQUAL:
|
||||
return result != 0
|
||||
case v3pb.Compare_GREATER:
|
||||
return result > 0
|
||||
case v3pb.Compare_LESS:
|
||||
return result < 0
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func gatherOps(ops []v3.Op) (ret []v3.Op) {
|
||||
for _, op := range ops {
|
||||
if !op.IsTxn() {
|
||||
ret = append(ret, op)
|
||||
continue
|
||||
}
|
||||
_, thenOps, elseOps := op.Txn()
|
||||
ret = append(ret, gatherOps(append(thenOps, elseOps...))...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func gatherResponseOps(resp []*v3pb.ResponseOp, ops []v3.Op) (ret []v3.Op) {
|
||||
for i, op := range ops {
|
||||
if !op.IsTxn() {
|
||||
ret = append(ret, op)
|
||||
continue
|
||||
}
|
||||
_, thenOps, elseOps := op.Txn()
|
||||
if txnResp := resp[i].GetResponseTxn(); txnResp.Succeeded {
|
||||
ret = append(ret, gatherResponseOps(txnResp.Responses, thenOps)...)
|
||||
} else {
|
||||
ret = append(ret, gatherResponseOps(txnResp.Responses, elseOps)...)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func copyHeader(hdr *v3pb.ResponseHeader) *v3pb.ResponseHeader {
|
||||
h := *hdr
|
||||
return &h
|
||||
}
|
||||
|
||||
func closeAll(chs []chan<- struct{}) {
|
||||
for _, ch := range chs {
|
||||
close(ch)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ package clientv3
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
|
|
@ -77,7 +78,7 @@ func NewMaintenance(c *Client) Maintenance {
|
|||
dial: func(endpoint string) (pb.MaintenanceClient, func(), error) {
|
||||
conn, err := c.dial(endpoint)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, fmt.Errorf("failed to dial endpoint %s with maintenance client: %v", endpoint, err)
|
||||
}
|
||||
cancel := func() { conn.Close() }
|
||||
return RetryMaintenanceClient(c, conn), cancel, nil
|
||||
|
|
@ -175,6 +176,7 @@ func (m *maintenance) Status(ctx context.Context, endpoint string) (*StatusRespo
|
|||
func (m *maintenance) HashKV(ctx context.Context, endpoint string, rev int64) (*HashKVResponse, error) {
|
||||
remote, cancel, err := m.dial(endpoint)
|
||||
if err != nil {
|
||||
|
||||
return nil, toErr(ctx, err)
|
||||
}
|
||||
defer cancel()
|
||||
|
|
@ -186,7 +188,7 @@ func (m *maintenance) HashKV(ctx context.Context, endpoint string, rev int64) (*
|
|||
}
|
||||
|
||||
func (m *maintenance) Snapshot(ctx context.Context) (io.ReadCloser, error) {
|
||||
ss, err := m.remote.Snapshot(ctx, &pb.SnapshotRequest{}, m.callOpts...)
|
||||
ss, err := m.remote.Snapshot(ctx, &pb.SnapshotRequest{}, append(m.callOpts, withMax(defaultStreamMaxRetries))...)
|
||||
if err != nil {
|
||||
return nil, toErr(ctx, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
// Copyright 2016 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 mirror implements etcd mirroring operations.
|
||||
package mirror
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
)
|
||||
|
||||
const (
|
||||
batchLimit = 1000
|
||||
)
|
||||
|
||||
// Syncer syncs with the key-value state of an etcd cluster.
|
||||
type Syncer interface {
|
||||
// SyncBase syncs the base state of the key-value state.
|
||||
// The key-value state are sent through the returned chan.
|
||||
SyncBase(ctx context.Context) (<-chan clientv3.GetResponse, chan error)
|
||||
// SyncUpdates syncs the updates of the key-value state.
|
||||
// The update events are sent through the returned chan.
|
||||
SyncUpdates(ctx context.Context) clientv3.WatchChan
|
||||
}
|
||||
|
||||
// NewSyncer creates a Syncer.
|
||||
func NewSyncer(c *clientv3.Client, prefix string, rev int64) Syncer {
|
||||
return &syncer{c: c, prefix: prefix, rev: rev}
|
||||
}
|
||||
|
||||
type syncer struct {
|
||||
c *clientv3.Client
|
||||
rev int64
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (s *syncer) SyncBase(ctx context.Context) (<-chan clientv3.GetResponse, chan error) {
|
||||
respchan := make(chan clientv3.GetResponse, 1024)
|
||||
errchan := make(chan error, 1)
|
||||
|
||||
// if rev is not specified, we will choose the most recent revision.
|
||||
if s.rev == 0 {
|
||||
resp, err := s.c.Get(ctx, "foo")
|
||||
if err != nil {
|
||||
errchan <- err
|
||||
close(respchan)
|
||||
close(errchan)
|
||||
return respchan, errchan
|
||||
}
|
||||
s.rev = resp.Header.Revision
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer close(respchan)
|
||||
defer close(errchan)
|
||||
|
||||
var key string
|
||||
|
||||
opts := []clientv3.OpOption{clientv3.WithLimit(batchLimit), clientv3.WithRev(s.rev)}
|
||||
|
||||
if len(s.prefix) == 0 {
|
||||
// If len(s.prefix) == 0, we will sync the entire key-value space.
|
||||
// We then range from the smallest key (0x00) to the end.
|
||||
opts = append(opts, clientv3.WithFromKey())
|
||||
key = "\x00"
|
||||
} else {
|
||||
// If len(s.prefix) != 0, we will sync key-value space with given prefix.
|
||||
// We then range from the prefix to the next prefix if exists. Or we will
|
||||
// range from the prefix to the end if the next prefix does not exists.
|
||||
opts = append(opts, clientv3.WithRange(clientv3.GetPrefixRangeEnd(s.prefix)))
|
||||
key = s.prefix
|
||||
}
|
||||
|
||||
for {
|
||||
resp, err := s.c.Get(ctx, key, opts...)
|
||||
if err != nil {
|
||||
errchan <- err
|
||||
return
|
||||
}
|
||||
|
||||
respchan <- (clientv3.GetResponse)(*resp)
|
||||
|
||||
if !resp.More {
|
||||
return
|
||||
}
|
||||
// move to next key
|
||||
key = string(append(resp.Kvs[len(resp.Kvs)-1].Key, 0))
|
||||
}
|
||||
}()
|
||||
|
||||
return respchan, errchan
|
||||
}
|
||||
|
||||
func (s *syncer) SyncUpdates(ctx context.Context) clientv3.WatchChan {
|
||||
if s.rev == 0 {
|
||||
panic("unexpected revision = 0. Calling SyncUpdates before SyncBase finishes?")
|
||||
}
|
||||
return s.c.Watch(ctx, s.prefix, clientv3.WithPrefix(), clientv3.WithRev(s.rev+1))
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright 2017 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 namespace is a clientv3 wrapper that translates all keys to begin
|
||||
// with a given prefix.
|
||||
//
|
||||
// First, create a client:
|
||||
//
|
||||
// cli, err := clientv3.New(clientv3.Config{Endpoints: []string{"localhost:2379"}})
|
||||
// if err != nil {
|
||||
// // handle error!
|
||||
// }
|
||||
//
|
||||
// Next, override the client interfaces:
|
||||
//
|
||||
// unprefixedKV := cli.KV
|
||||
// cli.KV = namespace.NewKV(cli.KV, "my-prefix/")
|
||||
// cli.Watcher = namespace.NewWatcher(cli.Watcher, "my-prefix/")
|
||||
// cli.Lease = namespace.NewLease(cli.Lease, "my-prefix/")
|
||||
//
|
||||
// Now calls using 'cli' will namespace / prefix all keys with "my-prefix/":
|
||||
//
|
||||
// cli.Put(context.TODO(), "abc", "123")
|
||||
// resp, _ := unprefixedKV.Get(context.TODO(), "my-prefix/abc")
|
||||
// fmt.Printf("%s\n", resp.Kvs[0].Value)
|
||||
// // Output: 123
|
||||
// unprefixedKV.Put(context.TODO(), "my-prefix/abc", "456")
|
||||
// resp, _ = cli.Get(context.TODO(), "abc")
|
||||
// fmt.Printf("%s\n", resp.Kvs[0].Value)
|
||||
// // Output: 456
|
||||
//
|
||||
package namespace
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
// Copyright 2017 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 namespace
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
)
|
||||
|
||||
type kvPrefix struct {
|
||||
clientv3.KV
|
||||
pfx string
|
||||
}
|
||||
|
||||
// NewKV wraps a KV instance so that all requests
|
||||
// are prefixed with a given string.
|
||||
func NewKV(kv clientv3.KV, prefix string) clientv3.KV {
|
||||
return &kvPrefix{kv, prefix}
|
||||
}
|
||||
|
||||
func (kv *kvPrefix) Put(ctx context.Context, key, val string, opts ...clientv3.OpOption) (*clientv3.PutResponse, error) {
|
||||
if len(key) == 0 {
|
||||
return nil, rpctypes.ErrEmptyKey
|
||||
}
|
||||
op := kv.prefixOp(clientv3.OpPut(key, val, opts...))
|
||||
r, err := kv.KV.Do(ctx, op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
put := r.Put()
|
||||
kv.unprefixPutResponse(put)
|
||||
return put, nil
|
||||
}
|
||||
|
||||
func (kv *kvPrefix) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) {
|
||||
if len(key) == 0 {
|
||||
return nil, rpctypes.ErrEmptyKey
|
||||
}
|
||||
r, err := kv.KV.Do(ctx, kv.prefixOp(clientv3.OpGet(key, opts...)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
get := r.Get()
|
||||
kv.unprefixGetResponse(get)
|
||||
return get, nil
|
||||
}
|
||||
|
||||
func (kv *kvPrefix) Delete(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.DeleteResponse, error) {
|
||||
if len(key) == 0 {
|
||||
return nil, rpctypes.ErrEmptyKey
|
||||
}
|
||||
r, err := kv.KV.Do(ctx, kv.prefixOp(clientv3.OpDelete(key, opts...)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
del := r.Del()
|
||||
kv.unprefixDeleteResponse(del)
|
||||
return del, nil
|
||||
}
|
||||
|
||||
func (kv *kvPrefix) Do(ctx context.Context, op clientv3.Op) (clientv3.OpResponse, error) {
|
||||
if len(op.KeyBytes()) == 0 && !op.IsTxn() {
|
||||
return clientv3.OpResponse{}, rpctypes.ErrEmptyKey
|
||||
}
|
||||
r, err := kv.KV.Do(ctx, kv.prefixOp(op))
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
switch {
|
||||
case r.Get() != nil:
|
||||
kv.unprefixGetResponse(r.Get())
|
||||
case r.Put() != nil:
|
||||
kv.unprefixPutResponse(r.Put())
|
||||
case r.Del() != nil:
|
||||
kv.unprefixDeleteResponse(r.Del())
|
||||
case r.Txn() != nil:
|
||||
kv.unprefixTxnResponse(r.Txn())
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
type txnPrefix struct {
|
||||
clientv3.Txn
|
||||
kv *kvPrefix
|
||||
}
|
||||
|
||||
func (kv *kvPrefix) Txn(ctx context.Context) clientv3.Txn {
|
||||
return &txnPrefix{kv.KV.Txn(ctx), kv}
|
||||
}
|
||||
|
||||
func (txn *txnPrefix) If(cs ...clientv3.Cmp) clientv3.Txn {
|
||||
txn.Txn = txn.Txn.If(txn.kv.prefixCmps(cs)...)
|
||||
return txn
|
||||
}
|
||||
|
||||
func (txn *txnPrefix) Then(ops ...clientv3.Op) clientv3.Txn {
|
||||
txn.Txn = txn.Txn.Then(txn.kv.prefixOps(ops)...)
|
||||
return txn
|
||||
}
|
||||
|
||||
func (txn *txnPrefix) Else(ops ...clientv3.Op) clientv3.Txn {
|
||||
txn.Txn = txn.Txn.Else(txn.kv.prefixOps(ops)...)
|
||||
return txn
|
||||
}
|
||||
|
||||
func (txn *txnPrefix) Commit() (*clientv3.TxnResponse, error) {
|
||||
resp, err := txn.Txn.Commit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
txn.kv.unprefixTxnResponse(resp)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (kv *kvPrefix) prefixOp(op clientv3.Op) clientv3.Op {
|
||||
if !op.IsTxn() {
|
||||
begin, end := kv.prefixInterval(op.KeyBytes(), op.RangeBytes())
|
||||
op.WithKeyBytes(begin)
|
||||
op.WithRangeBytes(end)
|
||||
return op
|
||||
}
|
||||
cmps, thenOps, elseOps := op.Txn()
|
||||
return clientv3.OpTxn(kv.prefixCmps(cmps), kv.prefixOps(thenOps), kv.prefixOps(elseOps))
|
||||
}
|
||||
|
||||
func (kv *kvPrefix) unprefixGetResponse(resp *clientv3.GetResponse) {
|
||||
for i := range resp.Kvs {
|
||||
resp.Kvs[i].Key = resp.Kvs[i].Key[len(kv.pfx):]
|
||||
}
|
||||
}
|
||||
|
||||
func (kv *kvPrefix) unprefixPutResponse(resp *clientv3.PutResponse) {
|
||||
if resp.PrevKv != nil {
|
||||
resp.PrevKv.Key = resp.PrevKv.Key[len(kv.pfx):]
|
||||
}
|
||||
}
|
||||
|
||||
func (kv *kvPrefix) unprefixDeleteResponse(resp *clientv3.DeleteResponse) {
|
||||
for i := range resp.PrevKvs {
|
||||
resp.PrevKvs[i].Key = resp.PrevKvs[i].Key[len(kv.pfx):]
|
||||
}
|
||||
}
|
||||
|
||||
func (kv *kvPrefix) unprefixTxnResponse(resp *clientv3.TxnResponse) {
|
||||
for _, r := range resp.Responses {
|
||||
switch tv := r.Response.(type) {
|
||||
case *pb.ResponseOp_ResponseRange:
|
||||
if tv.ResponseRange != nil {
|
||||
kv.unprefixGetResponse((*clientv3.GetResponse)(tv.ResponseRange))
|
||||
}
|
||||
case *pb.ResponseOp_ResponsePut:
|
||||
if tv.ResponsePut != nil {
|
||||
kv.unprefixPutResponse((*clientv3.PutResponse)(tv.ResponsePut))
|
||||
}
|
||||
case *pb.ResponseOp_ResponseDeleteRange:
|
||||
if tv.ResponseDeleteRange != nil {
|
||||
kv.unprefixDeleteResponse((*clientv3.DeleteResponse)(tv.ResponseDeleteRange))
|
||||
}
|
||||
case *pb.ResponseOp_ResponseTxn:
|
||||
if tv.ResponseTxn != nil {
|
||||
kv.unprefixTxnResponse((*clientv3.TxnResponse)(tv.ResponseTxn))
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (kv *kvPrefix) prefixInterval(key, end []byte) (pfxKey []byte, pfxEnd []byte) {
|
||||
return prefixInterval(kv.pfx, key, end)
|
||||
}
|
||||
|
||||
func (kv *kvPrefix) prefixCmps(cs []clientv3.Cmp) []clientv3.Cmp {
|
||||
newCmps := make([]clientv3.Cmp, len(cs))
|
||||
for i := range cs {
|
||||
newCmps[i] = cs[i]
|
||||
pfxKey, endKey := kv.prefixInterval(cs[i].KeyBytes(), cs[i].RangeEnd)
|
||||
newCmps[i].WithKeyBytes(pfxKey)
|
||||
if len(cs[i].RangeEnd) != 0 {
|
||||
newCmps[i].RangeEnd = endKey
|
||||
}
|
||||
}
|
||||
return newCmps
|
||||
}
|
||||
|
||||
func (kv *kvPrefix) prefixOps(ops []clientv3.Op) []clientv3.Op {
|
||||
newOps := make([]clientv3.Op, len(ops))
|
||||
for i := range ops {
|
||||
newOps[i] = kv.prefixOp(ops[i])
|
||||
}
|
||||
return newOps
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2017 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 namespace
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
)
|
||||
|
||||
type leasePrefix struct {
|
||||
clientv3.Lease
|
||||
pfx []byte
|
||||
}
|
||||
|
||||
// NewLease wraps a Lease interface to filter for only keys with a prefix
|
||||
// and remove that prefix when fetching attached keys through TimeToLive.
|
||||
func NewLease(l clientv3.Lease, prefix string) clientv3.Lease {
|
||||
return &leasePrefix{l, []byte(prefix)}
|
||||
}
|
||||
|
||||
func (l *leasePrefix) TimeToLive(ctx context.Context, id clientv3.LeaseID, opts ...clientv3.LeaseOption) (*clientv3.LeaseTimeToLiveResponse, error) {
|
||||
resp, err := l.Lease.TimeToLive(ctx, id, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(resp.Keys) > 0 {
|
||||
var outKeys [][]byte
|
||||
for i := range resp.Keys {
|
||||
if len(resp.Keys[i]) < len(l.pfx) {
|
||||
// too short
|
||||
continue
|
||||
}
|
||||
if !bytes.Equal(resp.Keys[i][:len(l.pfx)], l.pfx) {
|
||||
// doesn't match prefix
|
||||
continue
|
||||
}
|
||||
// strip prefix
|
||||
outKeys = append(outKeys, resp.Keys[i][len(l.pfx):])
|
||||
}
|
||||
resp.Keys = outKeys
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright 2017 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 namespace
|
||||
|
||||
func prefixInterval(pfx string, key, end []byte) (pfxKey []byte, pfxEnd []byte) {
|
||||
pfxKey = make([]byte, len(pfx)+len(key))
|
||||
copy(pfxKey[copy(pfxKey, pfx):], key)
|
||||
|
||||
if len(end) == 1 && end[0] == 0 {
|
||||
// the edge of the keyspace
|
||||
pfxEnd = make([]byte, len(pfx))
|
||||
copy(pfxEnd, pfx)
|
||||
ok := false
|
||||
for i := len(pfxEnd) - 1; i >= 0; i-- {
|
||||
if pfxEnd[i]++; pfxEnd[i] != 0 {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
// 0xff..ff => 0x00
|
||||
pfxEnd = []byte{0}
|
||||
}
|
||||
} else if len(end) >= 1 {
|
||||
pfxEnd = make([]byte, len(pfx)+len(end))
|
||||
copy(pfxEnd[copy(pfxEnd, pfx):], end)
|
||||
}
|
||||
|
||||
return pfxKey, pfxEnd
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright 2017 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 namespace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
)
|
||||
|
||||
type watcherPrefix struct {
|
||||
clientv3.Watcher
|
||||
pfx string
|
||||
|
||||
wg sync.WaitGroup
|
||||
stopc chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
// NewWatcher wraps a Watcher instance so that all Watch requests
|
||||
// are prefixed with a given string and all Watch responses have
|
||||
// the prefix removed.
|
||||
func NewWatcher(w clientv3.Watcher, prefix string) clientv3.Watcher {
|
||||
return &watcherPrefix{Watcher: w, pfx: prefix, stopc: make(chan struct{})}
|
||||
}
|
||||
|
||||
func (w *watcherPrefix) Watch(ctx context.Context, key string, opts ...clientv3.OpOption) clientv3.WatchChan {
|
||||
// since OpOption is opaque, determine range for prefixing through an OpGet
|
||||
op := clientv3.OpGet(key, opts...)
|
||||
end := op.RangeBytes()
|
||||
pfxBegin, pfxEnd := prefixInterval(w.pfx, []byte(key), end)
|
||||
if pfxEnd != nil {
|
||||
opts = append(opts, clientv3.WithRange(string(pfxEnd)))
|
||||
}
|
||||
|
||||
wch := w.Watcher.Watch(ctx, string(pfxBegin), opts...)
|
||||
|
||||
// translate watch events from prefixed to unprefixed
|
||||
pfxWch := make(chan clientv3.WatchResponse)
|
||||
w.wg.Add(1)
|
||||
go func() {
|
||||
defer func() {
|
||||
close(pfxWch)
|
||||
w.wg.Done()
|
||||
}()
|
||||
for wr := range wch {
|
||||
for i := range wr.Events {
|
||||
wr.Events[i].Kv.Key = wr.Events[i].Kv.Key[len(w.pfx):]
|
||||
if wr.Events[i].PrevKv != nil {
|
||||
wr.Events[i].PrevKv.Key = wr.Events[i].Kv.Key
|
||||
}
|
||||
}
|
||||
select {
|
||||
case pfxWch <- wr:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-w.stopc:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return pfxWch
|
||||
}
|
||||
|
||||
func (w *watcherPrefix) Close() error {
|
||||
err := w.Watcher.Close()
|
||||
w.stopOnce.Do(func() { close(w.stopc) })
|
||||
w.wg.Wait()
|
||||
return err
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright 2017 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 naming provides an etcd-backed gRPC resolver for discovering gRPC services.
|
||||
//
|
||||
// To use, first import the packages:
|
||||
//
|
||||
// import (
|
||||
// "github.com/coreos/etcd/clientv3"
|
||||
// etcdnaming "github.com/coreos/etcd/clientv3/naming"
|
||||
//
|
||||
// "google.golang.org/grpc"
|
||||
// "google.golang.org/grpc/naming"
|
||||
// )
|
||||
//
|
||||
// First, register new endpoint addresses for a service:
|
||||
//
|
||||
// func etcdAdd(c *clientv3.Client, service, addr string) error {
|
||||
// r := &etcdnaming.GRPCResolver{Client: c}
|
||||
// return r.Update(c.Ctx(), service, naming.Update{Op: naming.Add, Addr: addr})
|
||||
// }
|
||||
//
|
||||
// Dial an RPC service using the etcd gRPC resolver and a gRPC Balancer:
|
||||
//
|
||||
// func etcdDial(c *clientv3.Client, service string) (*grpc.ClientConn, error) {
|
||||
// r := &etcdnaming.GRPCResolver{Client: c}
|
||||
// b := grpc.RoundRobin(r)
|
||||
// return grpc.Dial(service, grpc.WithBalancer(b))
|
||||
// }
|
||||
//
|
||||
// Optionally, force delete an endpoint:
|
||||
//
|
||||
// func etcdDelete(c *clientv3, service, addr string) error {
|
||||
// r := &etcdnaming.GRPCResolver{Client: c}
|
||||
// return r.Update(c.Ctx(), "my-service", naming.Update{Op: naming.Delete, Addr: "1.2.3.4"})
|
||||
// }
|
||||
//
|
||||
// Or register an expiring endpoint with a lease:
|
||||
//
|
||||
// func etcdLeaseAdd(c *clientv3.Client, lid clientv3.LeaseID, service, addr string) error {
|
||||
// r := &etcdnaming.GRPCResolver{Client: c}
|
||||
// return r.Update(c.Ctx(), service, naming.Update{Op: naming.Add, Addr: addr}, clientv3.WithLease(lid))
|
||||
// }
|
||||
//
|
||||
package naming
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
// Copyright 2016 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 naming
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/naming"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var ErrWatcherClosed = fmt.Errorf("naming: watch closed")
|
||||
|
||||
// GRPCResolver creates a grpc.Watcher for a target to track its resolution changes.
|
||||
type GRPCResolver struct {
|
||||
// Client is an initialized etcd client.
|
||||
Client *etcd.Client
|
||||
}
|
||||
|
||||
func (gr *GRPCResolver) Update(ctx context.Context, target string, nm naming.Update, opts ...etcd.OpOption) (err error) {
|
||||
switch nm.Op {
|
||||
case naming.Add:
|
||||
var v []byte
|
||||
if v, err = json.Marshal(nm); err != nil {
|
||||
return status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
_, err = gr.Client.KV.Put(ctx, target+"/"+nm.Addr, string(v), opts...)
|
||||
case naming.Delete:
|
||||
_, err = gr.Client.Delete(ctx, target+"/"+nm.Addr, opts...)
|
||||
default:
|
||||
return status.Error(codes.InvalidArgument, "naming: bad naming op")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (gr *GRPCResolver) Resolve(target string) (naming.Watcher, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
w := &gRPCWatcher{c: gr.Client, target: target + "/", ctx: ctx, cancel: cancel}
|
||||
return w, nil
|
||||
}
|
||||
|
||||
type gRPCWatcher struct {
|
||||
c *etcd.Client
|
||||
target string
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wch etcd.WatchChan
|
||||
err error
|
||||
}
|
||||
|
||||
// Next gets the next set of updates from the etcd resolver.
|
||||
// Calls to Next should be serialized; concurrent calls are not safe since
|
||||
// there is no way to reconcile the update ordering.
|
||||
func (gw *gRPCWatcher) Next() ([]*naming.Update, error) {
|
||||
if gw.wch == nil {
|
||||
// first Next() returns all addresses
|
||||
return gw.firstNext()
|
||||
}
|
||||
if gw.err != nil {
|
||||
return nil, gw.err
|
||||
}
|
||||
|
||||
// process new events on target/*
|
||||
wr, ok := <-gw.wch
|
||||
if !ok {
|
||||
gw.err = status.Error(codes.Unavailable, ErrWatcherClosed.Error())
|
||||
return nil, gw.err
|
||||
}
|
||||
if gw.err = wr.Err(); gw.err != nil {
|
||||
return nil, gw.err
|
||||
}
|
||||
|
||||
updates := make([]*naming.Update, 0, len(wr.Events))
|
||||
for _, e := range wr.Events {
|
||||
var jupdate naming.Update
|
||||
var err error
|
||||
switch e.Type {
|
||||
case etcd.EventTypePut:
|
||||
err = json.Unmarshal(e.Kv.Value, &jupdate)
|
||||
jupdate.Op = naming.Add
|
||||
case etcd.EventTypeDelete:
|
||||
err = json.Unmarshal(e.PrevKv.Value, &jupdate)
|
||||
jupdate.Op = naming.Delete
|
||||
}
|
||||
if err == nil {
|
||||
updates = append(updates, &jupdate)
|
||||
}
|
||||
}
|
||||
return updates, nil
|
||||
}
|
||||
|
||||
func (gw *gRPCWatcher) firstNext() ([]*naming.Update, error) {
|
||||
// Use serialized request so resolution still works if the target etcd
|
||||
// server is partitioned away from the quorum.
|
||||
resp, err := gw.c.Get(gw.ctx, gw.target, etcd.WithPrefix(), etcd.WithSerializable())
|
||||
if gw.err = err; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updates := make([]*naming.Update, 0, len(resp.Kvs))
|
||||
for _, kv := range resp.Kvs {
|
||||
var jupdate naming.Update
|
||||
if err := json.Unmarshal(kv.Value, &jupdate); err != nil {
|
||||
continue
|
||||
}
|
||||
updates = append(updates, &jupdate)
|
||||
}
|
||||
|
||||
opts := []etcd.OpOption{etcd.WithRev(resp.Header.Revision + 1), etcd.WithPrefix(), etcd.WithPrevKV()}
|
||||
gw.wch = gw.c.Watch(gw.ctx, gw.target, opts...)
|
||||
return updates, nil
|
||||
}
|
||||
|
||||
func (gw *gRPCWatcher) Close() { gw.cancel() }
|
||||
|
|
@ -16,17 +16,17 @@ package clientv3
|
|||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var (
|
||||
// Disable gRPC internal retrial logic
|
||||
// TODO: enable when gRPC retry is stable (FailFast=false)
|
||||
// Reference:
|
||||
// - https://github.com/grpc/grpc-go/issues/1532
|
||||
// - https://github.com/grpc/proposal/blob/master/A6-client-retries.md
|
||||
defaultFailFast = grpc.FailFast(true)
|
||||
// client-side handling retrying of request failures where data was not written to the wire or
|
||||
// where server indicates it did not process the data. gRPC default is default is "FailFast(true)"
|
||||
// but for etcd we default to "FailFast(false)" to minimize client request error responses due to
|
||||
// transient failures.
|
||||
defaultFailFast = grpc.FailFast(false)
|
||||
|
||||
// client-side request send limit, gRPC default is math.MaxInt32
|
||||
// Make sure that "client-side send limit < server-side default send/recv limit"
|
||||
|
|
@ -38,6 +38,22 @@ var (
|
|||
// because range response can easily exceed request send limits
|
||||
// Default to math.MaxInt32; writes exceeding server-side send limit fails anyway
|
||||
defaultMaxCallRecvMsgSize = grpc.MaxCallRecvMsgSize(math.MaxInt32)
|
||||
|
||||
// client-side non-streaming retry limit, only applied to requests where server responds with
|
||||
// a error code clearly indicating it was unable to process the request such as codes.Unavailable.
|
||||
// If set to 0, retry is disabled.
|
||||
defaultUnaryMaxRetries uint = 100
|
||||
|
||||
// client-side streaming retry limit, only applied to requests where server responds with
|
||||
// a error code clearly indicating it was unable to process the request such as codes.Unavailable.
|
||||
// If set to 0, retry is disabled.
|
||||
defaultStreamMaxRetries uint = ^uint(0) // max uint
|
||||
|
||||
// client-side retry backoff wait between requests.
|
||||
defaultBackoffWaitBetween = 25 * time.Millisecond
|
||||
|
||||
// client-side retry backoff default jitter fraction.
|
||||
defaultBackoffJitterFraction = 0.10
|
||||
)
|
||||
|
||||
// defaultCallOpts defines a list of default "gRPC.CallOption".
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright 2017 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 ordering is a clientv3 wrapper that caches response header revisions
|
||||
// to detect ordering violations from stale responses. Users may define a
|
||||
// policy on how to handle the ordering violation, but typically the client
|
||||
// should connect to another endpoint and reissue the request.
|
||||
//
|
||||
// The most common situation where an ordering violation happens is a client
|
||||
// reconnects to a partitioned member and issues a serializable read. Since the
|
||||
// partitioned member is likely behind the last member, it may return a Get
|
||||
// response based on a store revision older than the store revision used to
|
||||
// service a prior Get on the former endpoint.
|
||||
//
|
||||
// First, create a client:
|
||||
//
|
||||
// cli, err := clientv3.New(clientv3.Config{Endpoints: []string{"localhost:2379"}})
|
||||
// if err != nil {
|
||||
// // handle error!
|
||||
// }
|
||||
//
|
||||
// Next, override the client interface with the ordering wrapper:
|
||||
//
|
||||
// vf := func(op clientv3.Op, resp clientv3.OpResponse, prevRev int64) error {
|
||||
// return fmt.Errorf("ordering: issued %+v, got %+v, expected rev=%v", op, resp, prevRev)
|
||||
// }
|
||||
// cli.KV = ordering.NewKV(cli.KV, vf)
|
||||
//
|
||||
// Now calls using 'cli' will reject order violations with an error.
|
||||
//
|
||||
package ordering
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
// Copyright 2017 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 ordering
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
)
|
||||
|
||||
// kvOrdering ensures that serialized requests do not return
|
||||
// get with revisions less than the previous
|
||||
// returned revision.
|
||||
type kvOrdering struct {
|
||||
clientv3.KV
|
||||
orderViolationFunc OrderViolationFunc
|
||||
prevRev int64
|
||||
revMu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewKV(kv clientv3.KV, orderViolationFunc OrderViolationFunc) *kvOrdering {
|
||||
return &kvOrdering{kv, orderViolationFunc, 0, sync.RWMutex{}}
|
||||
}
|
||||
|
||||
func (kv *kvOrdering) getPrevRev() int64 {
|
||||
kv.revMu.RLock()
|
||||
defer kv.revMu.RUnlock()
|
||||
return kv.prevRev
|
||||
}
|
||||
|
||||
func (kv *kvOrdering) setPrevRev(currRev int64) {
|
||||
kv.revMu.Lock()
|
||||
defer kv.revMu.Unlock()
|
||||
if currRev > kv.prevRev {
|
||||
kv.prevRev = currRev
|
||||
}
|
||||
}
|
||||
|
||||
func (kv *kvOrdering) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) {
|
||||
// prevRev is stored in a local variable in order to record the prevRev
|
||||
// at the beginning of the Get operation, because concurrent
|
||||
// access to kvOrdering could change the prevRev field in the
|
||||
// middle of the Get operation.
|
||||
prevRev := kv.getPrevRev()
|
||||
op := clientv3.OpGet(key, opts...)
|
||||
for {
|
||||
r, err := kv.KV.Do(ctx, op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := r.Get()
|
||||
if resp.Header.Revision == prevRev {
|
||||
return resp, nil
|
||||
} else if resp.Header.Revision > prevRev {
|
||||
kv.setPrevRev(resp.Header.Revision)
|
||||
return resp, nil
|
||||
}
|
||||
err = kv.orderViolationFunc(op, r, prevRev)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (kv *kvOrdering) Txn(ctx context.Context) clientv3.Txn {
|
||||
return &txnOrdering{
|
||||
kv.KV.Txn(ctx),
|
||||
kv,
|
||||
ctx,
|
||||
sync.Mutex{},
|
||||
[]clientv3.Cmp{},
|
||||
[]clientv3.Op{},
|
||||
[]clientv3.Op{},
|
||||
}
|
||||
}
|
||||
|
||||
// txnOrdering ensures that serialized requests do not return
|
||||
// txn responses with revisions less than the previous
|
||||
// returned revision.
|
||||
type txnOrdering struct {
|
||||
clientv3.Txn
|
||||
*kvOrdering
|
||||
ctx context.Context
|
||||
mu sync.Mutex
|
||||
cmps []clientv3.Cmp
|
||||
thenOps []clientv3.Op
|
||||
elseOps []clientv3.Op
|
||||
}
|
||||
|
||||
func (txn *txnOrdering) If(cs ...clientv3.Cmp) clientv3.Txn {
|
||||
txn.mu.Lock()
|
||||
defer txn.mu.Unlock()
|
||||
txn.cmps = cs
|
||||
txn.Txn.If(cs...)
|
||||
return txn
|
||||
}
|
||||
|
||||
func (txn *txnOrdering) Then(ops ...clientv3.Op) clientv3.Txn {
|
||||
txn.mu.Lock()
|
||||
defer txn.mu.Unlock()
|
||||
txn.thenOps = ops
|
||||
txn.Txn.Then(ops...)
|
||||
return txn
|
||||
}
|
||||
|
||||
func (txn *txnOrdering) Else(ops ...clientv3.Op) clientv3.Txn {
|
||||
txn.mu.Lock()
|
||||
defer txn.mu.Unlock()
|
||||
txn.elseOps = ops
|
||||
txn.Txn.Else(ops...)
|
||||
return txn
|
||||
}
|
||||
|
||||
func (txn *txnOrdering) Commit() (*clientv3.TxnResponse, error) {
|
||||
// prevRev is stored in a local variable in order to record the prevRev
|
||||
// at the beginning of the Commit operation, because concurrent
|
||||
// access to txnOrdering could change the prevRev field in the
|
||||
// middle of the Commit operation.
|
||||
prevRev := txn.getPrevRev()
|
||||
opTxn := clientv3.OpTxn(txn.cmps, txn.thenOps, txn.elseOps)
|
||||
for {
|
||||
opResp, err := txn.KV.Do(txn.ctx, opTxn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
txnResp := opResp.Txn()
|
||||
if txnResp.Header.Revision >= prevRev {
|
||||
txn.setPrevRev(txnResp.Header.Revision)
|
||||
return txnResp, nil
|
||||
}
|
||||
err = txn.orderViolationFunc(opTxn, opResp, prevRev)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright 2017 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 ordering
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
)
|
||||
|
||||
type OrderViolationFunc func(op clientv3.Op, resp clientv3.OpResponse, prevRev int64) error
|
||||
|
||||
var ErrNoGreaterRev = errors.New("etcdclient: no cluster members have a revision higher than the previously received revision")
|
||||
|
||||
func NewOrderViolationSwitchEndpointClosure(c clientv3.Client) OrderViolationFunc {
|
||||
var mu sync.Mutex
|
||||
violationCount := 0
|
||||
return func(op clientv3.Op, resp clientv3.OpResponse, prevRev int64) error {
|
||||
if violationCount > len(c.Endpoints()) {
|
||||
return ErrNoGreaterRev
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
eps := c.Endpoints()
|
||||
// force client to connect to given endpoint by limiting to a single endpoint
|
||||
c.SetEndpoints(eps[violationCount%len(eps)])
|
||||
// give enough time for operation
|
||||
time.Sleep(1 * time.Second)
|
||||
// set available endpoints back to all endpoints in to ensure
|
||||
// the client has access to all the endpoints.
|
||||
c.SetEndpoints(eps...)
|
||||
// give enough time for operation
|
||||
time.Sleep(1 * time.Second)
|
||||
violationCount++
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -32,467 +32,267 @@ const (
|
|||
nonRepeatable
|
||||
)
|
||||
|
||||
func (rp retryPolicy) String() string {
|
||||
switch rp {
|
||||
case repeatable:
|
||||
return "repeatable"
|
||||
case nonRepeatable:
|
||||
return "nonRepeatable"
|
||||
default:
|
||||
return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
|
||||
type rpcFunc func(ctx context.Context) error
|
||||
type retryRPCFunc func(context.Context, rpcFunc, retryPolicy) error
|
||||
type retryStopErrFunc func(error) bool
|
||||
|
||||
// isSafeRetryImmutableRPC returns "true" when an immutable request is safe for retry.
|
||||
//
|
||||
// immutable requests (e.g. Get) should be retried unless it's
|
||||
// an obvious server-side error (e.g. rpctypes.ErrRequestTooLarge).
|
||||
//
|
||||
// "isRepeatableStopError" returns "true" when an immutable request
|
||||
// is interrupted by server-side or gRPC-side error and its status
|
||||
// code is not transient (!= codes.Unavailable).
|
||||
//
|
||||
// Returning "true" means retry should stop, since client cannot
|
||||
// Returning "false" means retry should stop, since client cannot
|
||||
// handle itself even with retries.
|
||||
func isRepeatableStopError(err error) bool {
|
||||
func isSafeRetryImmutableRPC(err error) bool {
|
||||
eErr := rpctypes.Error(err)
|
||||
// always stop retry on etcd errors
|
||||
if serverErr, ok := eErr.(rpctypes.EtcdError); ok && serverErr.Code() != codes.Unavailable {
|
||||
return true
|
||||
// interrupted by non-transient server-side or gRPC-side error
|
||||
// client cannot handle itself (e.g. rpctypes.ErrCompacted)
|
||||
return false
|
||||
}
|
||||
// only retry if unavailable
|
||||
ev, ok := status.FromError(err)
|
||||
if !ok {
|
||||
// all errors from RPC is typed "grpc/status.(*statusError)"
|
||||
// (ref. https://github.com/grpc/grpc-go/pull/1782)
|
||||
//
|
||||
// if the error type is not "grpc/status.(*statusError)",
|
||||
// it could be from "Dial"
|
||||
// TODO: do not retry for now
|
||||
// ref. https://github.com/grpc/grpc-go/issues/1581
|
||||
return false
|
||||
}
|
||||
return ev.Code() != codes.Unavailable
|
||||
return ev.Code() == codes.Unavailable
|
||||
}
|
||||
|
||||
// isSafeRetryMutableRPC returns "true" when a mutable request is safe for retry.
|
||||
//
|
||||
// mutable requests (e.g. Put, Delete, Txn) should only be retried
|
||||
// when the status code is codes.Unavailable when initial connection
|
||||
// has not been established (no pinned endpoint).
|
||||
// has not been established (no endpoint is up).
|
||||
//
|
||||
// "isNonRepeatableStopError" returns "true" when a mutable request
|
||||
// is interrupted by non-transient error that client cannot handle itself,
|
||||
// or transient error while the connection has already been established
|
||||
// (pinned endpoint exists).
|
||||
//
|
||||
// Returning "true" means retry should stop, otherwise it violates
|
||||
// Returning "false" means retry should stop, otherwise it violates
|
||||
// write-at-most-once semantics.
|
||||
func isNonRepeatableStopError(err error) bool {
|
||||
func isSafeRetryMutableRPC(err error) bool {
|
||||
if ev, ok := status.FromError(err); ok && ev.Code() != codes.Unavailable {
|
||||
return true
|
||||
// not safe for mutable RPCs
|
||||
// e.g. interrupted by non-transient error that client cannot handle itself,
|
||||
// or transient error while the connection has already been established
|
||||
return false
|
||||
}
|
||||
desc := rpctypes.ErrorDesc(err)
|
||||
return desc != "there is no address available" && desc != "there is no connection available"
|
||||
}
|
||||
|
||||
func (c *Client) newRetryWrapper() retryRPCFunc {
|
||||
return func(rpcCtx context.Context, f rpcFunc, rp retryPolicy) error {
|
||||
var isStop retryStopErrFunc
|
||||
switch rp {
|
||||
case repeatable:
|
||||
isStop = isRepeatableStopError
|
||||
case nonRepeatable:
|
||||
isStop = isNonRepeatableStopError
|
||||
}
|
||||
for {
|
||||
if err := readyWait(rpcCtx, c.ctx, c.balancer.ConnectNotify()); err != nil {
|
||||
return err
|
||||
}
|
||||
pinned := c.balancer.Pinned()
|
||||
err := f(rpcCtx)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
lg.Lvl(4).Infof("clientv3/retry: error %q on pinned endpoint %q", err.Error(), pinned)
|
||||
|
||||
if s, ok := status.FromError(err); ok && (s.Code() == codes.Unavailable || s.Code() == codes.DeadlineExceeded || s.Code() == codes.Internal) {
|
||||
// mark this before endpoint switch is triggered
|
||||
c.balancer.HostPortError(pinned, err)
|
||||
c.balancer.Next()
|
||||
lg.Lvl(4).Infof("clientv3/retry: switching from %q due to error %q", pinned, err.Error())
|
||||
}
|
||||
|
||||
if isStop(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) newAuthRetryWrapper(retryf retryRPCFunc) retryRPCFunc {
|
||||
return func(rpcCtx context.Context, f rpcFunc, rp retryPolicy) error {
|
||||
for {
|
||||
pinned := c.balancer.Pinned()
|
||||
err := retryf(rpcCtx, f, rp)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
lg.Lvl(4).Infof("clientv3/auth-retry: error %q on pinned endpoint %q", err.Error(), pinned)
|
||||
// always stop retry on etcd errors other than invalid auth token
|
||||
if rpctypes.Error(err) == rpctypes.ErrInvalidAuthToken {
|
||||
gterr := c.getToken(rpcCtx)
|
||||
if gterr != nil {
|
||||
lg.Lvl(4).Infof("clientv3/auth-retry: cannot retry due to error %q(%q) on pinned endpoint %q", err.Error(), gterr.Error(), pinned)
|
||||
return err // return the original error for simplicity
|
||||
}
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return desc == "there is no address available" || desc == "there is no connection available"
|
||||
}
|
||||
|
||||
type retryKVClient struct {
|
||||
kc pb.KVClient
|
||||
retryf retryRPCFunc
|
||||
kc pb.KVClient
|
||||
}
|
||||
|
||||
// RetryKVClient implements a KVClient.
|
||||
func RetryKVClient(c *Client) pb.KVClient {
|
||||
return &retryKVClient{
|
||||
kc: pb.NewKVClient(c.conn),
|
||||
retryf: c.newAuthRetryWrapper(c.newRetryWrapper()),
|
||||
kc: pb.NewKVClient(c.conn),
|
||||
}
|
||||
}
|
||||
func (rkv *retryKVClient) Range(ctx context.Context, in *pb.RangeRequest, opts ...grpc.CallOption) (resp *pb.RangeResponse, err error) {
|
||||
err = rkv.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rkv.kc.Range(rctx, in, opts...)
|
||||
return err
|
||||
}, repeatable)
|
||||
return resp, err
|
||||
return rkv.kc.Range(ctx, in, append(opts, withRetryPolicy(repeatable))...)
|
||||
}
|
||||
|
||||
func (rkv *retryKVClient) Put(ctx context.Context, in *pb.PutRequest, opts ...grpc.CallOption) (resp *pb.PutResponse, err error) {
|
||||
err = rkv.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rkv.kc.Put(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rkv.kc.Put(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (rkv *retryKVClient) DeleteRange(ctx context.Context, in *pb.DeleteRangeRequest, opts ...grpc.CallOption) (resp *pb.DeleteRangeResponse, err error) {
|
||||
err = rkv.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rkv.kc.DeleteRange(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rkv.kc.DeleteRange(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (rkv *retryKVClient) Txn(ctx context.Context, in *pb.TxnRequest, opts ...grpc.CallOption) (resp *pb.TxnResponse, err error) {
|
||||
// TODO: "repeatable" for read-only txn
|
||||
err = rkv.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rkv.kc.Txn(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rkv.kc.Txn(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (rkv *retryKVClient) Compact(ctx context.Context, in *pb.CompactionRequest, opts ...grpc.CallOption) (resp *pb.CompactionResponse, err error) {
|
||||
err = rkv.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rkv.kc.Compact(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rkv.kc.Compact(ctx, in, opts...)
|
||||
}
|
||||
|
||||
type retryLeaseClient struct {
|
||||
lc pb.LeaseClient
|
||||
retryf retryRPCFunc
|
||||
lc pb.LeaseClient
|
||||
}
|
||||
|
||||
// RetryLeaseClient implements a LeaseClient.
|
||||
func RetryLeaseClient(c *Client) pb.LeaseClient {
|
||||
return &retryLeaseClient{
|
||||
lc: pb.NewLeaseClient(c.conn),
|
||||
retryf: c.newAuthRetryWrapper(c.newRetryWrapper()),
|
||||
lc: pb.NewLeaseClient(c.conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (rlc *retryLeaseClient) LeaseTimeToLive(ctx context.Context, in *pb.LeaseTimeToLiveRequest, opts ...grpc.CallOption) (resp *pb.LeaseTimeToLiveResponse, err error) {
|
||||
err = rlc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rlc.lc.LeaseTimeToLive(rctx, in, opts...)
|
||||
return err
|
||||
}, repeatable)
|
||||
return resp, err
|
||||
return rlc.lc.LeaseTimeToLive(ctx, in, append(opts, withRetryPolicy(repeatable))...)
|
||||
}
|
||||
|
||||
func (rlc *retryLeaseClient) LeaseLeases(ctx context.Context, in *pb.LeaseLeasesRequest, opts ...grpc.CallOption) (resp *pb.LeaseLeasesResponse, err error) {
|
||||
err = rlc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rlc.lc.LeaseLeases(rctx, in, opts...)
|
||||
return err
|
||||
}, repeatable)
|
||||
return resp, err
|
||||
return rlc.lc.LeaseLeases(ctx, in, append(opts, withRetryPolicy(repeatable))...)
|
||||
}
|
||||
|
||||
func (rlc *retryLeaseClient) LeaseGrant(ctx context.Context, in *pb.LeaseGrantRequest, opts ...grpc.CallOption) (resp *pb.LeaseGrantResponse, err error) {
|
||||
err = rlc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rlc.lc.LeaseGrant(rctx, in, opts...)
|
||||
return err
|
||||
}, repeatable)
|
||||
return resp, err
|
||||
|
||||
return rlc.lc.LeaseGrant(ctx, in, append(opts, withRetryPolicy(repeatable))...)
|
||||
}
|
||||
|
||||
func (rlc *retryLeaseClient) LeaseRevoke(ctx context.Context, in *pb.LeaseRevokeRequest, opts ...grpc.CallOption) (resp *pb.LeaseRevokeResponse, err error) {
|
||||
err = rlc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rlc.lc.LeaseRevoke(rctx, in, opts...)
|
||||
return err
|
||||
}, repeatable)
|
||||
return resp, err
|
||||
return rlc.lc.LeaseRevoke(ctx, in, append(opts, withRetryPolicy(repeatable))...)
|
||||
}
|
||||
|
||||
func (rlc *retryLeaseClient) LeaseKeepAlive(ctx context.Context, opts ...grpc.CallOption) (stream pb.Lease_LeaseKeepAliveClient, err error) {
|
||||
err = rlc.retryf(ctx, func(rctx context.Context) error {
|
||||
stream, err = rlc.lc.LeaseKeepAlive(rctx, opts...)
|
||||
return err
|
||||
}, repeatable)
|
||||
return stream, err
|
||||
return rlc.lc.LeaseKeepAlive(ctx, append(opts, withRetryPolicy(repeatable))...)
|
||||
}
|
||||
|
||||
type retryClusterClient struct {
|
||||
cc pb.ClusterClient
|
||||
retryf retryRPCFunc
|
||||
cc pb.ClusterClient
|
||||
}
|
||||
|
||||
// RetryClusterClient implements a ClusterClient.
|
||||
func RetryClusterClient(c *Client) pb.ClusterClient {
|
||||
return &retryClusterClient{
|
||||
cc: pb.NewClusterClient(c.conn),
|
||||
retryf: c.newRetryWrapper(),
|
||||
cc: pb.NewClusterClient(c.conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (rcc *retryClusterClient) MemberList(ctx context.Context, in *pb.MemberListRequest, opts ...grpc.CallOption) (resp *pb.MemberListResponse, err error) {
|
||||
err = rcc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rcc.cc.MemberList(rctx, in, opts...)
|
||||
return err
|
||||
}, repeatable)
|
||||
return resp, err
|
||||
return rcc.cc.MemberList(ctx, in, append(opts, withRetryPolicy(repeatable))...)
|
||||
}
|
||||
|
||||
func (rcc *retryClusterClient) MemberAdd(ctx context.Context, in *pb.MemberAddRequest, opts ...grpc.CallOption) (resp *pb.MemberAddResponse, err error) {
|
||||
err = rcc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rcc.cc.MemberAdd(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rcc.cc.MemberAdd(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (rcc *retryClusterClient) MemberRemove(ctx context.Context, in *pb.MemberRemoveRequest, opts ...grpc.CallOption) (resp *pb.MemberRemoveResponse, err error) {
|
||||
err = rcc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rcc.cc.MemberRemove(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rcc.cc.MemberRemove(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (rcc *retryClusterClient) MemberUpdate(ctx context.Context, in *pb.MemberUpdateRequest, opts ...grpc.CallOption) (resp *pb.MemberUpdateResponse, err error) {
|
||||
err = rcc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rcc.cc.MemberUpdate(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rcc.cc.MemberUpdate(ctx, in, opts...)
|
||||
}
|
||||
|
||||
type retryMaintenanceClient struct {
|
||||
mc pb.MaintenanceClient
|
||||
retryf retryRPCFunc
|
||||
mc pb.MaintenanceClient
|
||||
}
|
||||
|
||||
// RetryMaintenanceClient implements a Maintenance.
|
||||
func RetryMaintenanceClient(c *Client, conn *grpc.ClientConn) pb.MaintenanceClient {
|
||||
return &retryMaintenanceClient{
|
||||
mc: pb.NewMaintenanceClient(conn),
|
||||
retryf: c.newRetryWrapper(),
|
||||
mc: pb.NewMaintenanceClient(conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (rmc *retryMaintenanceClient) Alarm(ctx context.Context, in *pb.AlarmRequest, opts ...grpc.CallOption) (resp *pb.AlarmResponse, err error) {
|
||||
err = rmc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rmc.mc.Alarm(rctx, in, opts...)
|
||||
return err
|
||||
}, repeatable)
|
||||
return resp, err
|
||||
return rmc.mc.Alarm(ctx, in, append(opts, withRetryPolicy(repeatable))...)
|
||||
}
|
||||
|
||||
func (rmc *retryMaintenanceClient) Status(ctx context.Context, in *pb.StatusRequest, opts ...grpc.CallOption) (resp *pb.StatusResponse, err error) {
|
||||
err = rmc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rmc.mc.Status(rctx, in, opts...)
|
||||
return err
|
||||
}, repeatable)
|
||||
return resp, err
|
||||
return rmc.mc.Status(ctx, in, append(opts, withRetryPolicy(repeatable))...)
|
||||
}
|
||||
|
||||
func (rmc *retryMaintenanceClient) Hash(ctx context.Context, in *pb.HashRequest, opts ...grpc.CallOption) (resp *pb.HashResponse, err error) {
|
||||
err = rmc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rmc.mc.Hash(rctx, in, opts...)
|
||||
return err
|
||||
}, repeatable)
|
||||
return resp, err
|
||||
return rmc.mc.Hash(ctx, in, append(opts, withRetryPolicy(repeatable))...)
|
||||
}
|
||||
|
||||
func (rmc *retryMaintenanceClient) HashKV(ctx context.Context, in *pb.HashKVRequest, opts ...grpc.CallOption) (resp *pb.HashKVResponse, err error) {
|
||||
err = rmc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rmc.mc.HashKV(rctx, in, opts...)
|
||||
return err
|
||||
}, repeatable)
|
||||
return resp, err
|
||||
return rmc.mc.HashKV(ctx, in, append(opts, withRetryPolicy(repeatable))...)
|
||||
}
|
||||
|
||||
func (rmc *retryMaintenanceClient) Snapshot(ctx context.Context, in *pb.SnapshotRequest, opts ...grpc.CallOption) (stream pb.Maintenance_SnapshotClient, err error) {
|
||||
err = rmc.retryf(ctx, func(rctx context.Context) error {
|
||||
stream, err = rmc.mc.Snapshot(rctx, in, opts...)
|
||||
return err
|
||||
}, repeatable)
|
||||
return stream, err
|
||||
return rmc.mc.Snapshot(ctx, in, append(opts, withRetryPolicy(repeatable))...)
|
||||
}
|
||||
|
||||
func (rmc *retryMaintenanceClient) MoveLeader(ctx context.Context, in *pb.MoveLeaderRequest, opts ...grpc.CallOption) (resp *pb.MoveLeaderResponse, err error) {
|
||||
err = rmc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rmc.mc.MoveLeader(rctx, in, opts...)
|
||||
return err
|
||||
}, repeatable)
|
||||
return resp, err
|
||||
return rmc.mc.MoveLeader(ctx, in, append(opts, withRetryPolicy(repeatable))...)
|
||||
}
|
||||
|
||||
func (rmc *retryMaintenanceClient) Defragment(ctx context.Context, in *pb.DefragmentRequest, opts ...grpc.CallOption) (resp *pb.DefragmentResponse, err error) {
|
||||
err = rmc.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rmc.mc.Defragment(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rmc.mc.Defragment(ctx, in, opts...)
|
||||
}
|
||||
|
||||
type retryAuthClient struct {
|
||||
ac pb.AuthClient
|
||||
retryf retryRPCFunc
|
||||
ac pb.AuthClient
|
||||
}
|
||||
|
||||
// RetryAuthClient implements a AuthClient.
|
||||
func RetryAuthClient(c *Client) pb.AuthClient {
|
||||
return &retryAuthClient{
|
||||
ac: pb.NewAuthClient(c.conn),
|
||||
retryf: c.newRetryWrapper(),
|
||||
ac: pb.NewAuthClient(c.conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) UserList(ctx context.Context, in *pb.AuthUserListRequest, opts ...grpc.CallOption) (resp *pb.AuthUserListResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.ac.UserList(rctx, in, opts...)
|
||||
return err
|
||||
}, repeatable)
|
||||
return resp, err
|
||||
return rac.ac.UserList(ctx, in, append(opts, withRetryPolicy(repeatable))...)
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) UserGet(ctx context.Context, in *pb.AuthUserGetRequest, opts ...grpc.CallOption) (resp *pb.AuthUserGetResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.ac.UserGet(rctx, in, opts...)
|
||||
return err
|
||||
}, repeatable)
|
||||
return resp, err
|
||||
return rac.ac.UserGet(ctx, in, append(opts, withRetryPolicy(repeatable))...)
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) RoleGet(ctx context.Context, in *pb.AuthRoleGetRequest, opts ...grpc.CallOption) (resp *pb.AuthRoleGetResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.ac.RoleGet(rctx, in, opts...)
|
||||
return err
|
||||
}, repeatable)
|
||||
return resp, err
|
||||
return rac.ac.RoleGet(ctx, in, append(opts, withRetryPolicy(repeatable))...)
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) RoleList(ctx context.Context, in *pb.AuthRoleListRequest, opts ...grpc.CallOption) (resp *pb.AuthRoleListResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.ac.RoleList(rctx, in, opts...)
|
||||
return err
|
||||
}, repeatable)
|
||||
return resp, err
|
||||
return rac.ac.RoleList(ctx, in, append(opts, withRetryPolicy(repeatable))...)
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) AuthEnable(ctx context.Context, in *pb.AuthEnableRequest, opts ...grpc.CallOption) (resp *pb.AuthEnableResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.ac.AuthEnable(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rac.ac.AuthEnable(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) AuthDisable(ctx context.Context, in *pb.AuthDisableRequest, opts ...grpc.CallOption) (resp *pb.AuthDisableResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.ac.AuthDisable(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rac.ac.AuthDisable(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) UserAdd(ctx context.Context, in *pb.AuthUserAddRequest, opts ...grpc.CallOption) (resp *pb.AuthUserAddResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.ac.UserAdd(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rac.ac.UserAdd(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) UserDelete(ctx context.Context, in *pb.AuthUserDeleteRequest, opts ...grpc.CallOption) (resp *pb.AuthUserDeleteResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.ac.UserDelete(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rac.ac.UserDelete(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) UserChangePassword(ctx context.Context, in *pb.AuthUserChangePasswordRequest, opts ...grpc.CallOption) (resp *pb.AuthUserChangePasswordResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.ac.UserChangePassword(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rac.ac.UserChangePassword(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) UserGrantRole(ctx context.Context, in *pb.AuthUserGrantRoleRequest, opts ...grpc.CallOption) (resp *pb.AuthUserGrantRoleResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.ac.UserGrantRole(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rac.ac.UserGrantRole(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) UserRevokeRole(ctx context.Context, in *pb.AuthUserRevokeRoleRequest, opts ...grpc.CallOption) (resp *pb.AuthUserRevokeRoleResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.ac.UserRevokeRole(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rac.ac.UserRevokeRole(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) RoleAdd(ctx context.Context, in *pb.AuthRoleAddRequest, opts ...grpc.CallOption) (resp *pb.AuthRoleAddResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.ac.RoleAdd(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rac.ac.RoleAdd(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) RoleDelete(ctx context.Context, in *pb.AuthRoleDeleteRequest, opts ...grpc.CallOption) (resp *pb.AuthRoleDeleteResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.ac.RoleDelete(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rac.ac.RoleDelete(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) RoleGrantPermission(ctx context.Context, in *pb.AuthRoleGrantPermissionRequest, opts ...grpc.CallOption) (resp *pb.AuthRoleGrantPermissionResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.ac.RoleGrantPermission(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rac.ac.RoleGrantPermission(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) RoleRevokePermission(ctx context.Context, in *pb.AuthRoleRevokePermissionRequest, opts ...grpc.CallOption) (resp *pb.AuthRoleRevokePermissionResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.ac.RoleRevokePermission(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rac.ac.RoleRevokePermission(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (rac *retryAuthClient) Authenticate(ctx context.Context, in *pb.AuthenticateRequest, opts ...grpc.CallOption) (resp *pb.AuthenticateResponse, err error) {
|
||||
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||
resp, err = rac.ac.Authenticate(rctx, in, opts...)
|
||||
return err
|
||||
}, nonRepeatable)
|
||||
return resp, err
|
||||
return rac.ac.Authenticate(ctx, in, opts...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,382 @@
|
|||
// Copyright 2016 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.
|
||||
|
||||
// Based on github.com/grpc-ecosystem/go-grpc-middleware/retry, but modified to support the more
|
||||
// fine grained error checking required by write-at-most-once retry semantics of etcd.
|
||||
|
||||
package clientv3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/util/backoffutils"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// unaryClientInterceptor returns a new retrying unary client interceptor.
|
||||
//
|
||||
// The default configuration of the interceptor is to not retry *at all*. This behaviour can be
|
||||
// changed through options (e.g. WithMax) on creation of the interceptor or on call (through grpc.CallOptions).
|
||||
func (c *Client) unaryClientInterceptor(logger *zap.Logger, optFuncs ...retryOption) grpc.UnaryClientInterceptor {
|
||||
intOpts := reuseOrNewWithCallOptions(defaultOptions, optFuncs)
|
||||
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
grpcOpts, retryOpts := filterCallOptions(opts)
|
||||
callOpts := reuseOrNewWithCallOptions(intOpts, retryOpts)
|
||||
// short circuit for simplicity, and avoiding allocations.
|
||||
if callOpts.max == 0 {
|
||||
return invoker(ctx, method, req, reply, cc, grpcOpts...)
|
||||
}
|
||||
var lastErr error
|
||||
for attempt := uint(0); attempt < callOpts.max; attempt++ {
|
||||
if err := waitRetryBackoff(attempt, ctx, callOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
lastErr = invoker(ctx, method, req, reply, cc, grpcOpts...)
|
||||
logger.Info("retry unary intercept", zap.Uint("attempt", attempt), zap.Error(lastErr))
|
||||
if lastErr == nil {
|
||||
return nil
|
||||
}
|
||||
if isContextError(lastErr) {
|
||||
if ctx.Err() != nil {
|
||||
// its the context deadline or cancellation.
|
||||
return lastErr
|
||||
}
|
||||
// its the callCtx deadline or cancellation, in which case try again.
|
||||
continue
|
||||
}
|
||||
if callOpts.retryAuth && rpctypes.Error(lastErr) == rpctypes.ErrInvalidAuthToken {
|
||||
gterr := c.getToken(ctx)
|
||||
if gterr != nil {
|
||||
logger.Info("retry failed to fetch new auth token", zap.Error(gterr))
|
||||
return lastErr // return the original error for simplicity
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !isSafeRetry(c.lg, lastErr, callOpts) {
|
||||
return lastErr
|
||||
}
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
}
|
||||
|
||||
// streamClientInterceptor returns a new retrying stream client interceptor for server side streaming calls.
|
||||
//
|
||||
// The default configuration of the interceptor is to not retry *at all*. This behaviour can be
|
||||
// changed through options (e.g. WithMax) on creation of the interceptor or on call (through grpc.CallOptions).
|
||||
//
|
||||
// Retry logic is available *only for ServerStreams*, i.e. 1:n streams, as the internal logic needs
|
||||
// to buffer the messages sent by the client. If retry is enabled on any other streams (ClientStreams,
|
||||
// BidiStreams), the retry interceptor will fail the call.
|
||||
func (c *Client) streamClientInterceptor(logger *zap.Logger, optFuncs ...retryOption) grpc.StreamClientInterceptor {
|
||||
intOpts := reuseOrNewWithCallOptions(defaultOptions, optFuncs)
|
||||
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
grpcOpts, retryOpts := filterCallOptions(opts)
|
||||
callOpts := reuseOrNewWithCallOptions(intOpts, retryOpts)
|
||||
// short circuit for simplicity, and avoiding allocations.
|
||||
if callOpts.max == 0 {
|
||||
return streamer(ctx, desc, cc, method, grpcOpts...)
|
||||
}
|
||||
if desc.ClientStreams {
|
||||
return nil, grpc.Errorf(codes.Unimplemented, "clientv3/retry_interceptor: cannot retry on ClientStreams, set Disable()")
|
||||
}
|
||||
newStreamer, err := streamer(ctx, desc, cc, method, grpcOpts...)
|
||||
logger.Info("retry stream intercept", zap.Error(err))
|
||||
if err != nil {
|
||||
// TODO(mwitkow): Maybe dial and transport errors should be retriable?
|
||||
return nil, err
|
||||
}
|
||||
retryingStreamer := &serverStreamingRetryingStream{
|
||||
client: c,
|
||||
ClientStream: newStreamer,
|
||||
callOpts: callOpts,
|
||||
ctx: ctx,
|
||||
streamerCall: func(ctx context.Context) (grpc.ClientStream, error) {
|
||||
return streamer(ctx, desc, cc, method, grpcOpts...)
|
||||
},
|
||||
}
|
||||
return retryingStreamer, nil
|
||||
}
|
||||
}
|
||||
|
||||
// type serverStreamingRetryingStream is the implementation of grpc.ClientStream that acts as a
|
||||
// proxy to the underlying call. If any of the RecvMsg() calls fail, it will try to reestablish
|
||||
// a new ClientStream according to the retry policy.
|
||||
type serverStreamingRetryingStream struct {
|
||||
grpc.ClientStream
|
||||
client *Client
|
||||
bufferedSends []interface{} // single message that the client can sen
|
||||
receivedGood bool // indicates whether any prior receives were successful
|
||||
wasClosedSend bool // indicates that CloseSend was closed
|
||||
ctx context.Context
|
||||
callOpts *options
|
||||
streamerCall func(ctx context.Context) (grpc.ClientStream, error)
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (s *serverStreamingRetryingStream) setStream(clientStream grpc.ClientStream) {
|
||||
s.mu.Lock()
|
||||
s.ClientStream = clientStream
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *serverStreamingRetryingStream) getStream() grpc.ClientStream {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.ClientStream
|
||||
}
|
||||
|
||||
func (s *serverStreamingRetryingStream) SendMsg(m interface{}) error {
|
||||
s.mu.Lock()
|
||||
s.bufferedSends = append(s.bufferedSends, m)
|
||||
s.mu.Unlock()
|
||||
return s.getStream().SendMsg(m)
|
||||
}
|
||||
|
||||
func (s *serverStreamingRetryingStream) CloseSend() error {
|
||||
s.mu.Lock()
|
||||
s.wasClosedSend = true
|
||||
s.mu.Unlock()
|
||||
return s.getStream().CloseSend()
|
||||
}
|
||||
|
||||
func (s *serverStreamingRetryingStream) Header() (metadata.MD, error) {
|
||||
return s.getStream().Header()
|
||||
}
|
||||
|
||||
func (s *serverStreamingRetryingStream) Trailer() metadata.MD {
|
||||
return s.getStream().Trailer()
|
||||
}
|
||||
|
||||
func (s *serverStreamingRetryingStream) RecvMsg(m interface{}) error {
|
||||
attemptRetry, lastErr := s.receiveMsgAndIndicateRetry(m)
|
||||
if !attemptRetry {
|
||||
return lastErr // success or hard failure
|
||||
}
|
||||
// We start off from attempt 1, because zeroth was already made on normal SendMsg().
|
||||
for attempt := uint(1); attempt < s.callOpts.max; attempt++ {
|
||||
if err := waitRetryBackoff(attempt, s.ctx, s.callOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
newStream, err := s.reestablishStreamAndResendBuffer(s.ctx)
|
||||
if err != nil {
|
||||
// TODO(mwitkow): Maybe dial and transport errors should be retriable?
|
||||
return err
|
||||
}
|
||||
s.setStream(newStream)
|
||||
attemptRetry, lastErr = s.receiveMsgAndIndicateRetry(m)
|
||||
//fmt.Printf("Received message and indicate: %v %v\n", attemptRetry, lastErr)
|
||||
if !attemptRetry {
|
||||
return lastErr
|
||||
}
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (s *serverStreamingRetryingStream) receiveMsgAndIndicateRetry(m interface{}) (bool, error) {
|
||||
s.mu.RLock()
|
||||
wasGood := s.receivedGood
|
||||
s.mu.RUnlock()
|
||||
err := s.getStream().RecvMsg(m)
|
||||
if err == nil || err == io.EOF {
|
||||
s.mu.Lock()
|
||||
s.receivedGood = true
|
||||
s.mu.Unlock()
|
||||
return false, err
|
||||
} else if wasGood {
|
||||
// previous RecvMsg in the stream succeeded, no retry logic should interfere
|
||||
return false, err
|
||||
}
|
||||
if isContextError(err) {
|
||||
if s.ctx.Err() != nil {
|
||||
return false, err
|
||||
}
|
||||
// its the callCtx deadline or cancellation, in which case try again.
|
||||
return true, err
|
||||
}
|
||||
if s.callOpts.retryAuth && rpctypes.Error(err) == rpctypes.ErrInvalidAuthToken {
|
||||
gterr := s.client.getToken(s.ctx)
|
||||
if gterr != nil {
|
||||
s.client.lg.Info("retry failed to fetch new auth token", zap.Error(gterr))
|
||||
return false, err // return the original error for simplicity
|
||||
}
|
||||
return true, err
|
||||
|
||||
}
|
||||
return isSafeRetry(s.client.lg, err, s.callOpts), err
|
||||
}
|
||||
|
||||
func (s *serverStreamingRetryingStream) reestablishStreamAndResendBuffer(callCtx context.Context) (grpc.ClientStream, error) {
|
||||
s.mu.RLock()
|
||||
bufferedSends := s.bufferedSends
|
||||
s.mu.RUnlock()
|
||||
newStream, err := s.streamerCall(callCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, msg := range bufferedSends {
|
||||
if err := newStream.SendMsg(msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := newStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newStream, nil
|
||||
}
|
||||
|
||||
func waitRetryBackoff(attempt uint, ctx context.Context, callOpts *options) error {
|
||||
var waitTime time.Duration = 0
|
||||
if attempt > 0 {
|
||||
waitTime = callOpts.backoffFunc(attempt)
|
||||
}
|
||||
if waitTime > 0 {
|
||||
timer := time.NewTimer(waitTime)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return contextErrToGrpcErr(ctx.Err())
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isSafeRetry returns "true", if request is safe for retry with the given error.
|
||||
func isSafeRetry(lg *zap.Logger, err error, callOpts *options) bool {
|
||||
if isContextError(err) {
|
||||
return false
|
||||
}
|
||||
switch callOpts.retryPolicy {
|
||||
case repeatable:
|
||||
return isSafeRetryImmutableRPC(err)
|
||||
case nonRepeatable:
|
||||
return isSafeRetryMutableRPC(err)
|
||||
default:
|
||||
lg.Warn("unrecognized retry policy", zap.String("retryPolicy", callOpts.retryPolicy.String()))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isContextError(err error) bool {
|
||||
return grpc.Code(err) == codes.DeadlineExceeded || grpc.Code(err) == codes.Canceled
|
||||
}
|
||||
|
||||
func contextErrToGrpcErr(err error) error {
|
||||
switch err {
|
||||
case context.DeadlineExceeded:
|
||||
return grpc.Errorf(codes.DeadlineExceeded, err.Error())
|
||||
case context.Canceled:
|
||||
return grpc.Errorf(codes.Canceled, err.Error())
|
||||
default:
|
||||
return grpc.Errorf(codes.Unknown, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
defaultOptions = &options{
|
||||
retryPolicy: nonRepeatable,
|
||||
max: 0, // disable
|
||||
backoffFunc: backoffLinearWithJitter(50*time.Millisecond /*jitter*/, 0.10),
|
||||
retryAuth: true,
|
||||
}
|
||||
)
|
||||
|
||||
// backoffFunc denotes a family of functions that control the backoff duration between call retries.
|
||||
//
|
||||
// They are called with an identifier of the attempt, and should return a time the system client should
|
||||
// hold off for. If the time returned is longer than the `context.Context.Deadline` of the request
|
||||
// the deadline of the request takes precedence and the wait will be interrupted before proceeding
|
||||
// with the next iteration.
|
||||
type backoffFunc func(attempt uint) time.Duration
|
||||
|
||||
// withRetryPolicy sets the retry policy of this call.
|
||||
func withRetryPolicy(rp retryPolicy) retryOption {
|
||||
return retryOption{applyFunc: func(o *options) {
|
||||
o.retryPolicy = rp
|
||||
}}
|
||||
}
|
||||
|
||||
// withAuthRetry sets enables authentication retries.
|
||||
func withAuthRetry(retryAuth bool) retryOption {
|
||||
return retryOption{applyFunc: func(o *options) {
|
||||
o.retryAuth = retryAuth
|
||||
}}
|
||||
}
|
||||
|
||||
// withMax sets the maximum number of retries on this call, or this interceptor.
|
||||
func withMax(maxRetries uint) retryOption {
|
||||
return retryOption{applyFunc: func(o *options) {
|
||||
o.max = maxRetries
|
||||
}}
|
||||
}
|
||||
|
||||
// WithBackoff sets the `BackoffFunc `used to control time between retries.
|
||||
func withBackoff(bf backoffFunc) retryOption {
|
||||
return retryOption{applyFunc: func(o *options) {
|
||||
o.backoffFunc = bf
|
||||
}}
|
||||
}
|
||||
|
||||
type options struct {
|
||||
retryPolicy retryPolicy
|
||||
max uint
|
||||
backoffFunc backoffFunc
|
||||
retryAuth bool
|
||||
}
|
||||
|
||||
// retryOption is a grpc.CallOption that is local to clientv3's retry interceptor.
|
||||
type retryOption struct {
|
||||
grpc.EmptyCallOption // make sure we implement private after() and before() fields so we don't panic.
|
||||
applyFunc func(opt *options)
|
||||
}
|
||||
|
||||
func reuseOrNewWithCallOptions(opt *options, retryOptions []retryOption) *options {
|
||||
if len(retryOptions) == 0 {
|
||||
return opt
|
||||
}
|
||||
optCopy := &options{}
|
||||
*optCopy = *opt
|
||||
for _, f := range retryOptions {
|
||||
f.applyFunc(optCopy)
|
||||
}
|
||||
return optCopy
|
||||
}
|
||||
|
||||
func filterCallOptions(callOptions []grpc.CallOption) (grpcOptions []grpc.CallOption, retryOptions []retryOption) {
|
||||
for _, opt := range callOptions {
|
||||
if co, ok := opt.(retryOption); ok {
|
||||
retryOptions = append(retryOptions, co)
|
||||
} else {
|
||||
grpcOptions = append(grpcOptions, opt)
|
||||
}
|
||||
}
|
||||
return grpcOptions, retryOptions
|
||||
}
|
||||
|
||||
// BackoffLinearWithJitter waits a set period of time, allowing for jitter (fractional adjustment).
|
||||
//
|
||||
// For example waitBetween=1s and jitter=0.10 can generate waits between 900ms and 1100ms.
|
||||
func backoffLinearWithJitter(waitBetween time.Duration, jitterFraction float64) backoffFunc {
|
||||
return func(attempt uint) time.Duration {
|
||||
return backoffutils.JitterUp(waitBetween, jitterFraction)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2018 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 snapshot implements utilities around etcd snapshot.
|
||||
package snapshot
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright 2018 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 snapshot
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
type revision struct {
|
||||
main int64
|
||||
sub int64
|
||||
}
|
||||
|
||||
func bytesToRev(bytes []byte) revision {
|
||||
return revision{
|
||||
main: int64(binary.BigEndian.Uint64(bytes[0:8])),
|
||||
sub: int64(binary.BigEndian.Uint64(bytes[9:])),
|
||||
}
|
||||
}
|
||||
|
||||
// initIndex implements ConsistentIndexGetter so the snapshot won't block
|
||||
// the new raft instance by waiting for a future raft index.
|
||||
type initIndex int
|
||||
|
||||
func (i *initIndex) ConsistentIndex() uint64 { return uint64(*i) }
|
||||
|
|
@ -0,0 +1,485 @@
|
|||
// Copyright 2018 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 snapshot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/api/membership"
|
||||
"github.com/coreos/etcd/etcdserver/api/snap"
|
||||
"github.com/coreos/etcd/etcdserver/api/v2store"
|
||||
"github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/lease"
|
||||
"github.com/coreos/etcd/mvcc"
|
||||
"github.com/coreos/etcd/mvcc/backend"
|
||||
"github.com/coreos/etcd/pkg/fileutil"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/raft"
|
||||
"github.com/coreos/etcd/raft/raftpb"
|
||||
"github.com/coreos/etcd/wal"
|
||||
"github.com/coreos/etcd/wal/walpb"
|
||||
|
||||
bolt "github.com/coreos/bbolt"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Manager defines snapshot methods.
|
||||
type Manager interface {
|
||||
// Save fetches snapshot from remote etcd server and saves data
|
||||
// to target path. If the context "ctx" is canceled or timed out,
|
||||
// snapshot save stream will error out (e.g. context.Canceled,
|
||||
// context.DeadlineExceeded). Make sure to specify only one endpoint
|
||||
// in client configuration. Snapshot API must be requested to a
|
||||
// selected node, and saved snapshot is the point-in-time state of
|
||||
// the selected node.
|
||||
Save(ctx context.Context, cfg clientv3.Config, dbPath string) error
|
||||
|
||||
// Status returns the snapshot file information.
|
||||
Status(dbPath string) (Status, error)
|
||||
|
||||
// Restore restores a new etcd data directory from given snapshot
|
||||
// file. It returns an error if specified data directory already
|
||||
// exists, to prevent unintended data directory overwrites.
|
||||
Restore(cfg RestoreConfig) error
|
||||
}
|
||||
|
||||
// NewV3 returns a new snapshot Manager for v3.x snapshot.
|
||||
func NewV3(lg *zap.Logger) Manager {
|
||||
if lg == nil {
|
||||
lg = zap.NewExample()
|
||||
}
|
||||
return &v3Manager{lg: lg}
|
||||
}
|
||||
|
||||
type v3Manager struct {
|
||||
lg *zap.Logger
|
||||
|
||||
name string
|
||||
dbPath string
|
||||
walDir string
|
||||
snapDir string
|
||||
cl *membership.RaftCluster
|
||||
|
||||
skipHashCheck bool
|
||||
}
|
||||
|
||||
// Save fetches snapshot from remote etcd server and saves data to target path.
|
||||
func (s *v3Manager) Save(ctx context.Context, cfg clientv3.Config, dbPath string) error {
|
||||
if len(cfg.Endpoints) != 1 {
|
||||
return fmt.Errorf("snapshot must be requested to one selected node, not multiple %v", cfg.Endpoints)
|
||||
}
|
||||
cli, err := clientv3.New(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
partpath := dbPath + ".part"
|
||||
defer os.RemoveAll(partpath)
|
||||
|
||||
var f *os.File
|
||||
f, err = os.Create(partpath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open %s (%v)", partpath, err)
|
||||
}
|
||||
s.lg.Info(
|
||||
"created temporary db file",
|
||||
zap.String("path", partpath),
|
||||
)
|
||||
|
||||
now := time.Now()
|
||||
var rd io.ReadCloser
|
||||
rd, err = cli.Snapshot(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.lg.Info(
|
||||
"fetching snapshot",
|
||||
zap.String("endpoint", cfg.Endpoints[0]),
|
||||
)
|
||||
if _, err = io.Copy(f, rd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = fileutil.Fsync(f); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.lg.Info(
|
||||
"fetched snapshot",
|
||||
zap.String("endpoint", cfg.Endpoints[0]),
|
||||
zap.Duration("took", time.Since(now)),
|
||||
)
|
||||
|
||||
if err = os.Rename(partpath, dbPath); err != nil {
|
||||
return fmt.Errorf("could not rename %s to %s (%v)", partpath, dbPath, err)
|
||||
}
|
||||
s.lg.Info("saved", zap.String("path", dbPath))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status is the snapshot file status.
|
||||
type Status struct {
|
||||
Hash uint32 `json:"hash"`
|
||||
Revision int64 `json:"revision"`
|
||||
TotalKey int `json:"totalKey"`
|
||||
TotalSize int64 `json:"totalSize"`
|
||||
}
|
||||
|
||||
// Status returns the snapshot file information.
|
||||
func (s *v3Manager) Status(dbPath string) (ds Status, err error) {
|
||||
if _, err = os.Stat(dbPath); err != nil {
|
||||
return ds, err
|
||||
}
|
||||
|
||||
db, err := bolt.Open(dbPath, 0400, &bolt.Options{ReadOnly: true})
|
||||
if err != nil {
|
||||
return ds, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
h := crc32.New(crc32.MakeTable(crc32.Castagnoli))
|
||||
|
||||
if err = db.View(func(tx *bolt.Tx) error {
|
||||
ds.TotalSize = tx.Size()
|
||||
c := tx.Cursor()
|
||||
for next, _ := c.First(); next != nil; next, _ = c.Next() {
|
||||
b := tx.Bucket(next)
|
||||
if b == nil {
|
||||
return fmt.Errorf("cannot get hash of bucket %s", string(next))
|
||||
}
|
||||
h.Write(next)
|
||||
iskeyb := (string(next) == "key")
|
||||
b.ForEach(func(k, v []byte) error {
|
||||
h.Write(k)
|
||||
h.Write(v)
|
||||
if iskeyb {
|
||||
rev := bytesToRev(k)
|
||||
ds.Revision = rev.main
|
||||
}
|
||||
ds.TotalKey++
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return ds, err
|
||||
}
|
||||
|
||||
ds.Hash = h.Sum32()
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
// RestoreConfig configures snapshot restore operation.
|
||||
type RestoreConfig struct {
|
||||
// SnapshotPath is the path of snapshot file to restore from.
|
||||
SnapshotPath string
|
||||
|
||||
// Name is the human-readable name of this member.
|
||||
Name string
|
||||
|
||||
// OutputDataDir is the target data directory to save restored data.
|
||||
// OutputDataDir should not conflict with existing etcd data directory.
|
||||
// If OutputDataDir already exists, it will return an error to prevent
|
||||
// unintended data directory overwrites.
|
||||
// If empty, defaults to "[Name].etcd" if not given.
|
||||
OutputDataDir string
|
||||
// OutputWALDir is the target WAL data directory.
|
||||
// If empty, defaults to "[OutputDataDir]/member/wal" if not given.
|
||||
OutputWALDir string
|
||||
|
||||
// PeerURLs is a list of member's peer URLs to advertise to the rest of the cluster.
|
||||
PeerURLs []string
|
||||
|
||||
// InitialCluster is the initial cluster configuration for restore bootstrap.
|
||||
InitialCluster string
|
||||
// InitialClusterToken is the initial cluster token for etcd cluster during restore bootstrap.
|
||||
InitialClusterToken string
|
||||
|
||||
// SkipHashCheck is "true" to ignore snapshot integrity hash value
|
||||
// (required if copied from data directory).
|
||||
SkipHashCheck bool
|
||||
}
|
||||
|
||||
// Restore restores a new etcd data directory from given snapshot file.
|
||||
func (s *v3Manager) Restore(cfg RestoreConfig) error {
|
||||
pURLs, err := types.NewURLs(cfg.PeerURLs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var ics types.URLsMap
|
||||
ics, err = types.NewURLsMap(cfg.InitialCluster)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srv := etcdserver.ServerConfig{
|
||||
Logger: s.lg,
|
||||
Name: cfg.Name,
|
||||
PeerURLs: pURLs,
|
||||
InitialPeerURLsMap: ics,
|
||||
InitialClusterToken: cfg.InitialClusterToken,
|
||||
}
|
||||
if err = srv.VerifyBootstrap(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.cl, err = membership.NewClusterFromURLsMap(s.lg, cfg.InitialClusterToken, ics)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dataDir := cfg.OutputDataDir
|
||||
if dataDir == "" {
|
||||
dataDir = cfg.Name + ".etcd"
|
||||
}
|
||||
if fileutil.Exist(dataDir) {
|
||||
return fmt.Errorf("data-dir %q exists", dataDir)
|
||||
}
|
||||
|
||||
walDir := cfg.OutputWALDir
|
||||
if walDir == "" {
|
||||
walDir = filepath.Join(dataDir, "member", "wal")
|
||||
} else if fileutil.Exist(walDir) {
|
||||
return fmt.Errorf("wal-dir %q exists", walDir)
|
||||
}
|
||||
|
||||
s.name = cfg.Name
|
||||
s.dbPath = cfg.SnapshotPath
|
||||
s.walDir = walDir
|
||||
s.snapDir = filepath.Join(dataDir, "member", "snap")
|
||||
s.skipHashCheck = cfg.SkipHashCheck
|
||||
|
||||
s.lg.Info(
|
||||
"restoring snapshot",
|
||||
zap.String("path", s.dbPath),
|
||||
zap.String("wal-dir", s.walDir),
|
||||
zap.String("data-dir", dataDir),
|
||||
zap.String("snap-dir", s.snapDir),
|
||||
)
|
||||
if err = s.saveDB(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = s.saveWALAndSnap(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.lg.Info(
|
||||
"restored snapshot",
|
||||
zap.String("path", s.dbPath),
|
||||
zap.String("wal-dir", s.walDir),
|
||||
zap.String("data-dir", dataDir),
|
||||
zap.String("snap-dir", s.snapDir),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveDB copies the database snapshot to the snapshot directory
|
||||
func (s *v3Manager) saveDB() error {
|
||||
f, ferr := os.OpenFile(s.dbPath, os.O_RDONLY, 0600)
|
||||
if ferr != nil {
|
||||
return ferr
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// get snapshot integrity hash
|
||||
if _, err := f.Seek(-sha256.Size, io.SeekEnd); err != nil {
|
||||
return err
|
||||
}
|
||||
sha := make([]byte, sha256.Size)
|
||||
if _, err := f.Read(sha); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fileutil.CreateDirAll(s.snapDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbpath := filepath.Join(s.snapDir, "db")
|
||||
db, dberr := os.OpenFile(dbpath, os.O_RDWR|os.O_CREATE, 0600)
|
||||
if dberr != nil {
|
||||
return dberr
|
||||
}
|
||||
if _, err := io.Copy(db, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// truncate away integrity hash, if any.
|
||||
off, serr := db.Seek(0, io.SeekEnd)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
hasHash := (off % 512) == sha256.Size
|
||||
if hasHash {
|
||||
if err := db.Truncate(off - sha256.Size); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !hasHash && !s.skipHashCheck {
|
||||
return fmt.Errorf("snapshot missing hash but --skip-hash-check=false")
|
||||
}
|
||||
|
||||
if hasHash && !s.skipHashCheck {
|
||||
// check for match
|
||||
if _, err := db.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, db); err != nil {
|
||||
return err
|
||||
}
|
||||
dbsha := h.Sum(nil)
|
||||
if !reflect.DeepEqual(sha, dbsha) {
|
||||
return fmt.Errorf("expected sha256 %v, got %v", sha, dbsha)
|
||||
}
|
||||
}
|
||||
|
||||
// db hash is OK, can now modify DB so it can be part of a new cluster
|
||||
db.Close()
|
||||
|
||||
commit := len(s.cl.Members())
|
||||
|
||||
// update consistentIndex so applies go through on etcdserver despite
|
||||
// having a new raft instance
|
||||
be := backend.NewDefaultBackend(dbpath)
|
||||
|
||||
// a lessor never timeouts leases
|
||||
lessor := lease.NewLessor(be, math.MaxInt64)
|
||||
|
||||
mvs := mvcc.NewStore(s.lg, be, lessor, (*initIndex)(&commit))
|
||||
txn := mvs.Write()
|
||||
btx := be.BatchTx()
|
||||
del := func(k, v []byte) error {
|
||||
txn.DeleteRange(k, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// delete stored members from old cluster since using new members
|
||||
btx.UnsafeForEach([]byte("members"), del)
|
||||
|
||||
// todo: add back new members when we start to deprecate old snap file.
|
||||
btx.UnsafeForEach([]byte("members_removed"), del)
|
||||
|
||||
// trigger write-out of new consistent index
|
||||
txn.End()
|
||||
|
||||
mvs.Commit()
|
||||
mvs.Close()
|
||||
be.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveWALAndSnap creates a WAL for the initial cluster
|
||||
func (s *v3Manager) saveWALAndSnap() error {
|
||||
if err := fileutil.CreateDirAll(s.walDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add members again to persist them to the store we create.
|
||||
st := v2store.New(etcdserver.StoreClusterPrefix, etcdserver.StoreKeysPrefix)
|
||||
s.cl.SetStore(st)
|
||||
for _, m := range s.cl.Members() {
|
||||
s.cl.AddMember(m)
|
||||
}
|
||||
|
||||
m := s.cl.MemberByName(s.name)
|
||||
md := &etcdserverpb.Metadata{NodeID: uint64(m.ID), ClusterID: uint64(s.cl.ID())}
|
||||
metadata, merr := md.Marshal()
|
||||
if merr != nil {
|
||||
return merr
|
||||
}
|
||||
w, walerr := wal.Create(s.lg, s.walDir, metadata)
|
||||
if walerr != nil {
|
||||
return walerr
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
peers := make([]raft.Peer, len(s.cl.MemberIDs()))
|
||||
for i, id := range s.cl.MemberIDs() {
|
||||
ctx, err := json.Marshal((*s.cl).Member(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
peers[i] = raft.Peer{ID: uint64(id), Context: ctx}
|
||||
}
|
||||
|
||||
ents := make([]raftpb.Entry, len(peers))
|
||||
nodeIDs := make([]uint64, len(peers))
|
||||
for i, p := range peers {
|
||||
nodeIDs[i] = p.ID
|
||||
cc := raftpb.ConfChange{
|
||||
Type: raftpb.ConfChangeAddNode,
|
||||
NodeID: p.ID,
|
||||
Context: p.Context,
|
||||
}
|
||||
d, err := cc.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ents[i] = raftpb.Entry{
|
||||
Type: raftpb.EntryConfChange,
|
||||
Term: 1,
|
||||
Index: uint64(i + 1),
|
||||
Data: d,
|
||||
}
|
||||
}
|
||||
|
||||
commit, term := uint64(len(ents)), uint64(1)
|
||||
if err := w.Save(raftpb.HardState{
|
||||
Term: term,
|
||||
Vote: peers[0].ID,
|
||||
Commit: commit,
|
||||
}, ents); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, berr := st.Save()
|
||||
if berr != nil {
|
||||
return berr
|
||||
}
|
||||
raftSnap := raftpb.Snapshot{
|
||||
Data: b,
|
||||
Metadata: raftpb.SnapshotMetadata{
|
||||
Index: commit,
|
||||
Term: term,
|
||||
ConfState: raftpb.ConfState{
|
||||
Nodes: nodeIDs,
|
||||
},
|
||||
},
|
||||
}
|
||||
sn := snap.New(s.lg, s.snapDir)
|
||||
if err := sn.SaveSnap(raftSnap); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.SaveSnapshot(walpb.Snapshot{Index: commit, Term: term})
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright 2017 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 yaml handles yaml-formatted clientv3 configuration data.
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/pkg/tlsutil"
|
||||
)
|
||||
|
||||
type yamlConfig struct {
|
||||
clientv3.Config
|
||||
|
||||
InsecureTransport bool `json:"insecure-transport"`
|
||||
InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify"`
|
||||
Certfile string `json:"cert-file"`
|
||||
Keyfile string `json:"key-file"`
|
||||
TrustedCAfile string `json:"trusted-ca-file"`
|
||||
|
||||
// CAfile is being deprecated. Use 'TrustedCAfile' instead.
|
||||
// TODO: deprecate this in v4
|
||||
CAfile string `json:"ca-file"`
|
||||
}
|
||||
|
||||
// NewConfig creates a new clientv3.Config from a yaml file.
|
||||
func NewConfig(fpath string) (*clientv3.Config, error) {
|
||||
b, err := ioutil.ReadFile(fpath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
yc := &yamlConfig{}
|
||||
|
||||
err = yaml.Unmarshal(b, yc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if yc.InsecureTransport {
|
||||
return &yc.Config, nil
|
||||
}
|
||||
|
||||
var (
|
||||
cert *tls.Certificate
|
||||
cp *x509.CertPool
|
||||
)
|
||||
|
||||
if yc.Certfile != "" && yc.Keyfile != "" {
|
||||
cert, err = tlsutil.NewCert(yc.Certfile, yc.Keyfile, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if yc.TrustedCAfile != "" {
|
||||
cp, err = tlsutil.NewCertPool([]string{yc.TrustedCAfile})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tlscfg := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
InsecureSkipVerify: yc.InsecureSkipTLSVerify,
|
||||
RootCAs: cp,
|
||||
}
|
||||
if cert != nil {
|
||||
tlscfg.Certificates = []tls.Certificate{*cert}
|
||||
}
|
||||
yc.Config.TLS = tlscfg
|
||||
|
||||
return &yc.Config, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2016 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.
|
||||
|
||||
// raftexample is a simple KV store using the raft and rafthttp libraries.
|
||||
package main
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
// Copyright 2015 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 main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/coreos/etcd/raft/raftpb"
|
||||
)
|
||||
|
||||
// Handler for a http based key-value store backed by raft
|
||||
type httpKVAPI struct {
|
||||
store *kvstore
|
||||
confChangeC chan<- raftpb.ConfChange
|
||||
}
|
||||
|
||||
func (h *httpKVAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
key := r.RequestURI
|
||||
switch {
|
||||
case r.Method == "PUT":
|
||||
v, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Printf("Failed to read on PUT (%v)\n", err)
|
||||
http.Error(w, "Failed on PUT", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
h.store.Propose(key, string(v))
|
||||
|
||||
// Optimistic-- no waiting for ack from raft. Value is not yet
|
||||
// committed so a subsequent GET on the key may return old value
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case r.Method == "GET":
|
||||
if v, ok := h.store.Lookup(key); ok {
|
||||
w.Write([]byte(v))
|
||||
} else {
|
||||
http.Error(w, "Failed to GET", http.StatusNotFound)
|
||||
}
|
||||
case r.Method == "POST":
|
||||
url, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Printf("Failed to read on POST (%v)\n", err)
|
||||
http.Error(w, "Failed on POST", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
nodeId, err := strconv.ParseUint(key[1:], 0, 64)
|
||||
if err != nil {
|
||||
log.Printf("Failed to convert ID for conf change (%v)\n", err)
|
||||
http.Error(w, "Failed on POST", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
cc := raftpb.ConfChange{
|
||||
Type: raftpb.ConfChangeAddNode,
|
||||
NodeID: nodeId,
|
||||
Context: url,
|
||||
}
|
||||
h.confChangeC <- cc
|
||||
|
||||
// As above, optimistic that raft will apply the conf change
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case r.Method == "DELETE":
|
||||
nodeId, err := strconv.ParseUint(key[1:], 0, 64)
|
||||
if err != nil {
|
||||
log.Printf("Failed to convert ID for conf change (%v)\n", err)
|
||||
http.Error(w, "Failed on DELETE", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
cc := raftpb.ConfChange{
|
||||
Type: raftpb.ConfChangeRemoveNode,
|
||||
NodeID: nodeId,
|
||||
}
|
||||
h.confChangeC <- cc
|
||||
|
||||
// As above, optimistic that raft will apply the conf change
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
w.Header().Set("Allow", "PUT")
|
||||
w.Header().Add("Allow", "GET")
|
||||
w.Header().Add("Allow", "POST")
|
||||
w.Header().Add("Allow", "DELETE")
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// serveHttpKVAPI starts a key-value server with a GET/PUT API and listens.
|
||||
func serveHttpKVAPI(kv *kvstore, port int, confChangeC chan<- raftpb.ConfChange, errorC <-chan error) {
|
||||
srv := http.Server{
|
||||
Addr: ":" + strconv.Itoa(port),
|
||||
Handler: &httpKVAPI{
|
||||
store: kv,
|
||||
confChangeC: confChangeC,
|
||||
},
|
||||
}
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// exit when raft goes down
|
||||
if err, ok := <-errorC; ok {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
// Copyright 2015 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 main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/api/snap"
|
||||
)
|
||||
|
||||
// a key-value store backed by raft
|
||||
type kvstore struct {
|
||||
proposeC chan<- string // channel for proposing updates
|
||||
mu sync.RWMutex
|
||||
kvStore map[string]string // current committed key-value pairs
|
||||
snapshotter *snap.Snapshotter
|
||||
}
|
||||
|
||||
type kv struct {
|
||||
Key string
|
||||
Val string
|
||||
}
|
||||
|
||||
func newKVStore(snapshotter *snap.Snapshotter, proposeC chan<- string, commitC <-chan *string, errorC <-chan error) *kvstore {
|
||||
s := &kvstore{proposeC: proposeC, kvStore: make(map[string]string), snapshotter: snapshotter}
|
||||
// replay log into key-value map
|
||||
s.readCommits(commitC, errorC)
|
||||
// read commits from raft into kvStore map until error
|
||||
go s.readCommits(commitC, errorC)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *kvstore) Lookup(key string) (string, bool) {
|
||||
s.mu.RLock()
|
||||
v, ok := s.kvStore[key]
|
||||
s.mu.RUnlock()
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (s *kvstore) Propose(k string, v string) {
|
||||
var buf bytes.Buffer
|
||||
if err := gob.NewEncoder(&buf).Encode(kv{k, v}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
s.proposeC <- buf.String()
|
||||
}
|
||||
|
||||
func (s *kvstore) readCommits(commitC <-chan *string, errorC <-chan error) {
|
||||
for data := range commitC {
|
||||
if data == nil {
|
||||
// done replaying log; new data incoming
|
||||
// OR signaled to load snapshot
|
||||
snapshot, err := s.snapshotter.Load()
|
||||
if err == snap.ErrNoSnapshot {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
log.Printf("loading snapshot at term %d and index %d", snapshot.Metadata.Term, snapshot.Metadata.Index)
|
||||
if err := s.recoverFromSnapshot(snapshot.Data); err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var dataKv kv
|
||||
dec := gob.NewDecoder(bytes.NewBufferString(*data))
|
||||
if err := dec.Decode(&dataKv); err != nil {
|
||||
log.Fatalf("raftexample: could not decode message (%v)", err)
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.kvStore[dataKv.Key] = dataKv.Val
|
||||
s.mu.Unlock()
|
||||
}
|
||||
if err, ok := <-errorC; ok {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *kvstore) getSnapshot() ([]byte, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return json.Marshal(s.kvStore)
|
||||
}
|
||||
|
||||
func (s *kvstore) recoverFromSnapshot(snapshot []byte) error {
|
||||
var store map[string]string
|
||||
if err := json.Unmarshal(snapshot, &store); err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.kvStore = store
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright 2015 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 main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// stoppableListener sets TCP keep-alive timeouts on accepted
|
||||
// connections and waits on stopc message
|
||||
type stoppableListener struct {
|
||||
*net.TCPListener
|
||||
stopc <-chan struct{}
|
||||
}
|
||||
|
||||
func newStoppableListener(addr string, stopc <-chan struct{}) (*stoppableListener, error) {
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &stoppableListener{ln.(*net.TCPListener), stopc}, nil
|
||||
}
|
||||
|
||||
func (ln stoppableListener) Accept() (c net.Conn, err error) {
|
||||
connc := make(chan *net.TCPConn, 1)
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
tc, err := ln.AcceptTCP()
|
||||
if err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
connc <- tc
|
||||
}()
|
||||
select {
|
||||
case <-ln.stopc:
|
||||
return nil, errors.New("server stopped")
|
||||
case err := <-errc:
|
||||
return nil, err
|
||||
case tc := <-connc:
|
||||
tc.SetKeepAlive(true)
|
||||
tc.SetKeepAlivePeriod(3 * time.Minute)
|
||||
return tc, nil
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright 2015 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 main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/etcd/raft/raftpb"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cluster := flag.String("cluster", "http://127.0.0.1:9021", "comma separated cluster peers")
|
||||
id := flag.Int("id", 1, "node ID")
|
||||
kvport := flag.Int("port", 9121, "key-value server port")
|
||||
join := flag.Bool("join", false, "join an existing cluster")
|
||||
flag.Parse()
|
||||
|
||||
proposeC := make(chan string)
|
||||
defer close(proposeC)
|
||||
confChangeC := make(chan raftpb.ConfChange)
|
||||
defer close(confChangeC)
|
||||
|
||||
// raft provides a commit stream for the proposals from the http api
|
||||
var kvs *kvstore
|
||||
getSnapshot := func() ([]byte, error) { return kvs.getSnapshot() }
|
||||
commitC, errorC, snapshotterReady := newRaftNode(*id, strings.Split(*cluster, ","), *join, getSnapshot, proposeC, confChangeC)
|
||||
|
||||
kvs = newKVStore(<-snapshotterReady, proposeC, commitC, errorC)
|
||||
|
||||
// the key-value http handler will propose updates to raft
|
||||
serveHttpKVAPI(kvs, *kvport, confChangeC, errorC)
|
||||
}
|
||||
|
|
@ -0,0 +1,481 @@
|
|||
// Copyright 2015 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 main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/api/rafthttp"
|
||||
"github.com/coreos/etcd/etcdserver/api/snap"
|
||||
stats "github.com/coreos/etcd/etcdserver/api/v2stats"
|
||||
"github.com/coreos/etcd/pkg/fileutil"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/raft"
|
||||
"github.com/coreos/etcd/raft/raftpb"
|
||||
"github.com/coreos/etcd/wal"
|
||||
"github.com/coreos/etcd/wal/walpb"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// A key-value stream backed by raft
|
||||
type raftNode struct {
|
||||
proposeC <-chan string // proposed messages (k,v)
|
||||
confChangeC <-chan raftpb.ConfChange // proposed cluster config changes
|
||||
commitC chan<- *string // entries committed to log (k,v)
|
||||
errorC chan<- error // errors from raft session
|
||||
|
||||
id int // client ID for raft session
|
||||
peers []string // raft peer URLs
|
||||
join bool // node is joining an existing cluster
|
||||
waldir string // path to WAL directory
|
||||
snapdir string // path to snapshot directory
|
||||
getSnapshot func() ([]byte, error)
|
||||
lastIndex uint64 // index of log at start
|
||||
|
||||
confState raftpb.ConfState
|
||||
snapshotIndex uint64
|
||||
appliedIndex uint64
|
||||
|
||||
// raft backing for the commit/error channel
|
||||
node raft.Node
|
||||
raftStorage *raft.MemoryStorage
|
||||
wal *wal.WAL
|
||||
|
||||
snapshotter *snap.Snapshotter
|
||||
snapshotterReady chan *snap.Snapshotter // signals when snapshotter is ready
|
||||
|
||||
snapCount uint64
|
||||
transport *rafthttp.Transport
|
||||
stopc chan struct{} // signals proposal channel closed
|
||||
httpstopc chan struct{} // signals http server to shutdown
|
||||
httpdonec chan struct{} // signals http server shutdown complete
|
||||
}
|
||||
|
||||
var defaultSnapshotCount uint64 = 10000
|
||||
|
||||
// newRaftNode initiates a raft instance and returns a committed log entry
|
||||
// channel and error channel. Proposals for log updates are sent over the
|
||||
// provided the proposal channel. All log entries are replayed over the
|
||||
// commit channel, followed by a nil message (to indicate the channel is
|
||||
// current), then new log entries. To shutdown, close proposeC and read errorC.
|
||||
func newRaftNode(id int, peers []string, join bool, getSnapshot func() ([]byte, error), proposeC <-chan string,
|
||||
confChangeC <-chan raftpb.ConfChange) (<-chan *string, <-chan error, <-chan *snap.Snapshotter) {
|
||||
|
||||
commitC := make(chan *string)
|
||||
errorC := make(chan error)
|
||||
|
||||
rc := &raftNode{
|
||||
proposeC: proposeC,
|
||||
confChangeC: confChangeC,
|
||||
commitC: commitC,
|
||||
errorC: errorC,
|
||||
id: id,
|
||||
peers: peers,
|
||||
join: join,
|
||||
waldir: fmt.Sprintf("raftexample-%d", id),
|
||||
snapdir: fmt.Sprintf("raftexample-%d-snap", id),
|
||||
getSnapshot: getSnapshot,
|
||||
snapCount: defaultSnapshotCount,
|
||||
stopc: make(chan struct{}),
|
||||
httpstopc: make(chan struct{}),
|
||||
httpdonec: make(chan struct{}),
|
||||
|
||||
snapshotterReady: make(chan *snap.Snapshotter, 1),
|
||||
// rest of structure populated after WAL replay
|
||||
}
|
||||
go rc.startRaft()
|
||||
return commitC, errorC, rc.snapshotterReady
|
||||
}
|
||||
|
||||
func (rc *raftNode) saveSnap(snap raftpb.Snapshot) error {
|
||||
// must save the snapshot index to the WAL before saving the
|
||||
// snapshot to maintain the invariant that we only Open the
|
||||
// wal at previously-saved snapshot indexes.
|
||||
walSnap := walpb.Snapshot{
|
||||
Index: snap.Metadata.Index,
|
||||
Term: snap.Metadata.Term,
|
||||
}
|
||||
if err := rc.wal.SaveSnapshot(walSnap); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rc.snapshotter.SaveSnap(snap); err != nil {
|
||||
return err
|
||||
}
|
||||
return rc.wal.ReleaseLockTo(snap.Metadata.Index)
|
||||
}
|
||||
|
||||
func (rc *raftNode) entriesToApply(ents []raftpb.Entry) (nents []raftpb.Entry) {
|
||||
if len(ents) == 0 {
|
||||
return
|
||||
}
|
||||
firstIdx := ents[0].Index
|
||||
if firstIdx > rc.appliedIndex+1 {
|
||||
log.Fatalf("first index of committed entry[%d] should <= progress.appliedIndex[%d]+1", firstIdx, rc.appliedIndex)
|
||||
}
|
||||
if rc.appliedIndex-firstIdx+1 < uint64(len(ents)) {
|
||||
nents = ents[rc.appliedIndex-firstIdx+1:]
|
||||
}
|
||||
return nents
|
||||
}
|
||||
|
||||
// publishEntries writes committed log entries to commit channel and returns
|
||||
// whether all entries could be published.
|
||||
func (rc *raftNode) publishEntries(ents []raftpb.Entry) bool {
|
||||
for i := range ents {
|
||||
switch ents[i].Type {
|
||||
case raftpb.EntryNormal:
|
||||
if len(ents[i].Data) == 0 {
|
||||
// ignore empty messages
|
||||
break
|
||||
}
|
||||
s := string(ents[i].Data)
|
||||
select {
|
||||
case rc.commitC <- &s:
|
||||
case <-rc.stopc:
|
||||
return false
|
||||
}
|
||||
|
||||
case raftpb.EntryConfChange:
|
||||
var cc raftpb.ConfChange
|
||||
cc.Unmarshal(ents[i].Data)
|
||||
rc.confState = *rc.node.ApplyConfChange(cc)
|
||||
switch cc.Type {
|
||||
case raftpb.ConfChangeAddNode:
|
||||
if len(cc.Context) > 0 {
|
||||
rc.transport.AddPeer(types.ID(cc.NodeID), []string{string(cc.Context)})
|
||||
}
|
||||
case raftpb.ConfChangeRemoveNode:
|
||||
if cc.NodeID == uint64(rc.id) {
|
||||
log.Println("I've been removed from the cluster! Shutting down.")
|
||||
return false
|
||||
}
|
||||
rc.transport.RemovePeer(types.ID(cc.NodeID))
|
||||
}
|
||||
}
|
||||
|
||||
// after commit, update appliedIndex
|
||||
rc.appliedIndex = ents[i].Index
|
||||
|
||||
// special nil commit to signal replay has finished
|
||||
if ents[i].Index == rc.lastIndex {
|
||||
select {
|
||||
case rc.commitC <- nil:
|
||||
case <-rc.stopc:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (rc *raftNode) loadSnapshot() *raftpb.Snapshot {
|
||||
snapshot, err := rc.snapshotter.Load()
|
||||
if err != nil && err != snap.ErrNoSnapshot {
|
||||
log.Fatalf("raftexample: error loading snapshot (%v)", err)
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
// openWAL returns a WAL ready for reading.
|
||||
func (rc *raftNode) openWAL(snapshot *raftpb.Snapshot) *wal.WAL {
|
||||
if !wal.Exist(rc.waldir) {
|
||||
if err := os.Mkdir(rc.waldir, 0750); err != nil {
|
||||
log.Fatalf("raftexample: cannot create dir for wal (%v)", err)
|
||||
}
|
||||
|
||||
w, err := wal.Create(zap.NewExample(), rc.waldir, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("raftexample: create wal error (%v)", err)
|
||||
}
|
||||
w.Close()
|
||||
}
|
||||
|
||||
walsnap := walpb.Snapshot{}
|
||||
if snapshot != nil {
|
||||
walsnap.Index, walsnap.Term = snapshot.Metadata.Index, snapshot.Metadata.Term
|
||||
}
|
||||
log.Printf("loading WAL at term %d and index %d", walsnap.Term, walsnap.Index)
|
||||
w, err := wal.Open(zap.NewExample(), rc.waldir, walsnap)
|
||||
if err != nil {
|
||||
log.Fatalf("raftexample: error loading wal (%v)", err)
|
||||
}
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// replayWAL replays WAL entries into the raft instance.
|
||||
func (rc *raftNode) replayWAL() *wal.WAL {
|
||||
log.Printf("replaying WAL of member %d", rc.id)
|
||||
snapshot := rc.loadSnapshot()
|
||||
w := rc.openWAL(snapshot)
|
||||
_, st, ents, err := w.ReadAll()
|
||||
if err != nil {
|
||||
log.Fatalf("raftexample: failed to read WAL (%v)", err)
|
||||
}
|
||||
rc.raftStorage = raft.NewMemoryStorage()
|
||||
if snapshot != nil {
|
||||
rc.raftStorage.ApplySnapshot(*snapshot)
|
||||
}
|
||||
rc.raftStorage.SetHardState(st)
|
||||
|
||||
// append to storage so raft starts at the right place in log
|
||||
rc.raftStorage.Append(ents)
|
||||
// send nil once lastIndex is published so client knows commit channel is current
|
||||
if len(ents) > 0 {
|
||||
rc.lastIndex = ents[len(ents)-1].Index
|
||||
} else {
|
||||
rc.commitC <- nil
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
func (rc *raftNode) writeError(err error) {
|
||||
rc.stopHTTP()
|
||||
close(rc.commitC)
|
||||
rc.errorC <- err
|
||||
close(rc.errorC)
|
||||
rc.node.Stop()
|
||||
}
|
||||
|
||||
func (rc *raftNode) startRaft() {
|
||||
if !fileutil.Exist(rc.snapdir) {
|
||||
if err := os.Mkdir(rc.snapdir, 0750); err != nil {
|
||||
log.Fatalf("raftexample: cannot create dir for snapshot (%v)", err)
|
||||
}
|
||||
}
|
||||
rc.snapshotter = snap.New(zap.NewExample(), rc.snapdir)
|
||||
rc.snapshotterReady <- rc.snapshotter
|
||||
|
||||
oldwal := wal.Exist(rc.waldir)
|
||||
rc.wal = rc.replayWAL()
|
||||
|
||||
rpeers := make([]raft.Peer, len(rc.peers))
|
||||
for i := range rpeers {
|
||||
rpeers[i] = raft.Peer{ID: uint64(i + 1)}
|
||||
}
|
||||
c := &raft.Config{
|
||||
ID: uint64(rc.id),
|
||||
ElectionTick: 10,
|
||||
HeartbeatTick: 1,
|
||||
Storage: rc.raftStorage,
|
||||
MaxSizePerMsg: 1024 * 1024,
|
||||
MaxInflightMsgs: 256,
|
||||
}
|
||||
|
||||
if oldwal {
|
||||
rc.node = raft.RestartNode(c)
|
||||
} else {
|
||||
startPeers := rpeers
|
||||
if rc.join {
|
||||
startPeers = nil
|
||||
}
|
||||
rc.node = raft.StartNode(c, startPeers)
|
||||
}
|
||||
|
||||
rc.transport = &rafthttp.Transport{
|
||||
Logger: zap.NewExample(),
|
||||
ID: types.ID(rc.id),
|
||||
ClusterID: 0x1000,
|
||||
Raft: rc,
|
||||
ServerStats: stats.NewServerStats("", ""),
|
||||
LeaderStats: stats.NewLeaderStats(strconv.Itoa(rc.id)),
|
||||
ErrorC: make(chan error),
|
||||
}
|
||||
|
||||
rc.transport.Start()
|
||||
for i := range rc.peers {
|
||||
if i+1 != rc.id {
|
||||
rc.transport.AddPeer(types.ID(i+1), []string{rc.peers[i]})
|
||||
}
|
||||
}
|
||||
|
||||
go rc.serveRaft()
|
||||
go rc.serveChannels()
|
||||
}
|
||||
|
||||
// stop closes http, closes all channels, and stops raft.
|
||||
func (rc *raftNode) stop() {
|
||||
rc.stopHTTP()
|
||||
close(rc.commitC)
|
||||
close(rc.errorC)
|
||||
rc.node.Stop()
|
||||
}
|
||||
|
||||
func (rc *raftNode) stopHTTP() {
|
||||
rc.transport.Stop()
|
||||
close(rc.httpstopc)
|
||||
<-rc.httpdonec
|
||||
}
|
||||
|
||||
func (rc *raftNode) publishSnapshot(snapshotToSave raftpb.Snapshot) {
|
||||
if raft.IsEmptySnap(snapshotToSave) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("publishing snapshot at index %d", rc.snapshotIndex)
|
||||
defer log.Printf("finished publishing snapshot at index %d", rc.snapshotIndex)
|
||||
|
||||
if snapshotToSave.Metadata.Index <= rc.appliedIndex {
|
||||
log.Fatalf("snapshot index [%d] should > progress.appliedIndex [%d] + 1", snapshotToSave.Metadata.Index, rc.appliedIndex)
|
||||
}
|
||||
rc.commitC <- nil // trigger kvstore to load snapshot
|
||||
|
||||
rc.confState = snapshotToSave.Metadata.ConfState
|
||||
rc.snapshotIndex = snapshotToSave.Metadata.Index
|
||||
rc.appliedIndex = snapshotToSave.Metadata.Index
|
||||
}
|
||||
|
||||
var snapshotCatchUpEntriesN uint64 = 10000
|
||||
|
||||
func (rc *raftNode) maybeTriggerSnapshot() {
|
||||
if rc.appliedIndex-rc.snapshotIndex <= rc.snapCount {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("start snapshot [applied index: %d | last snapshot index: %d]", rc.appliedIndex, rc.snapshotIndex)
|
||||
data, err := rc.getSnapshot()
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
snap, err := rc.raftStorage.CreateSnapshot(rc.appliedIndex, &rc.confState, data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := rc.saveSnap(snap); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
compactIndex := uint64(1)
|
||||
if rc.appliedIndex > snapshotCatchUpEntriesN {
|
||||
compactIndex = rc.appliedIndex - snapshotCatchUpEntriesN
|
||||
}
|
||||
if err := rc.raftStorage.Compact(compactIndex); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.Printf("compacted log at index %d", compactIndex)
|
||||
rc.snapshotIndex = rc.appliedIndex
|
||||
}
|
||||
|
||||
func (rc *raftNode) serveChannels() {
|
||||
snap, err := rc.raftStorage.Snapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rc.confState = snap.Metadata.ConfState
|
||||
rc.snapshotIndex = snap.Metadata.Index
|
||||
rc.appliedIndex = snap.Metadata.Index
|
||||
|
||||
defer rc.wal.Close()
|
||||
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
// send proposals over raft
|
||||
go func() {
|
||||
var confChangeCount uint64 = 0
|
||||
|
||||
for rc.proposeC != nil && rc.confChangeC != nil {
|
||||
select {
|
||||
case prop, ok := <-rc.proposeC:
|
||||
if !ok {
|
||||
rc.proposeC = nil
|
||||
} else {
|
||||
// blocks until accepted by raft state machine
|
||||
rc.node.Propose(context.TODO(), []byte(prop))
|
||||
}
|
||||
|
||||
case cc, ok := <-rc.confChangeC:
|
||||
if !ok {
|
||||
rc.confChangeC = nil
|
||||
} else {
|
||||
confChangeCount += 1
|
||||
cc.ID = confChangeCount
|
||||
rc.node.ProposeConfChange(context.TODO(), cc)
|
||||
}
|
||||
}
|
||||
}
|
||||
// client closed channel; shutdown raft if not already
|
||||
close(rc.stopc)
|
||||
}()
|
||||
|
||||
// event loop on raft state machine updates
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
rc.node.Tick()
|
||||
|
||||
// store raft entries to wal, then publish over commit channel
|
||||
case rd := <-rc.node.Ready():
|
||||
rc.wal.Save(rd.HardState, rd.Entries)
|
||||
if !raft.IsEmptySnap(rd.Snapshot) {
|
||||
rc.saveSnap(rd.Snapshot)
|
||||
rc.raftStorage.ApplySnapshot(rd.Snapshot)
|
||||
rc.publishSnapshot(rd.Snapshot)
|
||||
}
|
||||
rc.raftStorage.Append(rd.Entries)
|
||||
rc.transport.Send(rd.Messages)
|
||||
if ok := rc.publishEntries(rc.entriesToApply(rd.CommittedEntries)); !ok {
|
||||
rc.stop()
|
||||
return
|
||||
}
|
||||
rc.maybeTriggerSnapshot()
|
||||
rc.node.Advance()
|
||||
|
||||
case err := <-rc.transport.ErrorC:
|
||||
rc.writeError(err)
|
||||
return
|
||||
|
||||
case <-rc.stopc:
|
||||
rc.stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *raftNode) serveRaft() {
|
||||
url, err := url.Parse(rc.peers[rc.id-1])
|
||||
if err != nil {
|
||||
log.Fatalf("raftexample: Failed parsing URL (%v)", err)
|
||||
}
|
||||
|
||||
ln, err := newStoppableListener(url.Host, rc.httpstopc)
|
||||
if err != nil {
|
||||
log.Fatalf("raftexample: Failed to listen rafthttp (%v)", err)
|
||||
}
|
||||
|
||||
err = (&http.Server{Handler: rc.transport.Handler()}).Serve(ln)
|
||||
select {
|
||||
case <-rc.httpstopc:
|
||||
default:
|
||||
log.Fatalf("raftexample: Failed to serve rafthttp (%v)", err)
|
||||
}
|
||||
close(rc.httpdonec)
|
||||
}
|
||||
|
||||
func (rc *raftNode) Process(ctx context.Context, m raftpb.Message) error {
|
||||
return rc.node.Step(ctx, m)
|
||||
}
|
||||
func (rc *raftNode) IsIDRemoved(id uint64) bool { return false }
|
||||
func (rc *raftNode) ReportUnreachable(id uint64) {}
|
||||
func (rc *raftNode) ReportSnapshot(id uint64, status raft.SnapshotStatus) {}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright 2016 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 recipe
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/mvcc/mvccpb"
|
||||
)
|
||||
|
||||
// Barrier creates a key in etcd to block processes, then deletes the key to
|
||||
// release all blocked processes.
|
||||
type Barrier struct {
|
||||
client *v3.Client
|
||||
ctx context.Context
|
||||
|
||||
key string
|
||||
}
|
||||
|
||||
func NewBarrier(client *v3.Client, key string) *Barrier {
|
||||
return &Barrier{client, context.TODO(), key}
|
||||
}
|
||||
|
||||
// Hold creates the barrier key causing processes to block on Wait.
|
||||
func (b *Barrier) Hold() error {
|
||||
_, err := newKey(b.client, b.key, 0)
|
||||
return err
|
||||
}
|
||||
|
||||
// Release deletes the barrier key to unblock all waiting processes.
|
||||
func (b *Barrier) Release() error {
|
||||
_, err := b.client.Delete(b.ctx, b.key)
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait blocks on the barrier key until it is deleted. If there is no key, Wait
|
||||
// assumes Release has already been called and returns immediately.
|
||||
func (b *Barrier) Wait() error {
|
||||
resp, err := b.client.Get(b.ctx, b.key, v3.WithFirstKey()...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(resp.Kvs) == 0 {
|
||||
// key already removed
|
||||
return nil
|
||||
}
|
||||
_, err = WaitEvents(
|
||||
b.client,
|
||||
b.key,
|
||||
resp.Header.Revision,
|
||||
[]mvccpb.Event_EventType{mvccpb.PUT, mvccpb.DELETE})
|
||||
return err
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright 2016 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 recipe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
spb "github.com/coreos/etcd/mvcc/mvccpb"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrKeyExists = errors.New("key already exists")
|
||||
ErrWaitMismatch = errors.New("unexpected wait result")
|
||||
ErrTooManyClients = errors.New("too many clients")
|
||||
ErrNoWatcher = errors.New("no watcher channel")
|
||||
)
|
||||
|
||||
// deleteRevKey deletes a key by revision, returning false if key is missing
|
||||
func deleteRevKey(kv v3.KV, key string, rev int64) (bool, error) {
|
||||
cmp := v3.Compare(v3.ModRevision(key), "=", rev)
|
||||
req := v3.OpDelete(key)
|
||||
txnresp, err := kv.Txn(context.TODO()).If(cmp).Then(req).Commit()
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if !txnresp.Succeeded {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func claimFirstKey(kv v3.KV, kvs []*spb.KeyValue) (*spb.KeyValue, error) {
|
||||
for _, k := range kvs {
|
||||
ok, err := deleteRevKey(kv, string(k.Key), k.ModRevision)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if ok {
|
||||
return k, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2017 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 recipe contains experimental client-side distributed
|
||||
// synchronization primitives.
|
||||
package recipe
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
// Copyright 2016 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 recipe
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/clientv3/concurrency"
|
||||
"github.com/coreos/etcd/mvcc/mvccpb"
|
||||
)
|
||||
|
||||
// DoubleBarrier blocks processes on Enter until an expected count enters, then
|
||||
// blocks again on Leave until all processes have left.
|
||||
type DoubleBarrier struct {
|
||||
s *concurrency.Session
|
||||
ctx context.Context
|
||||
|
||||
key string // key for the collective barrier
|
||||
count int
|
||||
myKey *EphemeralKV // current key for this process on the barrier
|
||||
}
|
||||
|
||||
func NewDoubleBarrier(s *concurrency.Session, key string, count int) *DoubleBarrier {
|
||||
return &DoubleBarrier{
|
||||
s: s,
|
||||
ctx: context.TODO(),
|
||||
key: key,
|
||||
count: count,
|
||||
}
|
||||
}
|
||||
|
||||
// Enter waits for "count" processes to enter the barrier then returns
|
||||
func (b *DoubleBarrier) Enter() error {
|
||||
client := b.s.Client()
|
||||
ek, err := newUniqueEphemeralKey(b.s, b.key+"/waiters")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.myKey = ek
|
||||
|
||||
resp, err := client.Get(b.ctx, b.key+"/waiters", clientv3.WithPrefix())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(resp.Kvs) > b.count {
|
||||
return ErrTooManyClients
|
||||
}
|
||||
|
||||
if len(resp.Kvs) == b.count {
|
||||
// unblock waiters
|
||||
_, err = client.Put(b.ctx, b.key+"/ready", "")
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = WaitEvents(
|
||||
client,
|
||||
b.key+"/ready",
|
||||
ek.Revision(),
|
||||
[]mvccpb.Event_EventType{mvccpb.PUT})
|
||||
return err
|
||||
}
|
||||
|
||||
// Leave waits for "count" processes to leave the barrier then returns
|
||||
func (b *DoubleBarrier) Leave() error {
|
||||
client := b.s.Client()
|
||||
resp, err := client.Get(b.ctx, b.key+"/waiters", clientv3.WithPrefix())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(resp.Kvs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
lowest, highest := resp.Kvs[0], resp.Kvs[0]
|
||||
for _, k := range resp.Kvs {
|
||||
if k.ModRevision < lowest.ModRevision {
|
||||
lowest = k
|
||||
}
|
||||
if k.ModRevision > highest.ModRevision {
|
||||
highest = k
|
||||
}
|
||||
}
|
||||
isLowest := string(lowest.Key) == b.myKey.Key()
|
||||
|
||||
if len(resp.Kvs) == 1 {
|
||||
// this is the only node in the barrier; finish up
|
||||
if _, err = client.Delete(b.ctx, b.key+"/ready"); err != nil {
|
||||
return err
|
||||
}
|
||||
return b.myKey.Delete()
|
||||
}
|
||||
|
||||
// this ensures that if a process fails, the ephemeral lease will be
|
||||
// revoked, its barrier key is removed, and the barrier can resume
|
||||
|
||||
// lowest process in node => wait on highest process
|
||||
if isLowest {
|
||||
_, err = WaitEvents(
|
||||
client,
|
||||
string(highest.Key),
|
||||
highest.ModRevision,
|
||||
[]mvccpb.Event_EventType{mvccpb.DELETE})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Leave()
|
||||
}
|
||||
|
||||
// delete self and wait on lowest process
|
||||
if err = b.myKey.Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key := string(lowest.Key)
|
||||
_, err = WaitEvents(
|
||||
client,
|
||||
key,
|
||||
lowest.ModRevision,
|
||||
[]mvccpb.Event_EventType{mvccpb.DELETE})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Leave()
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
// Copyright 2016 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 recipe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/clientv3/concurrency"
|
||||
)
|
||||
|
||||
// RemoteKV is a key/revision pair created by the client and stored on etcd
|
||||
type RemoteKV struct {
|
||||
kv v3.KV
|
||||
key string
|
||||
rev int64
|
||||
val string
|
||||
}
|
||||
|
||||
func newKey(kv v3.KV, key string, leaseID v3.LeaseID) (*RemoteKV, error) {
|
||||
return newKV(kv, key, "", leaseID)
|
||||
}
|
||||
|
||||
func newKV(kv v3.KV, key, val string, leaseID v3.LeaseID) (*RemoteKV, error) {
|
||||
rev, err := putNewKV(kv, key, val, leaseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &RemoteKV{kv, key, rev, val}, nil
|
||||
}
|
||||
|
||||
func newUniqueKV(kv v3.KV, prefix string, val string) (*RemoteKV, error) {
|
||||
for {
|
||||
newKey := fmt.Sprintf("%s/%v", prefix, time.Now().UnixNano())
|
||||
rev, err := putNewKV(kv, newKey, val, 0)
|
||||
if err == nil {
|
||||
return &RemoteKV{kv, newKey, rev, val}, nil
|
||||
}
|
||||
if err != ErrKeyExists {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// putNewKV attempts to create the given key, only succeeding if the key did
|
||||
// not yet exist.
|
||||
func putNewKV(kv v3.KV, key, val string, leaseID v3.LeaseID) (int64, error) {
|
||||
cmp := v3.Compare(v3.Version(key), "=", 0)
|
||||
req := v3.OpPut(key, val, v3.WithLease(leaseID))
|
||||
txnresp, err := kv.Txn(context.TODO()).If(cmp).Then(req).Commit()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !txnresp.Succeeded {
|
||||
return 0, ErrKeyExists
|
||||
}
|
||||
return txnresp.Header.Revision, nil
|
||||
}
|
||||
|
||||
// newSequentialKV allocates a new sequential key <prefix>/nnnnn with a given
|
||||
// prefix and value. Note: a bookkeeping node __<prefix> is also allocated.
|
||||
func newSequentialKV(kv v3.KV, prefix, val string) (*RemoteKV, error) {
|
||||
resp, err := kv.Get(context.TODO(), prefix, v3.WithLastKey()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add 1 to last key, if any
|
||||
newSeqNum := 0
|
||||
if len(resp.Kvs) != 0 {
|
||||
fields := strings.Split(string(resp.Kvs[0].Key), "/")
|
||||
_, serr := fmt.Sscanf(fields[len(fields)-1], "%d", &newSeqNum)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
newSeqNum++
|
||||
}
|
||||
newKey := fmt.Sprintf("%s/%016d", prefix, newSeqNum)
|
||||
|
||||
// base prefix key must be current (i.e., <=) with the server update;
|
||||
// the base key is important to avoid the following:
|
||||
// N1: LastKey() == 1, start txn.
|
||||
// N2: new Key 2, new Key 3, Delete Key 2
|
||||
// N1: txn succeeds allocating key 2 when it shouldn't
|
||||
baseKey := "__" + prefix
|
||||
|
||||
// current revision might contain modification so +1
|
||||
cmp := v3.Compare(v3.ModRevision(baseKey), "<", resp.Header.Revision+1)
|
||||
reqPrefix := v3.OpPut(baseKey, "")
|
||||
reqnewKey := v3.OpPut(newKey, val)
|
||||
|
||||
txn := kv.Txn(context.TODO())
|
||||
txnresp, err := txn.If(cmp).Then(reqPrefix, reqnewKey).Commit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !txnresp.Succeeded {
|
||||
return newSequentialKV(kv, prefix, val)
|
||||
}
|
||||
return &RemoteKV{kv, newKey, txnresp.Header.Revision, val}, nil
|
||||
}
|
||||
|
||||
func (rk *RemoteKV) Key() string { return rk.key }
|
||||
func (rk *RemoteKV) Revision() int64 { return rk.rev }
|
||||
func (rk *RemoteKV) Value() string { return rk.val }
|
||||
|
||||
func (rk *RemoteKV) Delete() error {
|
||||
if rk.kv == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := rk.kv.Delete(context.TODO(), rk.key)
|
||||
rk.kv = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (rk *RemoteKV) Put(val string) error {
|
||||
_, err := rk.kv.Put(context.TODO(), rk.key, val)
|
||||
return err
|
||||
}
|
||||
|
||||
// EphemeralKV is a new key associated with a session lease
|
||||
type EphemeralKV struct{ RemoteKV }
|
||||
|
||||
// newEphemeralKV creates a new key/value pair associated with a session lease
|
||||
func newEphemeralKV(s *concurrency.Session, key, val string) (*EphemeralKV, error) {
|
||||
k, err := newKV(s.Client(), key, val, s.Lease())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EphemeralKV{*k}, nil
|
||||
}
|
||||
|
||||
// newUniqueEphemeralKey creates a new unique valueless key associated with a session lease
|
||||
func newUniqueEphemeralKey(s *concurrency.Session, prefix string) (*EphemeralKV, error) {
|
||||
return newUniqueEphemeralKV(s, prefix, "")
|
||||
}
|
||||
|
||||
// newUniqueEphemeralKV creates a new unique key/value pair associated with a session lease
|
||||
func newUniqueEphemeralKV(s *concurrency.Session, prefix, val string) (ek *EphemeralKV, err error) {
|
||||
for {
|
||||
newKey := fmt.Sprintf("%s/%v", prefix, time.Now().UnixNano())
|
||||
ek, err = newEphemeralKV(s, newKey, val)
|
||||
if err == nil || err != ErrKeyExists {
|
||||
break
|
||||
}
|
||||
}
|
||||
return ek, err
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright 2016 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 recipe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/mvcc/mvccpb"
|
||||
)
|
||||
|
||||
// PriorityQueue implements a multi-reader, multi-writer distributed queue.
|
||||
type PriorityQueue struct {
|
||||
client *v3.Client
|
||||
ctx context.Context
|
||||
key string
|
||||
}
|
||||
|
||||
// NewPriorityQueue creates an etcd priority queue.
|
||||
func NewPriorityQueue(client *v3.Client, key string) *PriorityQueue {
|
||||
return &PriorityQueue{client, context.TODO(), key + "/"}
|
||||
}
|
||||
|
||||
// Enqueue puts a value into a queue with a given priority.
|
||||
func (q *PriorityQueue) Enqueue(val string, pr uint16) error {
|
||||
prefix := fmt.Sprintf("%s%05d", q.key, pr)
|
||||
_, err := newSequentialKV(q.client, prefix, val)
|
||||
return err
|
||||
}
|
||||
|
||||
// Dequeue returns Enqueue()'d items in FIFO order. If the
|
||||
// queue is empty, Dequeue blocks until items are available.
|
||||
func (q *PriorityQueue) Dequeue() (string, error) {
|
||||
// TODO: fewer round trips by fetching more than one key
|
||||
resp, err := q.client.Get(q.ctx, q.key, v3.WithFirstKey()...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
kv, err := claimFirstKey(q.client, resp.Kvs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if kv != nil {
|
||||
return string(kv.Value), nil
|
||||
} else if resp.More {
|
||||
// missed some items, retry to read in more
|
||||
return q.Dequeue()
|
||||
}
|
||||
|
||||
// nothing to dequeue; wait on items
|
||||
ev, err := WaitPrefixEvents(
|
||||
q.client,
|
||||
q.key,
|
||||
resp.Header.Revision,
|
||||
[]mvccpb.Event_EventType{mvccpb.PUT})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ok, err := deleteRevKey(q.client, string(ev.Kv.Key), ev.Kv.ModRevision)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if !ok {
|
||||
return q.Dequeue()
|
||||
}
|
||||
return string(ev.Kv.Value), err
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright 2016 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 recipe
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/mvcc/mvccpb"
|
||||
)
|
||||
|
||||
// Queue implements a multi-reader, multi-writer distributed queue.
|
||||
type Queue struct {
|
||||
client *v3.Client
|
||||
ctx context.Context
|
||||
|
||||
keyPrefix string
|
||||
}
|
||||
|
||||
func NewQueue(client *v3.Client, keyPrefix string) *Queue {
|
||||
return &Queue{client, context.TODO(), keyPrefix}
|
||||
}
|
||||
|
||||
func (q *Queue) Enqueue(val string) error {
|
||||
_, err := newUniqueKV(q.client, q.keyPrefix, val)
|
||||
return err
|
||||
}
|
||||
|
||||
// Dequeue returns Enqueue()'d elements in FIFO order. If the
|
||||
// queue is empty, Dequeue blocks until elements are available.
|
||||
func (q *Queue) Dequeue() (string, error) {
|
||||
// TODO: fewer round trips by fetching more than one key
|
||||
resp, err := q.client.Get(q.ctx, q.keyPrefix, v3.WithFirstRev()...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
kv, err := claimFirstKey(q.client, resp.Kvs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if kv != nil {
|
||||
return string(kv.Value), nil
|
||||
} else if resp.More {
|
||||
// missed some items, retry to read in more
|
||||
return q.Dequeue()
|
||||
}
|
||||
|
||||
// nothing yet; wait on elements
|
||||
ev, err := WaitPrefixEvents(
|
||||
q.client,
|
||||
q.keyPrefix,
|
||||
resp.Header.Revision,
|
||||
[]mvccpb.Event_EventType{mvccpb.PUT})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ok, err := deleteRevKey(q.client, string(ev.Kv.Key), ev.Kv.ModRevision)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if !ok {
|
||||
return q.Dequeue()
|
||||
}
|
||||
return string(ev.Kv.Value), err
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright 2016 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 recipe
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
v3 "github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/clientv3/concurrency"
|
||||
"github.com/coreos/etcd/mvcc/mvccpb"
|
||||
)
|
||||
|
||||
type RWMutex struct {
|
||||
s *concurrency.Session
|
||||
ctx context.Context
|
||||
|
||||
pfx string
|
||||
myKey *EphemeralKV
|
||||
}
|
||||
|
||||
func NewRWMutex(s *concurrency.Session, prefix string) *RWMutex {
|
||||
return &RWMutex{s, context.TODO(), prefix + "/", nil}
|
||||
}
|
||||
|
||||
func (rwm *RWMutex) RLock() error {
|
||||
rk, err := newUniqueEphemeralKey(rwm.s, rwm.pfx+"read")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rwm.myKey = rk
|
||||
// wait until nodes with "write-" and a lower revision number than myKey are gone
|
||||
for {
|
||||
if done, werr := rwm.waitOnLastRev(rwm.pfx + "write"); done || werr != nil {
|
||||
return werr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rwm *RWMutex) Lock() error {
|
||||
rk, err := newUniqueEphemeralKey(rwm.s, rwm.pfx+"write")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rwm.myKey = rk
|
||||
// wait until all keys of lower revision than myKey are gone
|
||||
for {
|
||||
if done, werr := rwm.waitOnLastRev(rwm.pfx); done || werr != nil {
|
||||
return werr
|
||||
}
|
||||
// get the new lowest key until this is the only one left
|
||||
}
|
||||
}
|
||||
|
||||
// waitOnLowest will wait on the last key with a revision < rwm.myKey.Revision with a
|
||||
// given prefix. If there are no keys left to wait on, return true.
|
||||
func (rwm *RWMutex) waitOnLastRev(pfx string) (bool, error) {
|
||||
client := rwm.s.Client()
|
||||
// get key that's blocking myKey
|
||||
opts := append(v3.WithLastRev(), v3.WithMaxModRev(rwm.myKey.Revision()-1))
|
||||
lastKey, err := client.Get(rwm.ctx, pfx, opts...)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(lastKey.Kvs) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
// wait for release on blocking key
|
||||
_, err = WaitEvents(
|
||||
client,
|
||||
string(lastKey.Kvs[0].Key),
|
||||
rwm.myKey.Revision(),
|
||||
[]mvccpb.Event_EventType{mvccpb.DELETE})
|
||||
return false, err
|
||||
}
|
||||
|
||||
func (rwm *RWMutex) RUnlock() error { return rwm.myKey.Delete() }
|
||||
func (rwm *RWMutex) Unlock() error { return rwm.myKey.Delete() }
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright 2016 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 recipe
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/mvcc/mvccpb"
|
||||
)
|
||||
|
||||
// WaitEvents waits on a key until it observes the given events and returns the final one.
|
||||
func WaitEvents(c *clientv3.Client, key string, rev int64, evs []mvccpb.Event_EventType) (*clientv3.Event, error) {
|
||||
wc := c.Watch(context.Background(), key, clientv3.WithRev(rev))
|
||||
if wc == nil {
|
||||
return nil, ErrNoWatcher
|
||||
}
|
||||
return waitEvents(wc, evs), nil
|
||||
}
|
||||
|
||||
func WaitPrefixEvents(c *clientv3.Client, prefix string, rev int64, evs []mvccpb.Event_EventType) (*clientv3.Event, error) {
|
||||
wc := c.Watch(context.Background(), prefix, clientv3.WithPrefix(), clientv3.WithRev(rev))
|
||||
if wc == nil {
|
||||
return nil, ErrNoWatcher
|
||||
}
|
||||
return waitEvents(wc, evs), nil
|
||||
}
|
||||
|
||||
func waitEvents(wc clientv3.WatchChan, evs []mvccpb.Event_EventType) *clientv3.Event {
|
||||
i := 0
|
||||
for wresp := range wc {
|
||||
for _, ev := range wresp.Events {
|
||||
if ev.Type == evs[i] {
|
||||
i++
|
||||
if i == len(evs) {
|
||||
return ev
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
126
vendor/github.com/coreos/etcd/contrib/systemd/etcd2-backup-coreos/etcd2-restore.go
generated
vendored
Normal file
126
vendor/github.com/coreos/etcd/contrib/systemd/etcd2-backup-coreos/etcd2-restore.go
generated
vendored
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
// Copyright 2015 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 main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
etcdctlPath string
|
||||
etcdPath string
|
||||
etcdRestoreDir string
|
||||
etcdName string
|
||||
etcdPeerUrls string
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&etcdctlPath, "etcdctl-path", "/usr/bin/etcdctl", "absolute path to etcdctl executable")
|
||||
flag.StringVar(&etcdPath, "etcd-path", "/usr/bin/etcd2", "absolute path to etcd2 executable")
|
||||
flag.StringVar(&etcdRestoreDir, "etcd-restore-dir", "/var/lib/etcd2-restore", "absolute path to etcd2 restore dir")
|
||||
flag.StringVar(&etcdName, "etcd-name", "default", "name of etcd2 node")
|
||||
flag.StringVar(&etcdPeerUrls, "etcd-peer-urls", "", "advertise peer urls")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if etcdPeerUrls == "" {
|
||||
panic("must set -etcd-peer-urls")
|
||||
}
|
||||
|
||||
if finfo, err := os.Stat(etcdRestoreDir); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
if !finfo.IsDir() {
|
||||
panic(fmt.Errorf("%s is not a directory", etcdRestoreDir))
|
||||
}
|
||||
}
|
||||
|
||||
if !path.IsAbs(etcdctlPath) {
|
||||
panic(fmt.Sprintf("etcdctl-path %s is not absolute", etcdctlPath))
|
||||
}
|
||||
|
||||
if !path.IsAbs(etcdPath) {
|
||||
panic(fmt.Sprintf("etcd-path %s is not absolute", etcdPath))
|
||||
}
|
||||
|
||||
if err := restoreEtcd(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func restoreEtcd() error {
|
||||
etcdCmd := exec.Command(etcdPath, "--force-new-cluster", "--data-dir", etcdRestoreDir)
|
||||
|
||||
etcdCmd.Stdout = os.Stdout
|
||||
etcdCmd.Stderr = os.Stderr
|
||||
|
||||
if err := etcdCmd.Start(); err != nil {
|
||||
return fmt.Errorf("Could not start etcd2: %s", err)
|
||||
}
|
||||
defer etcdCmd.Wait()
|
||||
defer etcdCmd.Process.Kill()
|
||||
|
||||
return runCommands(10, 2*time.Second)
|
||||
}
|
||||
|
||||
var (
|
||||
clusterHealthRegex = regexp.MustCompile(".*cluster is healthy.*")
|
||||
lineSplit = regexp.MustCompile("\n+")
|
||||
colonSplit = regexp.MustCompile(`\:`)
|
||||
)
|
||||
|
||||
func runCommands(maxRetry int, interval time.Duration) error {
|
||||
var retryCnt int
|
||||
for retryCnt = 1; retryCnt <= maxRetry; retryCnt++ {
|
||||
out, err := exec.Command(etcdctlPath, "cluster-health").CombinedOutput()
|
||||
if err == nil && clusterHealthRegex.Match(out) {
|
||||
break
|
||||
}
|
||||
fmt.Printf("Error: %s: %s\n", err, string(out))
|
||||
time.Sleep(interval)
|
||||
}
|
||||
|
||||
if retryCnt > maxRetry {
|
||||
return fmt.Errorf("Timed out waiting for healthy cluster\n")
|
||||
}
|
||||
|
||||
var (
|
||||
memberID string
|
||||
out []byte
|
||||
err error
|
||||
)
|
||||
if out, err = exec.Command(etcdctlPath, "member", "list").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("Error calling member list: %s", err)
|
||||
}
|
||||
members := lineSplit.Split(string(out), 2)
|
||||
if len(members) < 1 {
|
||||
return fmt.Errorf("Could not find a cluster member from: \"%s\"", members)
|
||||
}
|
||||
parts := colonSplit.Split(members[0], 2)
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("Could not parse member id from: \"%s\"", members[0])
|
||||
}
|
||||
memberID = parts[0]
|
||||
|
||||
out, err = exec.Command(etcdctlPath, "member", "update", memberID, etcdPeerUrls).CombinedOutput()
|
||||
fmt.Printf("member update result: %s\n", string(out))
|
||||
return err
|
||||
}
|
||||
|
|
@ -0,0 +1,888 @@
|
|||
// Copyright 2016 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 embed
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3compactor"
|
||||
"github.com/coreos/etcd/pkg/flags"
|
||||
"github.com/coreos/etcd/pkg/netutil"
|
||||
"github.com/coreos/etcd/pkg/srv"
|
||||
"github.com/coreos/etcd/pkg/tlsutil"
|
||||
"github.com/coreos/etcd/pkg/transport"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
ClusterStateFlagNew = "new"
|
||||
ClusterStateFlagExisting = "existing"
|
||||
|
||||
DefaultName = "default"
|
||||
DefaultMaxSnapshots = 5
|
||||
DefaultMaxWALs = 5
|
||||
DefaultMaxTxnOps = uint(128)
|
||||
DefaultMaxRequestBytes = 1.5 * 1024 * 1024
|
||||
DefaultGRPCKeepAliveMinTime = 5 * time.Second
|
||||
DefaultGRPCKeepAliveInterval = 2 * time.Hour
|
||||
DefaultGRPCKeepAliveTimeout = 20 * time.Second
|
||||
|
||||
DefaultListenPeerURLs = "http://localhost:2380"
|
||||
DefaultListenClientURLs = "http://localhost:2379"
|
||||
|
||||
DefaultLogOutput = "default"
|
||||
JournalLogOutput = "systemd/journal"
|
||||
StdErrLogOutput = "stderr"
|
||||
StdOutLogOutput = "stdout"
|
||||
|
||||
// DefaultStrictReconfigCheck is the default value for "--strict-reconfig-check" flag.
|
||||
// It's enabled by default.
|
||||
DefaultStrictReconfigCheck = true
|
||||
// DefaultEnableV2 is the default value for "--enable-v2" flag.
|
||||
// v2 is enabled by default.
|
||||
// TODO: disable v2 when deprecated.
|
||||
DefaultEnableV2 = true
|
||||
|
||||
// maxElectionMs specifies the maximum value of election timeout.
|
||||
// More details are listed in ../Documentation/tuning.md#time-parameters.
|
||||
maxElectionMs = 50000
|
||||
)
|
||||
|
||||
var (
|
||||
ErrConflictBootstrapFlags = fmt.Errorf("multiple discovery or bootstrap flags are set. " +
|
||||
"Choose one of \"initial-cluster\", \"discovery\" or \"discovery-srv\"")
|
||||
ErrUnsetAdvertiseClientURLsFlag = fmt.Errorf("--advertise-client-urls is required when --listen-client-urls is set explicitly")
|
||||
|
||||
DefaultInitialAdvertisePeerURLs = "http://localhost:2380"
|
||||
DefaultAdvertiseClientURLs = "http://localhost:2379"
|
||||
|
||||
defaultHostname string
|
||||
defaultHostStatus error
|
||||
)
|
||||
|
||||
var (
|
||||
// CompactorModePeriodic is periodic compaction mode
|
||||
// for "Config.AutoCompactionMode" field.
|
||||
// If "AutoCompactionMode" is CompactorModePeriodic and
|
||||
// "AutoCompactionRetention" is "1h", it automatically compacts
|
||||
// compacts storage every hour.
|
||||
CompactorModePeriodic = v3compactor.ModePeriodic
|
||||
|
||||
// CompactorModeRevision is revision-based compaction mode
|
||||
// for "Config.AutoCompactionMode" field.
|
||||
// If "AutoCompactionMode" is CompactorModeRevision and
|
||||
// "AutoCompactionRetention" is "1000", it compacts log on
|
||||
// revision 5000 when the current revision is 6000.
|
||||
// This runs every 5-minute if enough of logs have proceeded.
|
||||
CompactorModeRevision = v3compactor.ModeRevision
|
||||
)
|
||||
|
||||
func init() {
|
||||
defaultHostname, defaultHostStatus = netutil.GetDefaultHost()
|
||||
}
|
||||
|
||||
// Config holds the arguments for configuring an etcd server.
|
||||
type Config struct {
|
||||
Name string `json:"name"`
|
||||
Dir string `json:"data-dir"`
|
||||
WalDir string `json:"wal-dir"`
|
||||
|
||||
SnapshotCount uint64 `json:"snapshot-count"`
|
||||
|
||||
// SnapshotCatchUpEntries is the number of entries for a slow follower
|
||||
// to catch-up after compacting the raft storage entries.
|
||||
// We expect the follower has a millisecond level latency with the leader.
|
||||
// The max throughput is around 10K. Keep a 5K entries is enough for helping
|
||||
// follower to catch up.
|
||||
// WARNING: only change this for tests.
|
||||
// Always use "DefaultSnapshotCatchUpEntries"
|
||||
SnapshotCatchUpEntries uint64
|
||||
|
||||
MaxSnapFiles uint `json:"max-snapshots"`
|
||||
MaxWalFiles uint `json:"max-wals"`
|
||||
|
||||
// TickMs is the number of milliseconds between heartbeat ticks.
|
||||
// TODO: decouple tickMs and heartbeat tick (current heartbeat tick = 1).
|
||||
// make ticks a cluster wide configuration.
|
||||
TickMs uint `json:"heartbeat-interval"`
|
||||
ElectionMs uint `json:"election-timeout"`
|
||||
|
||||
// InitialElectionTickAdvance is true, then local member fast-forwards
|
||||
// election ticks to speed up "initial" leader election trigger. This
|
||||
// benefits the case of larger election ticks. For instance, cross
|
||||
// datacenter deployment may require longer election timeout of 10-second.
|
||||
// If true, local node does not need wait up to 10-second. Instead,
|
||||
// forwards its election ticks to 8-second, and have only 2-second left
|
||||
// before leader election.
|
||||
//
|
||||
// Major assumptions are that:
|
||||
// - cluster has no active leader thus advancing ticks enables faster
|
||||
// leader election, or
|
||||
// - cluster already has an established leader, and rejoining follower
|
||||
// is likely to receive heartbeats from the leader after tick advance
|
||||
// and before election timeout.
|
||||
//
|
||||
// However, when network from leader to rejoining follower is congested,
|
||||
// and the follower does not receive leader heartbeat within left election
|
||||
// ticks, disruptive election has to happen thus affecting cluster
|
||||
// availabilities.
|
||||
//
|
||||
// Disabling this would slow down initial bootstrap process for cross
|
||||
// datacenter deployments. Make your own tradeoffs by configuring
|
||||
// --initial-election-tick-advance at the cost of slow initial bootstrap.
|
||||
//
|
||||
// If single-node, it advances ticks regardless.
|
||||
//
|
||||
// See https://github.com/coreos/etcd/issues/9333 for more detail.
|
||||
InitialElectionTickAdvance bool `json:"initial-election-tick-advance"`
|
||||
|
||||
QuotaBackendBytes int64 `json:"quota-backend-bytes"`
|
||||
MaxTxnOps uint `json:"max-txn-ops"`
|
||||
MaxRequestBytes uint `json:"max-request-bytes"`
|
||||
|
||||
LPUrls, LCUrls []url.URL
|
||||
APUrls, ACUrls []url.URL
|
||||
ClientTLSInfo transport.TLSInfo
|
||||
ClientAutoTLS bool
|
||||
PeerTLSInfo transport.TLSInfo
|
||||
PeerAutoTLS bool
|
||||
|
||||
// CipherSuites is a list of supported TLS cipher suites between
|
||||
// client/server and peers. If empty, Go auto-populates the list.
|
||||
// Note that cipher suites are prioritized in the given order.
|
||||
CipherSuites []string `json:"cipher-suites"`
|
||||
|
||||
ClusterState string `json:"initial-cluster-state"`
|
||||
DNSCluster string `json:"discovery-srv"`
|
||||
DNSClusterServiceName string `json:"discovery-srv-name"`
|
||||
Dproxy string `json:"discovery-proxy"`
|
||||
Durl string `json:"discovery"`
|
||||
InitialCluster string `json:"initial-cluster"`
|
||||
InitialClusterToken string `json:"initial-cluster-token"`
|
||||
StrictReconfigCheck bool `json:"strict-reconfig-check"`
|
||||
EnableV2 bool `json:"enable-v2"`
|
||||
|
||||
// AutoCompactionMode is either 'periodic' or 'revision'.
|
||||
AutoCompactionMode string `json:"auto-compaction-mode"`
|
||||
// AutoCompactionRetention is either duration string with time unit
|
||||
// (e.g. '5m' for 5-minute), or revision unit (e.g. '5000').
|
||||
// If no time unit is provided and compaction mode is 'periodic',
|
||||
// the unit defaults to hour. For example, '5' translates into 5-hour.
|
||||
AutoCompactionRetention string `json:"auto-compaction-retention"`
|
||||
|
||||
// GRPCKeepAliveMinTime is the minimum interval that a client should
|
||||
// wait before pinging server. When client pings "too fast", server
|
||||
// sends goaway and closes the connection (errors: too_many_pings,
|
||||
// http2.ErrCodeEnhanceYourCalm). When too slow, nothing happens.
|
||||
// Server expects client pings only when there is any active streams
|
||||
// (PermitWithoutStream is set false).
|
||||
GRPCKeepAliveMinTime time.Duration `json:"grpc-keepalive-min-time"`
|
||||
// GRPCKeepAliveInterval is the frequency of server-to-client ping
|
||||
// to check if a connection is alive. Close a non-responsive connection
|
||||
// after an additional duration of Timeout. 0 to disable.
|
||||
GRPCKeepAliveInterval time.Duration `json:"grpc-keepalive-interval"`
|
||||
// GRPCKeepAliveTimeout is the additional duration of wait
|
||||
// before closing a non-responsive connection. 0 to disable.
|
||||
GRPCKeepAliveTimeout time.Duration `json:"grpc-keepalive-timeout"`
|
||||
|
||||
// PreVote is true to enable Raft Pre-Vote.
|
||||
// If enabled, Raft runs an additional election phase
|
||||
// to check whether it would get enough votes to win
|
||||
// an election, thus minimizing disruptions.
|
||||
// TODO: enable by default in 3.5.
|
||||
PreVote bool `json:"pre-vote"`
|
||||
|
||||
CORS map[string]struct{}
|
||||
|
||||
// HostWhitelist lists acceptable hostnames from HTTP client requests.
|
||||
// Client origin policy protects against "DNS Rebinding" attacks
|
||||
// to insecure etcd servers. That is, any website can simply create
|
||||
// an authorized DNS name, and direct DNS to "localhost" (or any
|
||||
// other address). Then, all HTTP endpoints of etcd server listening
|
||||
// on "localhost" becomes accessible, thus vulnerable to DNS rebinding
|
||||
// attacks. See "CVE-2018-5702" for more detail.
|
||||
//
|
||||
// 1. If client connection is secure via HTTPS, allow any hostnames.
|
||||
// 2. If client connection is not secure and "HostWhitelist" is not empty,
|
||||
// only allow HTTP requests whose Host field is listed in whitelist.
|
||||
//
|
||||
// Note that the client origin policy is enforced whether authentication
|
||||
// is enabled or not, for tighter controls.
|
||||
//
|
||||
// By default, "HostWhitelist" is "*", which allows any hostnames.
|
||||
// Note that when specifying hostnames, loopback addresses are not added
|
||||
// automatically. To allow loopback interfaces, leave it empty or set it "*",
|
||||
// or add them to whitelist manually (e.g. "localhost", "127.0.0.1", etc.).
|
||||
//
|
||||
// CVE-2018-5702 reference:
|
||||
// - https://bugs.chromium.org/p/project-zero/issues/detail?id=1447#c2
|
||||
// - https://github.com/transmission/transmission/pull/468
|
||||
// - https://github.com/coreos/etcd/issues/9353
|
||||
HostWhitelist map[string]struct{}
|
||||
|
||||
// UserHandlers is for registering users handlers and only used for
|
||||
// embedding etcd into other applications.
|
||||
// The map key is the route path for the handler, and
|
||||
// you must ensure it can't be conflicted with etcd's.
|
||||
UserHandlers map[string]http.Handler `json:"-"`
|
||||
// ServiceRegister is for registering users' gRPC services. A simple usage example:
|
||||
// cfg := embed.NewConfig()
|
||||
// cfg.ServerRegister = func(s *grpc.Server) {
|
||||
// pb.RegisterFooServer(s, &fooServer{})
|
||||
// pb.RegisterBarServer(s, &barServer{})
|
||||
// }
|
||||
// embed.StartEtcd(cfg)
|
||||
ServiceRegister func(*grpc.Server) `json:"-"`
|
||||
|
||||
AuthToken string `json:"auth-token"`
|
||||
BcryptCost uint `json:"bcrypt-cost"`
|
||||
|
||||
ExperimentalInitialCorruptCheck bool `json:"experimental-initial-corrupt-check"`
|
||||
ExperimentalCorruptCheckTime time.Duration `json:"experimental-corrupt-check-time"`
|
||||
ExperimentalEnableV2V3 string `json:"experimental-enable-v2v3"`
|
||||
|
||||
// ForceNewCluster starts a new cluster even if previously started; unsafe.
|
||||
ForceNewCluster bool `json:"force-new-cluster"`
|
||||
|
||||
EnablePprof bool `json:"enable-pprof"`
|
||||
Metrics string `json:"metrics"`
|
||||
ListenMetricsUrls []url.URL
|
||||
ListenMetricsUrlsJSON string `json:"listen-metrics-urls"`
|
||||
|
||||
// Logger is logger options: "zap", "capnslog".
|
||||
// WARN: "capnslog" is being deprecated in v3.5.
|
||||
Logger string `json:"logger"`
|
||||
|
||||
// DeprecatedLogOutput is to be deprecated in v3.5.
|
||||
// Just here for safe migration in v3.4.
|
||||
DeprecatedLogOutput []string `json:"log-output"`
|
||||
|
||||
// LogOutputs is either:
|
||||
// - "default" as os.Stderr,
|
||||
// - "stderr" as os.Stderr,
|
||||
// - "stdout" as os.Stdout,
|
||||
// - file path to append server logs to.
|
||||
// It can be multiple when "Logger" is zap.
|
||||
LogOutputs []string `json:"log-outputs"`
|
||||
|
||||
// Debug is true, to enable debug level logging.
|
||||
Debug bool `json:"debug"`
|
||||
|
||||
// logger logs server-side operations. The default is nil,
|
||||
// and "setupLogging" must be called before starting server.
|
||||
// Do not set logger directly.
|
||||
loggerMu *sync.RWMutex
|
||||
logger *zap.Logger
|
||||
|
||||
// loggerConfig is server logger configuration for Raft logger.
|
||||
// Must be either: "loggerConfig != nil" or "loggerCore != nil && loggerWriteSyncer != nil".
|
||||
loggerConfig *zap.Config
|
||||
// loggerCore is "zapcore.Core" for raft logger.
|
||||
// Must be either: "loggerConfig != nil" or "loggerCore != nil && loggerWriteSyncer != nil".
|
||||
loggerCore zapcore.Core
|
||||
loggerWriteSyncer zapcore.WriteSyncer
|
||||
|
||||
// TO BE DEPRECATED
|
||||
|
||||
// LogPkgLevels is being deprecated in v3.5.
|
||||
// Only valid if "logger" option is "capnslog".
|
||||
// WARN: DO NOT USE THIS!
|
||||
LogPkgLevels string `json:"log-package-levels"`
|
||||
}
|
||||
|
||||
// configYAML holds the config suitable for yaml parsing
|
||||
type configYAML struct {
|
||||
Config
|
||||
configJSON
|
||||
}
|
||||
|
||||
// configJSON has file options that are translated into Config options
|
||||
type configJSON struct {
|
||||
LPUrlsJSON string `json:"listen-peer-urls"`
|
||||
LCUrlsJSON string `json:"listen-client-urls"`
|
||||
APUrlsJSON string `json:"initial-advertise-peer-urls"`
|
||||
ACUrlsJSON string `json:"advertise-client-urls"`
|
||||
|
||||
CORSJSON string `json:"cors"`
|
||||
HostWhitelistJSON string `json:"host-whitelist"`
|
||||
|
||||
ClientSecurityJSON securityConfig `json:"client-transport-security"`
|
||||
PeerSecurityJSON securityConfig `json:"peer-transport-security"`
|
||||
}
|
||||
|
||||
type securityConfig struct {
|
||||
CertFile string `json:"cert-file"`
|
||||
KeyFile string `json:"key-file"`
|
||||
CertAuth bool `json:"client-cert-auth"`
|
||||
TrustedCAFile string `json:"trusted-ca-file"`
|
||||
AutoTLS bool `json:"auto-tls"`
|
||||
}
|
||||
|
||||
// NewConfig creates a new Config populated with default values.
|
||||
func NewConfig() *Config {
|
||||
lpurl, _ := url.Parse(DefaultListenPeerURLs)
|
||||
apurl, _ := url.Parse(DefaultInitialAdvertisePeerURLs)
|
||||
lcurl, _ := url.Parse(DefaultListenClientURLs)
|
||||
acurl, _ := url.Parse(DefaultAdvertiseClientURLs)
|
||||
cfg := &Config{
|
||||
MaxSnapFiles: DefaultMaxSnapshots,
|
||||
MaxWalFiles: DefaultMaxWALs,
|
||||
|
||||
Name: DefaultName,
|
||||
|
||||
SnapshotCount: etcdserver.DefaultSnapshotCount,
|
||||
SnapshotCatchUpEntries: etcdserver.DefaultSnapshotCatchUpEntries,
|
||||
|
||||
MaxTxnOps: DefaultMaxTxnOps,
|
||||
MaxRequestBytes: DefaultMaxRequestBytes,
|
||||
|
||||
GRPCKeepAliveMinTime: DefaultGRPCKeepAliveMinTime,
|
||||
GRPCKeepAliveInterval: DefaultGRPCKeepAliveInterval,
|
||||
GRPCKeepAliveTimeout: DefaultGRPCKeepAliveTimeout,
|
||||
|
||||
TickMs: 100,
|
||||
ElectionMs: 1000,
|
||||
InitialElectionTickAdvance: true,
|
||||
|
||||
LPUrls: []url.URL{*lpurl},
|
||||
LCUrls: []url.URL{*lcurl},
|
||||
APUrls: []url.URL{*apurl},
|
||||
ACUrls: []url.URL{*acurl},
|
||||
|
||||
ClusterState: ClusterStateFlagNew,
|
||||
InitialClusterToken: "etcd-cluster",
|
||||
|
||||
StrictReconfigCheck: DefaultStrictReconfigCheck,
|
||||
Metrics: "basic",
|
||||
EnableV2: DefaultEnableV2,
|
||||
|
||||
CORS: map[string]struct{}{"*": {}},
|
||||
HostWhitelist: map[string]struct{}{"*": {}},
|
||||
|
||||
AuthToken: "simple",
|
||||
BcryptCost: uint(bcrypt.DefaultCost),
|
||||
|
||||
PreVote: false, // TODO: enable by default in v3.5
|
||||
|
||||
loggerMu: new(sync.RWMutex),
|
||||
logger: nil,
|
||||
Logger: "capnslog",
|
||||
DeprecatedLogOutput: []string{DefaultLogOutput},
|
||||
LogOutputs: []string{DefaultLogOutput},
|
||||
Debug: false,
|
||||
LogPkgLevels: "",
|
||||
}
|
||||
cfg.InitialCluster = cfg.InitialClusterFromName(cfg.Name)
|
||||
return cfg
|
||||
}
|
||||
|
||||
func logTLSHandshakeFailure(conn *tls.Conn, err error) {
|
||||
state := conn.ConnectionState()
|
||||
remoteAddr := conn.RemoteAddr().String()
|
||||
serverName := state.ServerName
|
||||
if len(state.PeerCertificates) > 0 {
|
||||
cert := state.PeerCertificates[0]
|
||||
ips, dns := cert.IPAddresses, cert.DNSNames
|
||||
plog.Infof("rejected connection from %q (error %q, ServerName %q, IPAddresses %q, DNSNames %q)", remoteAddr, err.Error(), serverName, ips, dns)
|
||||
} else {
|
||||
plog.Infof("rejected connection from %q (error %q, ServerName %q)", remoteAddr, err.Error(), serverName)
|
||||
}
|
||||
}
|
||||
|
||||
func ConfigFromFile(path string) (*Config, error) {
|
||||
cfg := &configYAML{Config: *NewConfig()}
|
||||
if err := cfg.configFromFile(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cfg.Config, nil
|
||||
}
|
||||
|
||||
func (cfg *configYAML) configFromFile(path string) error {
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaultInitialCluster := cfg.InitialCluster
|
||||
|
||||
err = yaml.Unmarshal(b, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cfg.LPUrlsJSON != "" {
|
||||
u, err := types.NewURLs(strings.Split(cfg.LPUrlsJSON, ","))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unexpected error setting up listen-peer-urls: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg.LPUrls = []url.URL(u)
|
||||
}
|
||||
|
||||
if cfg.LCUrlsJSON != "" {
|
||||
u, err := types.NewURLs(strings.Split(cfg.LCUrlsJSON, ","))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unexpected error setting up listen-client-urls: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg.LCUrls = []url.URL(u)
|
||||
}
|
||||
|
||||
if cfg.APUrlsJSON != "" {
|
||||
u, err := types.NewURLs(strings.Split(cfg.APUrlsJSON, ","))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unexpected error setting up initial-advertise-peer-urls: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg.APUrls = []url.URL(u)
|
||||
}
|
||||
|
||||
if cfg.ACUrlsJSON != "" {
|
||||
u, err := types.NewURLs(strings.Split(cfg.ACUrlsJSON, ","))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unexpected error setting up advertise-peer-urls: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg.ACUrls = []url.URL(u)
|
||||
}
|
||||
|
||||
if cfg.ListenMetricsUrlsJSON != "" {
|
||||
u, err := types.NewURLs(strings.Split(cfg.ListenMetricsUrlsJSON, ","))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unexpected error setting up listen-metrics-urls: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg.ListenMetricsUrls = []url.URL(u)
|
||||
}
|
||||
|
||||
if cfg.CORSJSON != "" {
|
||||
uv := flags.NewUniqueURLsWithExceptions(cfg.CORSJSON, "*")
|
||||
cfg.CORS = uv.Values
|
||||
}
|
||||
|
||||
if cfg.HostWhitelistJSON != "" {
|
||||
uv := flags.NewUniqueStringsValue(cfg.HostWhitelistJSON)
|
||||
cfg.HostWhitelist = uv.Values
|
||||
}
|
||||
|
||||
// If a discovery flag is set, clear default initial cluster set by InitialClusterFromName
|
||||
if (cfg.Durl != "" || cfg.DNSCluster != "") && cfg.InitialCluster == defaultInitialCluster {
|
||||
cfg.InitialCluster = ""
|
||||
}
|
||||
if cfg.ClusterState == "" {
|
||||
cfg.ClusterState = ClusterStateFlagNew
|
||||
}
|
||||
|
||||
copySecurityDetails := func(tls *transport.TLSInfo, ysc *securityConfig) {
|
||||
tls.CertFile = ysc.CertFile
|
||||
tls.KeyFile = ysc.KeyFile
|
||||
tls.ClientCertAuth = ysc.CertAuth
|
||||
tls.TrustedCAFile = ysc.TrustedCAFile
|
||||
}
|
||||
copySecurityDetails(&cfg.ClientTLSInfo, &cfg.ClientSecurityJSON)
|
||||
copySecurityDetails(&cfg.PeerTLSInfo, &cfg.PeerSecurityJSON)
|
||||
cfg.ClientAutoTLS = cfg.ClientSecurityJSON.AutoTLS
|
||||
cfg.PeerAutoTLS = cfg.PeerSecurityJSON.AutoTLS
|
||||
|
||||
return cfg.Validate()
|
||||
}
|
||||
|
||||
func updateCipherSuites(tls *transport.TLSInfo, ss []string) error {
|
||||
if len(tls.CipherSuites) > 0 && len(ss) > 0 {
|
||||
return fmt.Errorf("TLSInfo.CipherSuites is already specified (given %v)", ss)
|
||||
}
|
||||
if len(ss) > 0 {
|
||||
cs := make([]uint16, len(ss))
|
||||
for i, s := range ss {
|
||||
var ok bool
|
||||
cs[i], ok = tlsutil.GetCipherSuite(s)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected TLS cipher suite %q", s)
|
||||
}
|
||||
}
|
||||
tls.CipherSuites = cs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate ensures that '*embed.Config' fields are properly configured.
|
||||
func (cfg *Config) Validate() error {
|
||||
if err := cfg.setupLogging(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkBindURLs(cfg.LPUrls); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkBindURLs(cfg.LCUrls); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkBindURLs(cfg.ListenMetricsUrls); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkHostURLs(cfg.APUrls); err != nil {
|
||||
addrs := cfg.getAPURLs()
|
||||
return fmt.Errorf(`--initial-advertise-peer-urls %q must be "host:port" (%v)`, strings.Join(addrs, ","), err)
|
||||
}
|
||||
if err := checkHostURLs(cfg.ACUrls); err != nil {
|
||||
addrs := cfg.getACURLs()
|
||||
return fmt.Errorf(`--advertise-client-urls %q must be "host:port" (%v)`, strings.Join(addrs, ","), err)
|
||||
}
|
||||
// Check if conflicting flags are passed.
|
||||
nSet := 0
|
||||
for _, v := range []bool{cfg.Durl != "", cfg.InitialCluster != "", cfg.DNSCluster != ""} {
|
||||
if v {
|
||||
nSet++
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.ClusterState != ClusterStateFlagNew && cfg.ClusterState != ClusterStateFlagExisting {
|
||||
return fmt.Errorf("unexpected clusterState %q", cfg.ClusterState)
|
||||
}
|
||||
|
||||
if nSet > 1 {
|
||||
return ErrConflictBootstrapFlags
|
||||
}
|
||||
|
||||
if cfg.TickMs <= 0 {
|
||||
return fmt.Errorf("--heartbeat-interval must be >0 (set to %dms)", cfg.TickMs)
|
||||
}
|
||||
if cfg.ElectionMs <= 0 {
|
||||
return fmt.Errorf("--election-timeout must be >0 (set to %dms)", cfg.ElectionMs)
|
||||
}
|
||||
if 5*cfg.TickMs > cfg.ElectionMs {
|
||||
return fmt.Errorf("--election-timeout[%vms] should be at least as 5 times as --heartbeat-interval[%vms]", cfg.ElectionMs, cfg.TickMs)
|
||||
}
|
||||
if cfg.ElectionMs > maxElectionMs {
|
||||
return fmt.Errorf("--election-timeout[%vms] is too long, and should be set less than %vms", cfg.ElectionMs, maxElectionMs)
|
||||
}
|
||||
|
||||
// check this last since proxying in etcdmain may make this OK
|
||||
if cfg.LCUrls != nil && cfg.ACUrls == nil {
|
||||
return ErrUnsetAdvertiseClientURLsFlag
|
||||
}
|
||||
|
||||
switch cfg.AutoCompactionMode {
|
||||
case "":
|
||||
case CompactorModeRevision, CompactorModePeriodic:
|
||||
default:
|
||||
return fmt.Errorf("unknown auto-compaction-mode %q", cfg.AutoCompactionMode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PeerURLsMapAndToken sets up an initial peer URLsMap and cluster token for bootstrap or discovery.
|
||||
func (cfg *Config) PeerURLsMapAndToken(which string) (urlsmap types.URLsMap, token string, err error) {
|
||||
token = cfg.InitialClusterToken
|
||||
switch {
|
||||
case cfg.Durl != "":
|
||||
urlsmap = types.URLsMap{}
|
||||
// If using discovery, generate a temporary cluster based on
|
||||
// self's advertised peer URLs
|
||||
urlsmap[cfg.Name] = cfg.APUrls
|
||||
token = cfg.Durl
|
||||
|
||||
case cfg.DNSCluster != "":
|
||||
clusterStrs, cerr := cfg.GetDNSClusterNames()
|
||||
lg := cfg.logger
|
||||
if cerr != nil {
|
||||
if lg != nil {
|
||||
lg.Warn("failed to resolve during SRV discovery", zap.Error(cerr))
|
||||
} else {
|
||||
plog.Errorf("couldn't resolve during SRV discovery (%v)", cerr)
|
||||
}
|
||||
return nil, "", cerr
|
||||
}
|
||||
for _, s := range clusterStrs {
|
||||
if lg != nil {
|
||||
lg.Info("got bootstrap from DNS for etcd-server", zap.String("node", s))
|
||||
} else {
|
||||
plog.Noticef("got bootstrap from DNS for etcd-server at %s", s)
|
||||
}
|
||||
}
|
||||
clusterStr := strings.Join(clusterStrs, ",")
|
||||
if strings.Contains(clusterStr, "https://") && cfg.PeerTLSInfo.TrustedCAFile == "" {
|
||||
cfg.PeerTLSInfo.ServerName = cfg.DNSCluster
|
||||
}
|
||||
urlsmap, err = types.NewURLsMap(clusterStr)
|
||||
// only etcd member must belong to the discovered cluster.
|
||||
// proxy does not need to belong to the discovered cluster.
|
||||
if which == "etcd" {
|
||||
if _, ok := urlsmap[cfg.Name]; !ok {
|
||||
return nil, "", fmt.Errorf("cannot find local etcd member %q in SRV records", cfg.Name)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
// We're statically configured, and cluster has appropriately been set.
|
||||
urlsmap, err = types.NewURLsMap(cfg.InitialCluster)
|
||||
}
|
||||
return urlsmap, token, err
|
||||
}
|
||||
|
||||
// GetDNSClusterNames uses DNS SRV records to get a list of initial nodes for cluster bootstrapping.
|
||||
func (cfg *Config) GetDNSClusterNames() ([]string, error) {
|
||||
var (
|
||||
clusterStrs []string
|
||||
cerr error
|
||||
serviceNameSuffix string
|
||||
)
|
||||
if cfg.DNSClusterServiceName != "" {
|
||||
serviceNameSuffix = "-" + cfg.DNSClusterServiceName
|
||||
}
|
||||
|
||||
lg := cfg.GetLogger()
|
||||
|
||||
// Use both etcd-server-ssl and etcd-server for discovery.
|
||||
// Combine the results if both are available.
|
||||
clusterStrs, cerr = srv.GetCluster("https", "etcd-server-ssl"+serviceNameSuffix, cfg.Name, cfg.DNSCluster, cfg.APUrls)
|
||||
if cerr != nil {
|
||||
clusterStrs = make([]string, 0)
|
||||
}
|
||||
if lg != nil {
|
||||
lg.Info(
|
||||
"get cluster for etcd-server-ssl SRV",
|
||||
zap.String("service-scheme", "https"),
|
||||
zap.String("service-name", "etcd-server-ssl"+serviceNameSuffix),
|
||||
zap.String("server-name", cfg.Name),
|
||||
zap.String("discovery-srv", cfg.DNSCluster),
|
||||
zap.Strings("advertise-peer-urls", cfg.getAPURLs()),
|
||||
zap.Strings("found-cluster", clusterStrs),
|
||||
zap.Error(cerr),
|
||||
)
|
||||
}
|
||||
|
||||
defaultHTTPClusterStrs, httpCerr := srv.GetCluster("http", "etcd-server"+serviceNameSuffix, cfg.Name, cfg.DNSCluster, cfg.APUrls)
|
||||
if httpCerr != nil {
|
||||
clusterStrs = append(clusterStrs, defaultHTTPClusterStrs...)
|
||||
}
|
||||
if lg != nil {
|
||||
lg.Info(
|
||||
"get cluster for etcd-server SRV",
|
||||
zap.String("service-scheme", "http"),
|
||||
zap.String("service-name", "etcd-server"+serviceNameSuffix),
|
||||
zap.String("server-name", cfg.Name),
|
||||
zap.String("discovery-srv", cfg.DNSCluster),
|
||||
zap.Strings("advertise-peer-urls", cfg.getAPURLs()),
|
||||
zap.Strings("found-cluster", clusterStrs),
|
||||
zap.Error(httpCerr),
|
||||
)
|
||||
}
|
||||
|
||||
return clusterStrs, cerr
|
||||
}
|
||||
|
||||
func (cfg Config) InitialClusterFromName(name string) (ret string) {
|
||||
if len(cfg.APUrls) == 0 {
|
||||
return ""
|
||||
}
|
||||
n := name
|
||||
if name == "" {
|
||||
n = DefaultName
|
||||
}
|
||||
for i := range cfg.APUrls {
|
||||
ret = ret + "," + n + "=" + cfg.APUrls[i].String()
|
||||
}
|
||||
return ret[1:]
|
||||
}
|
||||
|
||||
func (cfg Config) IsNewCluster() bool { return cfg.ClusterState == ClusterStateFlagNew }
|
||||
func (cfg Config) ElectionTicks() int { return int(cfg.ElectionMs / cfg.TickMs) }
|
||||
|
||||
func (cfg Config) defaultPeerHost() bool {
|
||||
return len(cfg.APUrls) == 1 && cfg.APUrls[0].String() == DefaultInitialAdvertisePeerURLs
|
||||
}
|
||||
|
||||
func (cfg Config) defaultClientHost() bool {
|
||||
return len(cfg.ACUrls) == 1 && cfg.ACUrls[0].String() == DefaultAdvertiseClientURLs
|
||||
}
|
||||
|
||||
func (cfg *Config) ClientSelfCert() (err error) {
|
||||
if !cfg.ClientAutoTLS {
|
||||
return nil
|
||||
}
|
||||
if !cfg.ClientTLSInfo.Empty() {
|
||||
if cfg.logger != nil {
|
||||
cfg.logger.Warn("ignoring client auto TLS since certs given")
|
||||
} else {
|
||||
plog.Warningf("ignoring client auto TLS since certs given")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
chosts := make([]string, len(cfg.LCUrls))
|
||||
for i, u := range cfg.LCUrls {
|
||||
chosts[i] = u.Host
|
||||
}
|
||||
cfg.ClientTLSInfo, err = transport.SelfCert(cfg.logger, filepath.Join(cfg.Dir, "fixtures", "client"), chosts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return updateCipherSuites(&cfg.ClientTLSInfo, cfg.CipherSuites)
|
||||
}
|
||||
|
||||
func (cfg *Config) PeerSelfCert() (err error) {
|
||||
if !cfg.PeerAutoTLS {
|
||||
return nil
|
||||
}
|
||||
if !cfg.PeerTLSInfo.Empty() {
|
||||
if cfg.logger != nil {
|
||||
cfg.logger.Warn("ignoring peer auto TLS since certs given")
|
||||
} else {
|
||||
plog.Warningf("ignoring peer auto TLS since certs given")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
phosts := make([]string, len(cfg.LPUrls))
|
||||
for i, u := range cfg.LPUrls {
|
||||
phosts[i] = u.Host
|
||||
}
|
||||
cfg.PeerTLSInfo, err = transport.SelfCert(cfg.logger, filepath.Join(cfg.Dir, "fixtures", "peer"), phosts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return updateCipherSuites(&cfg.PeerTLSInfo, cfg.CipherSuites)
|
||||
}
|
||||
|
||||
// UpdateDefaultClusterFromName updates cluster advertise URLs with, if available, default host,
|
||||
// if advertise URLs are default values(localhost:2379,2380) AND if listen URL is 0.0.0.0.
|
||||
// e.g. advertise peer URL localhost:2380 or listen peer URL 0.0.0.0:2380
|
||||
// then the advertise peer host would be updated with machine's default host,
|
||||
// while keeping the listen URL's port.
|
||||
// User can work around this by explicitly setting URL with 127.0.0.1.
|
||||
// It returns the default hostname, if used, and the error, if any, from getting the machine's default host.
|
||||
// TODO: check whether fields are set instead of whether fields have default value
|
||||
func (cfg *Config) UpdateDefaultClusterFromName(defaultInitialCluster string) (string, error) {
|
||||
if defaultHostname == "" || defaultHostStatus != nil {
|
||||
// update 'initial-cluster' when only the name is specified (e.g. 'etcd --name=abc')
|
||||
if cfg.Name != DefaultName && cfg.InitialCluster == defaultInitialCluster {
|
||||
cfg.InitialCluster = cfg.InitialClusterFromName(cfg.Name)
|
||||
}
|
||||
return "", defaultHostStatus
|
||||
}
|
||||
|
||||
used := false
|
||||
pip, pport := cfg.LPUrls[0].Hostname(), cfg.LPUrls[0].Port()
|
||||
if cfg.defaultPeerHost() && pip == "0.0.0.0" {
|
||||
cfg.APUrls[0] = url.URL{Scheme: cfg.APUrls[0].Scheme, Host: fmt.Sprintf("%s:%s", defaultHostname, pport)}
|
||||
used = true
|
||||
}
|
||||
// update 'initial-cluster' when only the name is specified (e.g. 'etcd --name=abc')
|
||||
if cfg.Name != DefaultName && cfg.InitialCluster == defaultInitialCluster {
|
||||
cfg.InitialCluster = cfg.InitialClusterFromName(cfg.Name)
|
||||
}
|
||||
|
||||
cip, cport := cfg.LCUrls[0].Hostname(), cfg.LCUrls[0].Port()
|
||||
if cfg.defaultClientHost() && cip == "0.0.0.0" {
|
||||
cfg.ACUrls[0] = url.URL{Scheme: cfg.ACUrls[0].Scheme, Host: fmt.Sprintf("%s:%s", defaultHostname, cport)}
|
||||
used = true
|
||||
}
|
||||
dhost := defaultHostname
|
||||
if !used {
|
||||
dhost = ""
|
||||
}
|
||||
return dhost, defaultHostStatus
|
||||
}
|
||||
|
||||
// checkBindURLs returns an error if any URL uses a domain name.
|
||||
func checkBindURLs(urls []url.URL) error {
|
||||
for _, url := range urls {
|
||||
if url.Scheme == "unix" || url.Scheme == "unixs" {
|
||||
continue
|
||||
}
|
||||
host, _, err := net.SplitHostPort(url.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if host == "localhost" {
|
||||
// special case for local address
|
||||
// TODO: support /etc/hosts ?
|
||||
continue
|
||||
}
|
||||
if net.ParseIP(host) == nil {
|
||||
return fmt.Errorf("expected IP in URL for binding (%s)", url.String())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkHostURLs(urls []url.URL) error {
|
||||
for _, url := range urls {
|
||||
host, _, err := net.SplitHostPort(url.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if host == "" {
|
||||
return fmt.Errorf("unexpected empty host (%s)", url.String())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) getAPURLs() (ss []string) {
|
||||
ss = make([]string, len(cfg.APUrls))
|
||||
for i := range cfg.APUrls {
|
||||
ss[i] = cfg.APUrls[i].String()
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
func (cfg *Config) getLPURLs() (ss []string) {
|
||||
ss = make([]string, len(cfg.LPUrls))
|
||||
for i := range cfg.LPUrls {
|
||||
ss[i] = cfg.LPUrls[i].String()
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
func (cfg *Config) getACURLs() (ss []string) {
|
||||
ss = make([]string, len(cfg.ACUrls))
|
||||
for i := range cfg.ACUrls {
|
||||
ss[i] = cfg.ACUrls[i].String()
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
func (cfg *Config) getLCURLs() (ss []string) {
|
||||
ss = make([]string, len(cfg.LCUrls))
|
||||
for i := range cfg.LCUrls {
|
||||
ss[i] = cfg.LCUrls[i].String()
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
func (cfg *Config) getMetricsURLs() (ss []string) {
|
||||
ss = make([]string, len(cfg.ListenMetricsUrls))
|
||||
for i := range cfg.ListenMetricsUrls {
|
||||
ss[i] = cfg.ListenMetricsUrls[i].String()
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
|
@ -0,0 +1,285 @@
|
|||
// Copyright 2018 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 embed
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/coreos/etcd/pkg/logutil"
|
||||
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
)
|
||||
|
||||
// GetLogger returns the logger.
|
||||
func (cfg Config) GetLogger() *zap.Logger {
|
||||
cfg.loggerMu.RLock()
|
||||
l := cfg.logger
|
||||
cfg.loggerMu.RUnlock()
|
||||
return l
|
||||
}
|
||||
|
||||
// for testing
|
||||
var grpcLogOnce = new(sync.Once)
|
||||
|
||||
// setupLogging initializes etcd logging.
|
||||
// Must be called after flag parsing or finishing configuring embed.Config.
|
||||
func (cfg *Config) setupLogging() error {
|
||||
// handle "DeprecatedLogOutput" in v3.4
|
||||
// TODO: remove "DeprecatedLogOutput" in v3.5
|
||||
len1 := len(cfg.DeprecatedLogOutput)
|
||||
len2 := len(cfg.LogOutputs)
|
||||
if len1 != len2 {
|
||||
switch {
|
||||
case len1 > len2: // deprecate "log-output" flag is used
|
||||
fmt.Fprintln(os.Stderr, "'--log-output' flag has been deprecated! Please use '--log-outputs'!")
|
||||
cfg.LogOutputs = cfg.DeprecatedLogOutput
|
||||
case len1 < len2: // "--log-outputs" flag has been set with multiple writers
|
||||
cfg.DeprecatedLogOutput = []string{}
|
||||
}
|
||||
} else {
|
||||
if len1 > 1 {
|
||||
return errors.New("both '--log-output' and '--log-outputs' are set; only set '--log-outputs'")
|
||||
}
|
||||
if len1 < 1 {
|
||||
return errors.New("either '--log-output' or '--log-outputs' flag must be set")
|
||||
}
|
||||
if reflect.DeepEqual(cfg.DeprecatedLogOutput, cfg.LogOutputs) && cfg.DeprecatedLogOutput[0] != DefaultLogOutput {
|
||||
return fmt.Errorf("'--log-output=%q' and '--log-outputs=%q' are incompatible; only set --log-outputs", cfg.DeprecatedLogOutput, cfg.LogOutputs)
|
||||
}
|
||||
if !reflect.DeepEqual(cfg.DeprecatedLogOutput, []string{DefaultLogOutput}) {
|
||||
fmt.Fprintf(os.Stderr, "Deprecated '--log-output' flag is set to %q\n", cfg.DeprecatedLogOutput)
|
||||
fmt.Fprintln(os.Stderr, "Please use '--log-outputs' flag")
|
||||
}
|
||||
}
|
||||
|
||||
switch cfg.Logger {
|
||||
case "capnslog": // TODO: deprecate this in v3.5
|
||||
cfg.ClientTLSInfo.HandshakeFailure = logTLSHandshakeFailure
|
||||
cfg.PeerTLSInfo.HandshakeFailure = logTLSHandshakeFailure
|
||||
|
||||
if cfg.Debug {
|
||||
capnslog.SetGlobalLogLevel(capnslog.DEBUG)
|
||||
grpc.EnableTracing = true
|
||||
// enable info, warning, error
|
||||
grpclog.SetLoggerV2(grpclog.NewLoggerV2(os.Stderr, os.Stderr, os.Stderr))
|
||||
} else {
|
||||
capnslog.SetGlobalLogLevel(capnslog.INFO)
|
||||
// only discard info
|
||||
grpclog.SetLoggerV2(grpclog.NewLoggerV2(ioutil.Discard, os.Stderr, os.Stderr))
|
||||
}
|
||||
|
||||
// TODO: deprecate with "capnslog"
|
||||
if cfg.LogPkgLevels != "" {
|
||||
repoLog := capnslog.MustRepoLogger("github.com/coreos/etcd")
|
||||
settings, err := repoLog.ParseLogLevelConfig(cfg.LogPkgLevels)
|
||||
if err != nil {
|
||||
plog.Warningf("couldn't parse log level string: %s, continuing with default levels", err.Error())
|
||||
return nil
|
||||
}
|
||||
repoLog.SetLogLevel(settings)
|
||||
}
|
||||
|
||||
if len(cfg.LogOutputs) != 1 {
|
||||
fmt.Printf("--logger=capnslog supports only 1 value in '--log-outputs', got %q\n", cfg.LogOutputs)
|
||||
os.Exit(1)
|
||||
}
|
||||
// capnslog initially SetFormatter(NewDefaultFormatter(os.Stderr))
|
||||
// where NewDefaultFormatter returns NewJournaldFormatter when syscall.Getppid() == 1
|
||||
// specify 'stdout' or 'stderr' to skip journald logging even when running under systemd
|
||||
output := cfg.LogOutputs[0]
|
||||
switch output {
|
||||
case StdErrLogOutput:
|
||||
capnslog.SetFormatter(capnslog.NewPrettyFormatter(os.Stderr, cfg.Debug))
|
||||
case StdOutLogOutput:
|
||||
capnslog.SetFormatter(capnslog.NewPrettyFormatter(os.Stdout, cfg.Debug))
|
||||
case DefaultLogOutput:
|
||||
default:
|
||||
plog.Panicf(`unknown log-output %q (only supports %q, %q, %q)`, output, DefaultLogOutput, StdErrLogOutput, StdOutLogOutput)
|
||||
}
|
||||
|
||||
case "zap":
|
||||
if len(cfg.LogOutputs) == 0 {
|
||||
cfg.LogOutputs = []string{DefaultLogOutput}
|
||||
}
|
||||
if len(cfg.LogOutputs) > 1 {
|
||||
for _, v := range cfg.LogOutputs {
|
||||
if v == DefaultLogOutput {
|
||||
panic(fmt.Errorf("multi logoutput for %q is not supported yet", DefaultLogOutput))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use zapcore to support more features?
|
||||
lcfg := zap.Config{
|
||||
Level: zap.NewAtomicLevelAt(zap.InfoLevel),
|
||||
Development: false,
|
||||
Sampling: &zap.SamplingConfig{
|
||||
Initial: 100,
|
||||
Thereafter: 100,
|
||||
},
|
||||
Encoding: "json",
|
||||
EncoderConfig: zap.NewProductionEncoderConfig(),
|
||||
|
||||
OutputPaths: make([]string, 0),
|
||||
ErrorOutputPaths: make([]string, 0),
|
||||
}
|
||||
|
||||
outputPaths, errOutputPaths := make(map[string]struct{}), make(map[string]struct{})
|
||||
isJournal := false
|
||||
for _, v := range cfg.LogOutputs {
|
||||
switch v {
|
||||
case DefaultLogOutput:
|
||||
return errors.New("'--log-outputs=default' is not supported for v3.4 during zap logger migraion (use 'journal', 'stderr', 'stdout', etc.)")
|
||||
|
||||
case JournalLogOutput:
|
||||
isJournal = true
|
||||
|
||||
case StdErrLogOutput:
|
||||
outputPaths[StdErrLogOutput] = struct{}{}
|
||||
errOutputPaths[StdErrLogOutput] = struct{}{}
|
||||
|
||||
case StdOutLogOutput:
|
||||
outputPaths[StdOutLogOutput] = struct{}{}
|
||||
errOutputPaths[StdOutLogOutput] = struct{}{}
|
||||
|
||||
default:
|
||||
outputPaths[v] = struct{}{}
|
||||
errOutputPaths[v] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if !isJournal {
|
||||
for v := range outputPaths {
|
||||
lcfg.OutputPaths = append(lcfg.OutputPaths, v)
|
||||
}
|
||||
for v := range errOutputPaths {
|
||||
lcfg.ErrorOutputPaths = append(lcfg.ErrorOutputPaths, v)
|
||||
}
|
||||
sort.Strings(lcfg.OutputPaths)
|
||||
sort.Strings(lcfg.ErrorOutputPaths)
|
||||
|
||||
if cfg.Debug {
|
||||
lcfg.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
|
||||
grpc.EnableTracing = true
|
||||
}
|
||||
|
||||
var err error
|
||||
cfg.logger, err = lcfg.Build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.loggerConfig = &lcfg
|
||||
cfg.loggerCore = nil
|
||||
cfg.loggerWriteSyncer = nil
|
||||
|
||||
grpcLogOnce.Do(func() {
|
||||
// debug true, enable info, warning, error
|
||||
// debug false, only discard info
|
||||
var gl grpclog.LoggerV2
|
||||
gl, err = logutil.NewGRPCLoggerV2(lcfg)
|
||||
if err == nil {
|
||||
grpclog.SetLoggerV2(gl)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if len(cfg.LogOutputs) > 1 {
|
||||
for _, v := range cfg.LogOutputs {
|
||||
if v != DefaultLogOutput {
|
||||
return fmt.Errorf("running with systemd/journal but other '--log-outputs' values (%q) are configured with 'default'; override 'default' value with something else", cfg.LogOutputs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// use stderr as fallback
|
||||
syncer, lerr := getJournalWriteSyncer()
|
||||
if lerr != nil {
|
||||
return lerr
|
||||
}
|
||||
|
||||
lvl := zap.NewAtomicLevelAt(zap.InfoLevel)
|
||||
if cfg.Debug {
|
||||
lvl = zap.NewAtomicLevelAt(zap.DebugLevel)
|
||||
grpc.EnableTracing = true
|
||||
}
|
||||
|
||||
// WARN: do not change field names in encoder config
|
||||
// journald logging writer assumes field names of "level" and "caller"
|
||||
cr := zapcore.NewCore(
|
||||
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
|
||||
syncer,
|
||||
lvl,
|
||||
)
|
||||
cfg.logger = zap.New(cr, zap.AddCaller(), zap.ErrorOutput(syncer))
|
||||
|
||||
cfg.loggerConfig = nil
|
||||
cfg.loggerCore = cr
|
||||
cfg.loggerWriteSyncer = syncer
|
||||
|
||||
grpcLogOnce.Do(func() {
|
||||
grpclog.SetLoggerV2(logutil.NewGRPCLoggerV2FromZapCore(cr, syncer))
|
||||
})
|
||||
}
|
||||
|
||||
logTLSHandshakeFailure := func(conn *tls.Conn, err error) {
|
||||
state := conn.ConnectionState()
|
||||
remoteAddr := conn.RemoteAddr().String()
|
||||
serverName := state.ServerName
|
||||
if len(state.PeerCertificates) > 0 {
|
||||
cert := state.PeerCertificates[0]
|
||||
ips := make([]string, 0, len(cert.IPAddresses))
|
||||
for i := range cert.IPAddresses {
|
||||
ips[i] = cert.IPAddresses[i].String()
|
||||
}
|
||||
cfg.logger.Warn(
|
||||
"rejected connection",
|
||||
zap.String("remote-addr", remoteAddr),
|
||||
zap.String("server-name", serverName),
|
||||
zap.Strings("ip-addresses", ips),
|
||||
zap.Strings("dns-names", cert.DNSNames),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else {
|
||||
cfg.logger.Warn(
|
||||
"rejected connection",
|
||||
zap.String("remote-addr", remoteAddr),
|
||||
zap.String("server-name", serverName),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
cfg.ClientTLSInfo.HandshakeFailure = logTLSHandshakeFailure
|
||||
cfg.PeerTLSInfo.HandshakeFailure = logTLSHandshakeFailure
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown logger option %q", cfg.Logger)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
35
vendor/github.com/coreos/etcd/embed/config_logging_journal_unix.go
generated
vendored
Normal file
35
vendor/github.com/coreos/etcd/embed/config_logging_journal_unix.go
generated
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright 2018 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.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package embed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/coreos/etcd/pkg/logutil"
|
||||
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// use stderr as fallback
|
||||
func getJournalWriteSyncer() (zapcore.WriteSyncer, error) {
|
||||
jw, err := logutil.NewJournalWriter(os.Stderr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't find journal (%v)", err)
|
||||
}
|
||||
return zapcore.AddSync(jw), nil
|
||||
}
|
||||
27
vendor/github.com/coreos/etcd/embed/config_logging_journal_windows.go
generated
vendored
Normal file
27
vendor/github.com/coreos/etcd/embed/config_logging_journal_windows.go
generated
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright 2018 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.
|
||||
|
||||
// +build windows
|
||||
|
||||
package embed
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
func getJournalWriteSyncer() (zapcore.WriteSyncer, error) {
|
||||
return zapcore.AddSync(os.Stderr), nil
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright 2016 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 embed provides bindings for embedding an etcd server in a program.
|
||||
|
||||
Launch an embedded etcd server using the configuration defaults:
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/embed"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := embed.NewConfig()
|
||||
cfg.Dir = "default.etcd"
|
||||
e, err := embed.StartEtcd(cfg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer e.Close()
|
||||
select {
|
||||
case <-e.Server.ReadyNotify():
|
||||
log.Printf("Server is ready!")
|
||||
case <-time.After(60 * time.Second):
|
||||
e.Server.Stop() // trigger a shutdown
|
||||
log.Printf("Server took too long to start!")
|
||||
}
|
||||
log.Fatal(<-e.Err())
|
||||
}
|
||||
*/
|
||||
package embed
|
||||
|
|
@ -0,0 +1,754 @@
|
|||
// Copyright 2016 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 embed
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
defaultLog "log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/api/etcdhttp"
|
||||
"github.com/coreos/etcd/etcdserver/api/rafthttp"
|
||||
"github.com/coreos/etcd/etcdserver/api/v2http"
|
||||
"github.com/coreos/etcd/etcdserver/api/v2v3"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3client"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc"
|
||||
"github.com/coreos/etcd/pkg/debugutil"
|
||||
runtimeutil "github.com/coreos/etcd/pkg/runtime"
|
||||
"github.com/coreos/etcd/pkg/transport"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"github.com/grpc-ecosystem/go-grpc-prometheus"
|
||||
"github.com/soheilhy/cmux"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
)
|
||||
|
||||
var plog = capnslog.NewPackageLogger("github.com/coreos/etcd", "embed")
|
||||
|
||||
const (
|
||||
// internal fd usage includes disk usage and transport usage.
|
||||
// To read/write snapshot, snap pkg needs 1. In normal case, wal pkg needs
|
||||
// at most 2 to read/lock/write WALs. One case that it needs to 2 is to
|
||||
// read all logs after some snapshot index, which locates at the end of
|
||||
// the second last and the head of the last. For purging, it needs to read
|
||||
// directory, so it needs 1. For fd monitor, it needs 1.
|
||||
// For transport, rafthttp builds two long-polling connections and at most
|
||||
// four temporary connections with each member. There are at most 9 members
|
||||
// in a cluster, so it should reserve 96.
|
||||
// For the safety, we set the total reserved number to 150.
|
||||
reservedInternalFDNum = 150
|
||||
)
|
||||
|
||||
// Etcd contains a running etcd server and its listeners.
|
||||
type Etcd struct {
|
||||
Peers []*peerListener
|
||||
Clients []net.Listener
|
||||
// a map of contexts for the servers that serves client requests.
|
||||
sctxs map[string]*serveCtx
|
||||
metricsListeners []net.Listener
|
||||
|
||||
Server *etcdserver.EtcdServer
|
||||
|
||||
cfg Config
|
||||
stopc chan struct{}
|
||||
errc chan error
|
||||
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
type peerListener struct {
|
||||
net.Listener
|
||||
serve func() error
|
||||
close func(context.Context) error
|
||||
}
|
||||
|
||||
// StartEtcd launches the etcd server and HTTP handlers for client/server communication.
|
||||
// The returned Etcd.Server is not guaranteed to have joined the cluster. Wait
|
||||
// on the Etcd.Server.ReadyNotify() channel to know when it completes and is ready for use.
|
||||
func StartEtcd(inCfg *Config) (e *Etcd, err error) {
|
||||
if err = inCfg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serving := false
|
||||
e = &Etcd{cfg: *inCfg, stopc: make(chan struct{})}
|
||||
cfg := &e.cfg
|
||||
defer func() {
|
||||
if e == nil || err == nil {
|
||||
return
|
||||
}
|
||||
if !serving {
|
||||
// errored before starting gRPC server for serveCtx.serversC
|
||||
for _, sctx := range e.sctxs {
|
||||
close(sctx.serversC)
|
||||
}
|
||||
}
|
||||
e.Close()
|
||||
e = nil
|
||||
}()
|
||||
|
||||
if e.cfg.logger != nil {
|
||||
e.cfg.logger.Info(
|
||||
"configuring peer listeners",
|
||||
zap.Strings("listen-peer-urls", e.cfg.getLPURLs()),
|
||||
)
|
||||
}
|
||||
if e.Peers, err = configurePeerListeners(cfg); err != nil {
|
||||
return e, err
|
||||
}
|
||||
|
||||
if e.cfg.logger != nil {
|
||||
e.cfg.logger.Info(
|
||||
"configuring client listeners",
|
||||
zap.Strings("listen-client-urls", e.cfg.getLCURLs()),
|
||||
)
|
||||
}
|
||||
if e.sctxs, err = configureClientListeners(cfg); err != nil {
|
||||
return e, err
|
||||
}
|
||||
|
||||
for _, sctx := range e.sctxs {
|
||||
e.Clients = append(e.Clients, sctx.l)
|
||||
}
|
||||
|
||||
var (
|
||||
urlsmap types.URLsMap
|
||||
token string
|
||||
)
|
||||
memberInitialized := true
|
||||
if !isMemberInitialized(cfg) {
|
||||
memberInitialized = false
|
||||
urlsmap, token, err = cfg.PeerURLsMapAndToken("etcd")
|
||||
if err != nil {
|
||||
return e, fmt.Errorf("error setting up initial cluster: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// AutoCompactionRetention defaults to "0" if not set.
|
||||
if len(cfg.AutoCompactionRetention) == 0 {
|
||||
cfg.AutoCompactionRetention = "0"
|
||||
}
|
||||
autoCompactionRetention, err := parseCompactionRetention(cfg.AutoCompactionMode, cfg.AutoCompactionRetention)
|
||||
if err != nil {
|
||||
return e, err
|
||||
}
|
||||
|
||||
srvcfg := etcdserver.ServerConfig{
|
||||
Name: cfg.Name,
|
||||
ClientURLs: cfg.ACUrls,
|
||||
PeerURLs: cfg.APUrls,
|
||||
DataDir: cfg.Dir,
|
||||
DedicatedWALDir: cfg.WalDir,
|
||||
SnapshotCount: cfg.SnapshotCount,
|
||||
MaxSnapFiles: cfg.MaxSnapFiles,
|
||||
MaxWALFiles: cfg.MaxWalFiles,
|
||||
InitialPeerURLsMap: urlsmap,
|
||||
InitialClusterToken: token,
|
||||
DiscoveryURL: cfg.Durl,
|
||||
DiscoveryProxy: cfg.Dproxy,
|
||||
NewCluster: cfg.IsNewCluster(),
|
||||
PeerTLSInfo: cfg.PeerTLSInfo,
|
||||
TickMs: cfg.TickMs,
|
||||
ElectionTicks: cfg.ElectionTicks(),
|
||||
InitialElectionTickAdvance: cfg.InitialElectionTickAdvance,
|
||||
AutoCompactionRetention: autoCompactionRetention,
|
||||
AutoCompactionMode: cfg.AutoCompactionMode,
|
||||
QuotaBackendBytes: cfg.QuotaBackendBytes,
|
||||
MaxTxnOps: cfg.MaxTxnOps,
|
||||
MaxRequestBytes: cfg.MaxRequestBytes,
|
||||
StrictReconfigCheck: cfg.StrictReconfigCheck,
|
||||
ClientCertAuthEnabled: cfg.ClientTLSInfo.ClientCertAuth,
|
||||
AuthToken: cfg.AuthToken,
|
||||
BcryptCost: cfg.BcryptCost,
|
||||
CORS: cfg.CORS,
|
||||
HostWhitelist: cfg.HostWhitelist,
|
||||
InitialCorruptCheck: cfg.ExperimentalInitialCorruptCheck,
|
||||
CorruptCheckTime: cfg.ExperimentalCorruptCheckTime,
|
||||
PreVote: cfg.PreVote,
|
||||
Logger: cfg.logger,
|
||||
LoggerConfig: cfg.loggerConfig,
|
||||
LoggerCore: cfg.loggerCore,
|
||||
LoggerWriteSyncer: cfg.loggerWriteSyncer,
|
||||
Debug: cfg.Debug,
|
||||
ForceNewCluster: cfg.ForceNewCluster,
|
||||
}
|
||||
if e.Server, err = etcdserver.NewServer(srvcfg); err != nil {
|
||||
return e, err
|
||||
}
|
||||
|
||||
if len(e.cfg.CORS) > 0 {
|
||||
ss := make([]string, 0, len(e.cfg.CORS))
|
||||
for v := range e.cfg.CORS {
|
||||
ss = append(ss, v)
|
||||
}
|
||||
sort.Strings(ss)
|
||||
if e.cfg.logger != nil {
|
||||
e.cfg.logger.Info("configured CORS", zap.Strings("cors", ss))
|
||||
} else {
|
||||
plog.Infof("%s starting with cors %q", e.Server.ID(), ss)
|
||||
}
|
||||
}
|
||||
if len(e.cfg.HostWhitelist) > 0 {
|
||||
ss := make([]string, 0, len(e.cfg.HostWhitelist))
|
||||
for v := range e.cfg.HostWhitelist {
|
||||
ss = append(ss, v)
|
||||
}
|
||||
sort.Strings(ss)
|
||||
if e.cfg.logger != nil {
|
||||
e.cfg.logger.Info("configured host whitelist", zap.Strings("hosts", ss))
|
||||
} else {
|
||||
plog.Infof("%s starting with host whitelist %q", e.Server.ID(), ss)
|
||||
}
|
||||
}
|
||||
|
||||
// buffer channel so goroutines on closed connections won't wait forever
|
||||
e.errc = make(chan error, len(e.Peers)+len(e.Clients)+2*len(e.sctxs))
|
||||
|
||||
// newly started member ("memberInitialized==false")
|
||||
// does not need corruption check
|
||||
if memberInitialized {
|
||||
if err = e.Server.CheckInitialHashKV(); err != nil {
|
||||
// set "EtcdServer" to nil, so that it does not block on "EtcdServer.Close()"
|
||||
// (nothing to close since rafthttp transports have not been started)
|
||||
e.Server = nil
|
||||
return e, err
|
||||
}
|
||||
}
|
||||
e.Server.Start()
|
||||
|
||||
if err = e.servePeers(); err != nil {
|
||||
return e, err
|
||||
}
|
||||
if err = e.serveClients(); err != nil {
|
||||
return e, err
|
||||
}
|
||||
if err = e.serveMetrics(); err != nil {
|
||||
return e, err
|
||||
}
|
||||
|
||||
if e.cfg.logger != nil {
|
||||
e.cfg.logger.Info(
|
||||
"now serving peer/client/metrics",
|
||||
zap.String("local-member-id", e.Server.ID().String()),
|
||||
zap.Strings("initial-advertise-peer-urls", e.cfg.getAPURLs()),
|
||||
zap.Strings("listen-peer-urls", e.cfg.getLPURLs()),
|
||||
zap.Strings("advertise-client-urls", e.cfg.getACURLs()),
|
||||
zap.Strings("listen-client-urls", e.cfg.getLCURLs()),
|
||||
zap.Strings("listen-metrics-urls", e.cfg.getMetricsURLs()),
|
||||
)
|
||||
}
|
||||
serving = true
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Config returns the current configuration.
|
||||
func (e *Etcd) Config() Config {
|
||||
return e.cfg
|
||||
}
|
||||
|
||||
// Close gracefully shuts down all servers/listeners.
|
||||
// Client requests will be terminated with request timeout.
|
||||
// After timeout, enforce remaning requests be closed immediately.
|
||||
func (e *Etcd) Close() {
|
||||
fields := []zap.Field{
|
||||
zap.String("name", e.cfg.Name),
|
||||
zap.String("data-dir", e.cfg.Dir),
|
||||
zap.Strings("advertise-peer-urls", e.cfg.getAPURLs()),
|
||||
zap.Strings("advertise-client-urls", e.cfg.getACURLs()),
|
||||
}
|
||||
lg := e.GetLogger()
|
||||
if lg != nil {
|
||||
lg.Info("closing etcd server", fields...)
|
||||
}
|
||||
defer func() {
|
||||
if lg != nil {
|
||||
lg.Info("closed etcd server", fields...)
|
||||
lg.Sync()
|
||||
}
|
||||
}()
|
||||
|
||||
e.closeOnce.Do(func() { close(e.stopc) })
|
||||
|
||||
// close client requests with request timeout
|
||||
timeout := 2 * time.Second
|
||||
if e.Server != nil {
|
||||
timeout = e.Server.Cfg.ReqTimeout()
|
||||
}
|
||||
for _, sctx := range e.sctxs {
|
||||
for ss := range sctx.serversC {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
stopServers(ctx, ss)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
for _, sctx := range e.sctxs {
|
||||
sctx.cancel()
|
||||
}
|
||||
|
||||
for i := range e.Clients {
|
||||
if e.Clients[i] != nil {
|
||||
e.Clients[i].Close()
|
||||
}
|
||||
}
|
||||
|
||||
for i := range e.metricsListeners {
|
||||
e.metricsListeners[i].Close()
|
||||
}
|
||||
|
||||
// close rafthttp transports
|
||||
if e.Server != nil {
|
||||
e.Server.Stop()
|
||||
}
|
||||
|
||||
// close all idle connections in peer handler (wait up to 1-second)
|
||||
for i := range e.Peers {
|
||||
if e.Peers[i] != nil && e.Peers[i].close != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
e.Peers[i].close(ctx)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopServers(ctx context.Context, ss *servers) {
|
||||
shutdownNow := func() {
|
||||
// first, close the http.Server
|
||||
ss.http.Shutdown(ctx)
|
||||
// then close grpc.Server; cancels all active RPCs
|
||||
ss.grpc.Stop()
|
||||
}
|
||||
|
||||
// do not grpc.Server.GracefulStop with TLS enabled etcd server
|
||||
// See https://github.com/grpc/grpc-go/issues/1384#issuecomment-317124531
|
||||
// and https://github.com/coreos/etcd/issues/8916
|
||||
if ss.secure {
|
||||
shutdownNow()
|
||||
return
|
||||
}
|
||||
|
||||
ch := make(chan struct{})
|
||||
go func() {
|
||||
defer close(ch)
|
||||
// close listeners to stop accepting new connections,
|
||||
// will block on any existing transports
|
||||
ss.grpc.GracefulStop()
|
||||
}()
|
||||
|
||||
// wait until all pending RPCs are finished
|
||||
select {
|
||||
case <-ch:
|
||||
case <-ctx.Done():
|
||||
// took too long, manually close open transports
|
||||
// e.g. watch streams
|
||||
shutdownNow()
|
||||
|
||||
// concurrent GracefulStop should be interrupted
|
||||
<-ch
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Etcd) Err() <-chan error { return e.errc }
|
||||
|
||||
func configurePeerListeners(cfg *Config) (peers []*peerListener, err error) {
|
||||
if err = updateCipherSuites(&cfg.PeerTLSInfo, cfg.CipherSuites); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = cfg.PeerSelfCert(); err != nil {
|
||||
if cfg.logger != nil {
|
||||
cfg.logger.Fatal("failed to get peer self-signed certs", zap.Error(err))
|
||||
} else {
|
||||
plog.Fatalf("could not get certs (%v)", err)
|
||||
}
|
||||
}
|
||||
if !cfg.PeerTLSInfo.Empty() {
|
||||
if cfg.logger != nil {
|
||||
cfg.logger.Info(
|
||||
"starting with peer TLS",
|
||||
zap.String("tls-info", fmt.Sprintf("%+v", cfg.PeerTLSInfo)),
|
||||
zap.Strings("cipher-suites", cfg.CipherSuites),
|
||||
)
|
||||
} else {
|
||||
plog.Infof("peerTLS: %s", cfg.PeerTLSInfo)
|
||||
}
|
||||
}
|
||||
|
||||
peers = make([]*peerListener, len(cfg.LPUrls))
|
||||
defer func() {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
for i := range peers {
|
||||
if peers[i] != nil && peers[i].close != nil {
|
||||
if cfg.logger != nil {
|
||||
cfg.logger.Warn(
|
||||
"closing peer listener",
|
||||
zap.String("address", cfg.LPUrls[i].String()),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else {
|
||||
plog.Info("stopping listening for peers on ", cfg.LPUrls[i].String())
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
peers[i].close(ctx)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for i, u := range cfg.LPUrls {
|
||||
if u.Scheme == "http" {
|
||||
if !cfg.PeerTLSInfo.Empty() {
|
||||
if cfg.logger != nil {
|
||||
cfg.logger.Warn("scheme is HTTP while key and cert files are present; ignoring key and cert files", zap.String("peer-url", u.String()))
|
||||
} else {
|
||||
plog.Warningf("The scheme of peer url %s is HTTP while peer key/cert files are presented. Ignored peer key/cert files.", u.String())
|
||||
}
|
||||
}
|
||||
if cfg.PeerTLSInfo.ClientCertAuth {
|
||||
if cfg.logger != nil {
|
||||
cfg.logger.Warn("scheme is HTTP while --peer-client-cert-auth is enabled; ignoring client cert auth for this URL", zap.String("peer-url", u.String()))
|
||||
} else {
|
||||
plog.Warningf("The scheme of peer url %s is HTTP while client cert auth (--peer-client-cert-auth) is enabled. Ignored client cert auth for this url.", u.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
peers[i] = &peerListener{close: func(context.Context) error { return nil }}
|
||||
peers[i].Listener, err = rafthttp.NewListener(u, &cfg.PeerTLSInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// once serve, overwrite with 'http.Server.Shutdown'
|
||||
peers[i].close = func(context.Context) error {
|
||||
return peers[i].Listener.Close()
|
||||
}
|
||||
}
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
// configure peer handlers after rafthttp.Transport started
|
||||
func (e *Etcd) servePeers() (err error) {
|
||||
ph := etcdhttp.NewPeerHandler(e.GetLogger(), e.Server)
|
||||
var peerTLScfg *tls.Config
|
||||
if !e.cfg.PeerTLSInfo.Empty() {
|
||||
if peerTLScfg, err = e.cfg.PeerTLSInfo.ServerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range e.Peers {
|
||||
u := p.Listener.Addr().String()
|
||||
gs := v3rpc.Server(e.Server, peerTLScfg)
|
||||
m := cmux.New(p.Listener)
|
||||
go gs.Serve(m.Match(cmux.HTTP2()))
|
||||
srv := &http.Server{
|
||||
Handler: grpcHandlerFunc(gs, ph),
|
||||
ReadTimeout: 5 * time.Minute,
|
||||
ErrorLog: defaultLog.New(ioutil.Discard, "", 0), // do not log user error
|
||||
}
|
||||
go srv.Serve(m.Match(cmux.Any()))
|
||||
p.serve = func() error { return m.Serve() }
|
||||
p.close = func(ctx context.Context) error {
|
||||
// gracefully shutdown http.Server
|
||||
// close open listeners, idle connections
|
||||
// until context cancel or time-out
|
||||
if e.cfg.logger != nil {
|
||||
e.cfg.logger.Info(
|
||||
"stopping serving peer traffic",
|
||||
zap.String("address", u),
|
||||
)
|
||||
}
|
||||
stopServers(ctx, &servers{secure: peerTLScfg != nil, grpc: gs, http: srv})
|
||||
if e.cfg.logger != nil {
|
||||
e.cfg.logger.Info(
|
||||
"stopped serving peer traffic",
|
||||
zap.String("address", u),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// start peer servers in a goroutine
|
||||
for _, pl := range e.Peers {
|
||||
go func(l *peerListener) {
|
||||
u := l.Addr().String()
|
||||
if e.cfg.logger != nil {
|
||||
e.cfg.logger.Info(
|
||||
"serving peer traffic",
|
||||
zap.String("address", u),
|
||||
)
|
||||
} else {
|
||||
plog.Info("listening for peers on ", u)
|
||||
}
|
||||
e.errHandler(l.serve())
|
||||
}(pl)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func configureClientListeners(cfg *Config) (sctxs map[string]*serveCtx, err error) {
|
||||
if err = updateCipherSuites(&cfg.ClientTLSInfo, cfg.CipherSuites); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = cfg.ClientSelfCert(); err != nil {
|
||||
if cfg.logger != nil {
|
||||
cfg.logger.Fatal("failed to get client self-signed certs", zap.Error(err))
|
||||
} else {
|
||||
plog.Fatalf("could not get certs (%v)", err)
|
||||
}
|
||||
}
|
||||
if cfg.EnablePprof {
|
||||
if cfg.logger != nil {
|
||||
cfg.logger.Info("pprof is enabled", zap.String("path", debugutil.HTTPPrefixPProf))
|
||||
} else {
|
||||
plog.Infof("pprof is enabled under %s", debugutil.HTTPPrefixPProf)
|
||||
}
|
||||
}
|
||||
|
||||
sctxs = make(map[string]*serveCtx)
|
||||
for _, u := range cfg.LCUrls {
|
||||
sctx := newServeCtx(cfg.logger)
|
||||
if u.Scheme == "http" || u.Scheme == "unix" {
|
||||
if !cfg.ClientTLSInfo.Empty() {
|
||||
if cfg.logger != nil {
|
||||
cfg.logger.Warn("scheme is HTTP while key and cert files are present; ignoring key and cert files", zap.String("client-url", u.String()))
|
||||
} else {
|
||||
plog.Warningf("The scheme of client url %s is HTTP while peer key/cert files are presented. Ignored key/cert files.", u.String())
|
||||
}
|
||||
}
|
||||
if cfg.ClientTLSInfo.ClientCertAuth {
|
||||
if cfg.logger != nil {
|
||||
cfg.logger.Warn("scheme is HTTP while --client-cert-auth is enabled; ignoring client cert auth for this URL", zap.String("client-url", u.String()))
|
||||
} else {
|
||||
plog.Warningf("The scheme of client url %s is HTTP while client cert auth (--client-cert-auth) is enabled. Ignored client cert auth for this url.", u.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
if (u.Scheme == "https" || u.Scheme == "unixs") && cfg.ClientTLSInfo.Empty() {
|
||||
return nil, fmt.Errorf("TLS key/cert (--cert-file, --key-file) must be provided for client url %s with HTTPs scheme", u.String())
|
||||
}
|
||||
|
||||
network := "tcp"
|
||||
addr := u.Host
|
||||
if u.Scheme == "unix" || u.Scheme == "unixs" {
|
||||
network = "unix"
|
||||
addr = u.Host + u.Path
|
||||
}
|
||||
sctx.network = network
|
||||
|
||||
sctx.secure = u.Scheme == "https" || u.Scheme == "unixs"
|
||||
sctx.insecure = !sctx.secure
|
||||
if oldctx := sctxs[addr]; oldctx != nil {
|
||||
oldctx.secure = oldctx.secure || sctx.secure
|
||||
oldctx.insecure = oldctx.insecure || sctx.insecure
|
||||
continue
|
||||
}
|
||||
|
||||
if sctx.l, err = net.Listen(network, addr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// net.Listener will rewrite ipv4 0.0.0.0 to ipv6 [::], breaking
|
||||
// hosts that disable ipv6. So, use the address given by the user.
|
||||
sctx.addr = addr
|
||||
|
||||
if fdLimit, fderr := runtimeutil.FDLimit(); fderr == nil {
|
||||
if fdLimit <= reservedInternalFDNum {
|
||||
if cfg.logger != nil {
|
||||
cfg.logger.Fatal(
|
||||
"file descriptor limit of etcd process is too low; please set higher",
|
||||
zap.Uint64("limit", fdLimit),
|
||||
zap.Int("recommended-limit", reservedInternalFDNum),
|
||||
)
|
||||
} else {
|
||||
plog.Fatalf("file descriptor limit[%d] of etcd process is too low, and should be set higher than %d to ensure internal usage", fdLimit, reservedInternalFDNum)
|
||||
}
|
||||
}
|
||||
sctx.l = transport.LimitListener(sctx.l, int(fdLimit-reservedInternalFDNum))
|
||||
}
|
||||
|
||||
if network == "tcp" {
|
||||
if sctx.l, err = transport.NewKeepAliveListener(sctx.l, network, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
sctx.l.Close()
|
||||
if cfg.logger != nil {
|
||||
cfg.logger.Warn(
|
||||
"closing peer listener",
|
||||
zap.String("address", u.Host),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else {
|
||||
plog.Info("stopping listening for client requests on ", u.Host)
|
||||
}
|
||||
}()
|
||||
for k := range cfg.UserHandlers {
|
||||
sctx.userHandlers[k] = cfg.UserHandlers[k]
|
||||
}
|
||||
sctx.serviceRegister = cfg.ServiceRegister
|
||||
if cfg.EnablePprof || cfg.Debug {
|
||||
sctx.registerPprof()
|
||||
}
|
||||
if cfg.Debug {
|
||||
sctx.registerTrace()
|
||||
}
|
||||
sctxs[addr] = sctx
|
||||
}
|
||||
return sctxs, nil
|
||||
}
|
||||
|
||||
func (e *Etcd) serveClients() (err error) {
|
||||
if !e.cfg.ClientTLSInfo.Empty() {
|
||||
if e.cfg.logger != nil {
|
||||
e.cfg.logger.Info(
|
||||
"starting with client TLS",
|
||||
zap.String("tls-info", fmt.Sprintf("%+v", e.cfg.ClientTLSInfo)),
|
||||
zap.Strings("cipher-suites", e.cfg.CipherSuites),
|
||||
)
|
||||
} else {
|
||||
plog.Infof("ClientTLS: %s", e.cfg.ClientTLSInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// Start a client server goroutine for each listen address
|
||||
var h http.Handler
|
||||
if e.Config().EnableV2 {
|
||||
if len(e.Config().ExperimentalEnableV2V3) > 0 {
|
||||
srv := v2v3.NewServer(e.cfg.logger, v3client.New(e.Server), e.cfg.ExperimentalEnableV2V3)
|
||||
h = v2http.NewClientHandler(e.GetLogger(), srv, e.Server.Cfg.ReqTimeout())
|
||||
} else {
|
||||
h = v2http.NewClientHandler(e.GetLogger(), e.Server, e.Server.Cfg.ReqTimeout())
|
||||
}
|
||||
} else {
|
||||
mux := http.NewServeMux()
|
||||
etcdhttp.HandleBasic(mux, e.Server)
|
||||
h = mux
|
||||
}
|
||||
|
||||
gopts := []grpc.ServerOption{}
|
||||
if e.cfg.GRPCKeepAliveMinTime > time.Duration(0) {
|
||||
gopts = append(gopts, grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
|
||||
MinTime: e.cfg.GRPCKeepAliveMinTime,
|
||||
PermitWithoutStream: false,
|
||||
}))
|
||||
}
|
||||
if e.cfg.GRPCKeepAliveInterval > time.Duration(0) &&
|
||||
e.cfg.GRPCKeepAliveTimeout > time.Duration(0) {
|
||||
gopts = append(gopts, grpc.KeepaliveParams(keepalive.ServerParameters{
|
||||
Time: e.cfg.GRPCKeepAliveInterval,
|
||||
Timeout: e.cfg.GRPCKeepAliveTimeout,
|
||||
}))
|
||||
}
|
||||
|
||||
// start client servers in a goroutine
|
||||
for _, sctx := range e.sctxs {
|
||||
go func(s *serveCtx) {
|
||||
e.errHandler(s.serve(e.Server, &e.cfg.ClientTLSInfo, h, e.errHandler, gopts...))
|
||||
}(sctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Etcd) serveMetrics() (err error) {
|
||||
if e.cfg.Metrics == "extensive" {
|
||||
grpc_prometheus.EnableHandlingTimeHistogram()
|
||||
}
|
||||
|
||||
if len(e.cfg.ListenMetricsUrls) > 0 {
|
||||
metricsMux := http.NewServeMux()
|
||||
etcdhttp.HandleMetricsHealth(metricsMux, e.Server)
|
||||
|
||||
for _, murl := range e.cfg.ListenMetricsUrls {
|
||||
tlsInfo := &e.cfg.ClientTLSInfo
|
||||
if murl.Scheme == "http" {
|
||||
tlsInfo = nil
|
||||
}
|
||||
ml, err := transport.NewListener(murl.Host, murl.Scheme, tlsInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.metricsListeners = append(e.metricsListeners, ml)
|
||||
go func(u url.URL, ln net.Listener) {
|
||||
if e.cfg.logger != nil {
|
||||
e.cfg.logger.Info(
|
||||
"serving metrics",
|
||||
zap.String("address", u.String()),
|
||||
)
|
||||
} else {
|
||||
plog.Info("listening for metrics on ", u.String())
|
||||
}
|
||||
e.errHandler(http.Serve(ln, metricsMux))
|
||||
}(murl, ml)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Etcd) errHandler(err error) {
|
||||
select {
|
||||
case <-e.stopc:
|
||||
return
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case <-e.stopc:
|
||||
case e.errc <- err:
|
||||
}
|
||||
}
|
||||
|
||||
// GetLogger returns the logger.
|
||||
func (e *Etcd) GetLogger() *zap.Logger {
|
||||
e.cfg.loggerMu.RLock()
|
||||
l := e.cfg.logger
|
||||
e.cfg.loggerMu.RUnlock()
|
||||
return l
|
||||
}
|
||||
|
||||
func parseCompactionRetention(mode, retention string) (ret time.Duration, err error) {
|
||||
h, err := strconv.Atoi(retention)
|
||||
if err == nil {
|
||||
switch mode {
|
||||
case CompactorModeRevision:
|
||||
ret = time.Duration(int64(h))
|
||||
case CompactorModePeriodic:
|
||||
ret = time.Duration(int64(h)) * time.Hour
|
||||
}
|
||||
} else {
|
||||
// periodic compaction
|
||||
ret, err = time.ParseDuration(retention)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error parsing CompactionRetention: %v", err)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,418 @@
|
|||
// Copyright 2015 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 embed
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
defaultLog "log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3client"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3election"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3election/v3electionpb"
|
||||
v3electiongw "github.com/coreos/etcd/etcdserver/api/v3election/v3electionpb/gw"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3lock"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3lock/v3lockpb"
|
||||
v3lockgw "github.com/coreos/etcd/etcdserver/api/v3lock/v3lockpb/gw"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc"
|
||||
etcdservergw "github.com/coreos/etcd/etcdserver/etcdserverpb/gw"
|
||||
"github.com/coreos/etcd/pkg/debugutil"
|
||||
"github.com/coreos/etcd/pkg/httputil"
|
||||
"github.com/coreos/etcd/pkg/transport"
|
||||
|
||||
gw "github.com/grpc-ecosystem/grpc-gateway/runtime"
|
||||
"github.com/soheilhy/cmux"
|
||||
"github.com/tmc/grpc-websocket-proxy/wsproxy"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/trace"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
type serveCtx struct {
|
||||
lg *zap.Logger
|
||||
l net.Listener
|
||||
addr string
|
||||
network string
|
||||
secure bool
|
||||
insecure bool
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
userHandlers map[string]http.Handler
|
||||
serviceRegister func(*grpc.Server)
|
||||
serversC chan *servers
|
||||
}
|
||||
|
||||
type servers struct {
|
||||
secure bool
|
||||
grpc *grpc.Server
|
||||
http *http.Server
|
||||
}
|
||||
|
||||
func newServeCtx(lg *zap.Logger) *serveCtx {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &serveCtx{
|
||||
lg: lg,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
userHandlers: make(map[string]http.Handler),
|
||||
serversC: make(chan *servers, 2), // in case sctx.insecure,sctx.secure true
|
||||
}
|
||||
}
|
||||
|
||||
// serve accepts incoming connections on the listener l,
|
||||
// creating a new service goroutine for each. The service goroutines
|
||||
// read requests and then call handler to reply to them.
|
||||
func (sctx *serveCtx) serve(
|
||||
s *etcdserver.EtcdServer,
|
||||
tlsinfo *transport.TLSInfo,
|
||||
handler http.Handler,
|
||||
errHandler func(error),
|
||||
gopts ...grpc.ServerOption) (err error) {
|
||||
logger := defaultLog.New(ioutil.Discard, "etcdhttp", 0)
|
||||
<-s.ReadyNotify()
|
||||
|
||||
if sctx.lg == nil {
|
||||
plog.Info("ready to serve client requests")
|
||||
}
|
||||
|
||||
m := cmux.New(sctx.l)
|
||||
v3c := v3client.New(s)
|
||||
servElection := v3election.NewElectionServer(v3c)
|
||||
servLock := v3lock.NewLockServer(v3c)
|
||||
|
||||
var gs *grpc.Server
|
||||
defer func() {
|
||||
if err != nil && gs != nil {
|
||||
gs.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
if sctx.insecure {
|
||||
gs = v3rpc.Server(s, nil, gopts...)
|
||||
v3electionpb.RegisterElectionServer(gs, servElection)
|
||||
v3lockpb.RegisterLockServer(gs, servLock)
|
||||
if sctx.serviceRegister != nil {
|
||||
sctx.serviceRegister(gs)
|
||||
}
|
||||
grpcl := m.Match(cmux.HTTP2())
|
||||
go func() { errHandler(gs.Serve(grpcl)) }()
|
||||
|
||||
var gwmux *gw.ServeMux
|
||||
gwmux, err = sctx.registerGateway([]grpc.DialOption{grpc.WithInsecure()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpmux := sctx.createMux(gwmux, handler)
|
||||
|
||||
srvhttp := &http.Server{
|
||||
Handler: createAccessController(sctx.lg, s, httpmux),
|
||||
ErrorLog: logger, // do not log user error
|
||||
}
|
||||
httpl := m.Match(cmux.HTTP1())
|
||||
go func() { errHandler(srvhttp.Serve(httpl)) }()
|
||||
|
||||
sctx.serversC <- &servers{grpc: gs, http: srvhttp}
|
||||
if sctx.lg != nil {
|
||||
sctx.lg.Info(
|
||||
"serving client traffic insecurely; this is strongly discouraged!",
|
||||
zap.String("address", sctx.l.Addr().String()),
|
||||
)
|
||||
} else {
|
||||
plog.Noticef("serving insecure client requests on %s, this is strongly discouraged!", sctx.l.Addr().String())
|
||||
}
|
||||
}
|
||||
|
||||
if sctx.secure {
|
||||
tlscfg, tlsErr := tlsinfo.ServerConfig()
|
||||
if tlsErr != nil {
|
||||
return tlsErr
|
||||
}
|
||||
gs = v3rpc.Server(s, tlscfg, gopts...)
|
||||
v3electionpb.RegisterElectionServer(gs, servElection)
|
||||
v3lockpb.RegisterLockServer(gs, servLock)
|
||||
if sctx.serviceRegister != nil {
|
||||
sctx.serviceRegister(gs)
|
||||
}
|
||||
handler = grpcHandlerFunc(gs, handler)
|
||||
|
||||
dtls := tlscfg.Clone()
|
||||
// trust local server
|
||||
dtls.InsecureSkipVerify = true
|
||||
creds := credentials.NewTLS(dtls)
|
||||
opts := []grpc.DialOption{grpc.WithTransportCredentials(creds)}
|
||||
var gwmux *gw.ServeMux
|
||||
gwmux, err = sctx.registerGateway(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var tlsl net.Listener
|
||||
tlsl, err = transport.NewTLSListener(m.Match(cmux.Any()), tlsinfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: add debug flag; enable logging when debug flag is set
|
||||
httpmux := sctx.createMux(gwmux, handler)
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: createAccessController(sctx.lg, s, httpmux),
|
||||
TLSConfig: tlscfg,
|
||||
ErrorLog: logger, // do not log user error
|
||||
}
|
||||
go func() { errHandler(srv.Serve(tlsl)) }()
|
||||
|
||||
sctx.serversC <- &servers{secure: true, grpc: gs, http: srv}
|
||||
if sctx.lg != nil {
|
||||
sctx.lg.Info(
|
||||
"serving client traffic insecurely",
|
||||
zap.String("address", sctx.l.Addr().String()),
|
||||
)
|
||||
} else {
|
||||
plog.Infof("serving client requests on %s", sctx.l.Addr().String())
|
||||
}
|
||||
}
|
||||
|
||||
close(sctx.serversC)
|
||||
return m.Serve()
|
||||
}
|
||||
|
||||
// grpcHandlerFunc returns an http.Handler that delegates to grpcServer on incoming gRPC
|
||||
// connections or otherHandler otherwise. Given in gRPC docs.
|
||||
func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
|
||||
if otherHandler == nil {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
grpcServer.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
|
||||
grpcServer.ServeHTTP(w, r)
|
||||
} else {
|
||||
otherHandler.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type registerHandlerFunc func(context.Context, *gw.ServeMux, *grpc.ClientConn) error
|
||||
|
||||
func (sctx *serveCtx) registerGateway(opts []grpc.DialOption) (*gw.ServeMux, error) {
|
||||
ctx := sctx.ctx
|
||||
|
||||
addr := sctx.addr
|
||||
if network := sctx.network; network == "unix" {
|
||||
// explicitly define unix network for gRPC socket support
|
||||
addr = fmt.Sprintf("%s://%s", network, addr)
|
||||
}
|
||||
|
||||
conn, err := grpc.DialContext(ctx, addr, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gwmux := gw.NewServeMux()
|
||||
|
||||
handlers := []registerHandlerFunc{
|
||||
etcdservergw.RegisterKVHandler,
|
||||
etcdservergw.RegisterWatchHandler,
|
||||
etcdservergw.RegisterLeaseHandler,
|
||||
etcdservergw.RegisterClusterHandler,
|
||||
etcdservergw.RegisterMaintenanceHandler,
|
||||
etcdservergw.RegisterAuthHandler,
|
||||
v3lockgw.RegisterLockHandler,
|
||||
v3electiongw.RegisterElectionHandler,
|
||||
}
|
||||
for _, h := range handlers {
|
||||
if err := h(ctx, gwmux, conn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
if sctx.lg != nil {
|
||||
sctx.lg.Warn(
|
||||
"failed to close connection",
|
||||
zap.String("address", sctx.l.Addr().String()),
|
||||
zap.Error(cerr),
|
||||
)
|
||||
} else {
|
||||
plog.Warningf("failed to close conn to %s: %v", sctx.l.Addr().String(), cerr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return gwmux, nil
|
||||
}
|
||||
|
||||
func (sctx *serveCtx) createMux(gwmux *gw.ServeMux, handler http.Handler) *http.ServeMux {
|
||||
httpmux := http.NewServeMux()
|
||||
for path, h := range sctx.userHandlers {
|
||||
httpmux.Handle(path, h)
|
||||
}
|
||||
|
||||
httpmux.Handle(
|
||||
"/v3/",
|
||||
wsproxy.WebsocketProxy(
|
||||
gwmux,
|
||||
wsproxy.WithRequestMutator(
|
||||
// Default to the POST method for streams
|
||||
func(incoming *http.Request, outgoing *http.Request) *http.Request {
|
||||
outgoing.Method = "POST"
|
||||
return outgoing
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
if handler != nil {
|
||||
httpmux.Handle("/", handler)
|
||||
}
|
||||
return httpmux
|
||||
}
|
||||
|
||||
// createAccessController wraps HTTP multiplexer:
|
||||
// - mutate gRPC gateway request paths
|
||||
// - check hostname whitelist
|
||||
// client HTTP requests goes here first
|
||||
func createAccessController(lg *zap.Logger, s *etcdserver.EtcdServer, mux *http.ServeMux) http.Handler {
|
||||
return &accessController{lg: lg, s: s, mux: mux}
|
||||
}
|
||||
|
||||
type accessController struct {
|
||||
lg *zap.Logger
|
||||
s *etcdserver.EtcdServer
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
func (ac *accessController) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
// redirect for backward compatibilities
|
||||
if req != nil && req.URL != nil && strings.HasPrefix(req.URL.Path, "/v3beta/") {
|
||||
req.URL.Path = strings.Replace(req.URL.Path, "/v3beta/", "/v3/", 1)
|
||||
}
|
||||
|
||||
if req.TLS == nil { // check origin if client connection is not secure
|
||||
host := httputil.GetHostname(req)
|
||||
if !ac.s.AccessController.IsHostWhitelisted(host) {
|
||||
if ac.lg != nil {
|
||||
ac.lg.Warn(
|
||||
"rejecting HTTP request to prevent DNS rebinding attacks",
|
||||
zap.String("host", host),
|
||||
)
|
||||
} else {
|
||||
plog.Warningf("rejecting HTTP request from %q to prevent DNS rebinding attacks", host)
|
||||
}
|
||||
// TODO: use Go's "http.StatusMisdirectedRequest" (421)
|
||||
// https://github.com/golang/go/commit/4b8a7eafef039af1834ef9bfa879257c4a72b7b5
|
||||
http.Error(rw, errCVE20185702(host), 421)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Write CORS header.
|
||||
if ac.s.AccessController.OriginAllowed("*") {
|
||||
addCORSHeader(rw, "*")
|
||||
} else if origin := req.Header.Get("Origin"); ac.s.OriginAllowed(origin) {
|
||||
addCORSHeader(rw, origin)
|
||||
}
|
||||
|
||||
if req.Method == "OPTIONS" {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
ac.mux.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
// addCORSHeader adds the correct cors headers given an origin
|
||||
func addCORSHeader(w http.ResponseWriter, origin string) {
|
||||
w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
|
||||
w.Header().Add("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Add("Access-Control-Allow-Headers", "accept, content-type, authorization")
|
||||
}
|
||||
|
||||
// https://github.com/transmission/transmission/pull/468
|
||||
func errCVE20185702(host string) string {
|
||||
return fmt.Sprintf(`
|
||||
etcd received your request, but the Host header was unrecognized.
|
||||
|
||||
To fix this, choose one of the following options:
|
||||
- Enable TLS, then any HTTPS request will be allowed.
|
||||
- Add the hostname you want to use to the whitelist in settings.
|
||||
- e.g. etcd --host-whitelist %q
|
||||
|
||||
This requirement has been added to help prevent "DNS Rebinding" attacks (CVE-2018-5702).
|
||||
`, host)
|
||||
}
|
||||
|
||||
// WrapCORS wraps existing handler with CORS.
|
||||
// TODO: deprecate this after v2 proxy deprecate
|
||||
func WrapCORS(cors map[string]struct{}, h http.Handler) http.Handler {
|
||||
return &corsHandler{
|
||||
ac: &etcdserver.AccessController{CORS: cors},
|
||||
h: h,
|
||||
}
|
||||
}
|
||||
|
||||
type corsHandler struct {
|
||||
ac *etcdserver.AccessController
|
||||
h http.Handler
|
||||
}
|
||||
|
||||
func (ch *corsHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if ch.ac.OriginAllowed("*") {
|
||||
addCORSHeader(rw, "*")
|
||||
} else if origin := req.Header.Get("Origin"); ch.ac.OriginAllowed(origin) {
|
||||
addCORSHeader(rw, origin)
|
||||
}
|
||||
|
||||
if req.Method == "OPTIONS" {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
ch.h.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
func (sctx *serveCtx) registerUserHandler(s string, h http.Handler) {
|
||||
if sctx.userHandlers[s] != nil {
|
||||
if sctx.lg != nil {
|
||||
sctx.lg.Warn("path is already registered by user handler", zap.String("path", s))
|
||||
} else {
|
||||
plog.Warningf("path %s already registered by user handler", s)
|
||||
}
|
||||
return
|
||||
}
|
||||
sctx.userHandlers[s] = h
|
||||
}
|
||||
|
||||
func (sctx *serveCtx) registerPprof() {
|
||||
for p, h := range debugutil.PProfHandlers() {
|
||||
sctx.registerUserHandler(p, h)
|
||||
}
|
||||
}
|
||||
|
||||
func (sctx *serveCtx) registerTrace() {
|
||||
reqf := func(w http.ResponseWriter, r *http.Request) { trace.Render(w, r, true) }
|
||||
sctx.registerUserHandler("/debug/requests", http.HandlerFunc(reqf))
|
||||
evf := func(w http.ResponseWriter, r *http.Request) { trace.RenderEvents(w, r, true) }
|
||||
sctx.registerUserHandler("/debug/events", http.HandlerFunc(evf))
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright 2016 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 embed
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/coreos/etcd/wal"
|
||||
)
|
||||
|
||||
func isMemberInitialized(cfg *Config) bool {
|
||||
waldir := cfg.WalDir
|
||||
if waldir == "" {
|
||||
waldir = filepath.Join(cfg.Dir, "member", "wal")
|
||||
}
|
||||
return wal.Exist(waldir)
|
||||
}
|
||||
90
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/auth_commands.go
generated
vendored
Normal file
90
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/auth_commands.go
generated
vendored
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func NewAuthCommands() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "auth",
|
||||
Usage: "overall auth controls",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "enable",
|
||||
Usage: "enable auth access controls",
|
||||
ArgsUsage: " ",
|
||||
Action: actionAuthEnable,
|
||||
},
|
||||
{
|
||||
Name: "disable",
|
||||
Usage: "disable auth access controls",
|
||||
ArgsUsage: " ",
|
||||
Action: actionAuthDisable,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func actionAuthEnable(c *cli.Context) error {
|
||||
authEnableDisable(c, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func actionAuthDisable(c *cli.Context) error {
|
||||
authEnableDisable(c, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustNewAuthAPI(c *cli.Context) client.AuthAPI {
|
||||
hc := mustNewClient(c)
|
||||
|
||||
if c.GlobalBool("debug") {
|
||||
fmt.Fprintf(os.Stderr, "Cluster-Endpoints: %s\n", strings.Join(hc.Endpoints(), ", "))
|
||||
}
|
||||
|
||||
return client.NewAuthAPI(hc)
|
||||
}
|
||||
|
||||
func authEnableDisable(c *cli.Context, enable bool) {
|
||||
if len(c.Args()) != 0 {
|
||||
fmt.Fprintln(os.Stderr, "No arguments accepted")
|
||||
os.Exit(1)
|
||||
}
|
||||
s := mustNewAuthAPI(c)
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
var err error
|
||||
if enable {
|
||||
err = s.Enable(ctx)
|
||||
} else {
|
||||
err = s.Disable(ctx)
|
||||
}
|
||||
cancel()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
if enable {
|
||||
fmt.Println("Authentication Enabled")
|
||||
} else {
|
||||
fmt.Println("Authentication Disabled")
|
||||
}
|
||||
}
|
||||
257
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/backup_command.go
generated
vendored
Normal file
257
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/backup_command.go
generated
vendored
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/api/membership"
|
||||
"github.com/coreos/etcd/etcdserver/api/snap"
|
||||
"github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/pkg/fileutil"
|
||||
"github.com/coreos/etcd/pkg/idutil"
|
||||
"github.com/coreos/etcd/pkg/pbutil"
|
||||
"github.com/coreos/etcd/raft/raftpb"
|
||||
"github.com/coreos/etcd/wal"
|
||||
"github.com/coreos/etcd/wal/walpb"
|
||||
|
||||
bolt "github.com/coreos/bbolt"
|
||||
"github.com/urfave/cli"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func NewBackupCommand() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "backup",
|
||||
Usage: "backup an etcd directory",
|
||||
ArgsUsage: " ",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{Name: "data-dir", Value: "", Usage: "Path to the etcd data dir"},
|
||||
cli.StringFlag{Name: "wal-dir", Value: "", Usage: "Path to the etcd wal dir"},
|
||||
cli.StringFlag{Name: "backup-dir", Value: "", Usage: "Path to the backup dir"},
|
||||
cli.StringFlag{Name: "backup-wal-dir", Value: "", Usage: "Path to the backup wal dir"},
|
||||
cli.BoolFlag{Name: "with-v3", Usage: "Backup v3 backend data"},
|
||||
},
|
||||
Action: handleBackup,
|
||||
}
|
||||
}
|
||||
|
||||
// handleBackup handles a request that intends to do a backup.
|
||||
func handleBackup(c *cli.Context) error {
|
||||
var srcWAL string
|
||||
var destWAL string
|
||||
|
||||
withV3 := c.Bool("with-v3")
|
||||
srcSnap := filepath.Join(c.String("data-dir"), "member", "snap")
|
||||
destSnap := filepath.Join(c.String("backup-dir"), "member", "snap")
|
||||
|
||||
if c.String("wal-dir") != "" {
|
||||
srcWAL = c.String("wal-dir")
|
||||
} else {
|
||||
srcWAL = filepath.Join(c.String("data-dir"), "member", "wal")
|
||||
}
|
||||
|
||||
if c.String("backup-wal-dir") != "" {
|
||||
destWAL = c.String("backup-wal-dir")
|
||||
} else {
|
||||
destWAL = filepath.Join(c.String("backup-dir"), "member", "wal")
|
||||
}
|
||||
|
||||
if err := fileutil.CreateDirAll(destSnap); err != nil {
|
||||
log.Fatalf("failed creating backup snapshot dir %v: %v", destSnap, err)
|
||||
}
|
||||
|
||||
walsnap := saveSnap(destSnap, srcSnap)
|
||||
metadata, state, ents := loadWAL(srcWAL, walsnap, withV3)
|
||||
saveDB(filepath.Join(destSnap, "db"), filepath.Join(srcSnap, "db"), state.Commit, withV3)
|
||||
|
||||
idgen := idutil.NewGenerator(0, time.Now())
|
||||
metadata.NodeID = idgen.Next()
|
||||
metadata.ClusterID = idgen.Next()
|
||||
|
||||
neww, err := wal.Create(zap.NewExample(), destWAL, pbutil.MustMarshal(&metadata))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer neww.Close()
|
||||
if err := neww.Save(state, ents); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := neww.SaveSnapshot(walsnap); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveSnap(destSnap, srcSnap string) (walsnap walpb.Snapshot) {
|
||||
ss := snap.New(zap.NewExample(), srcSnap)
|
||||
snapshot, err := ss.Load()
|
||||
if err != nil && err != snap.ErrNoSnapshot {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if snapshot != nil {
|
||||
walsnap.Index, walsnap.Term = snapshot.Metadata.Index, snapshot.Metadata.Term
|
||||
newss := snap.New(zap.NewExample(), destSnap)
|
||||
if err = newss.SaveSnap(*snapshot); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return walsnap
|
||||
}
|
||||
|
||||
func loadWAL(srcWAL string, walsnap walpb.Snapshot, v3 bool) (etcdserverpb.Metadata, raftpb.HardState, []raftpb.Entry) {
|
||||
w, err := wal.OpenForRead(zap.NewExample(), srcWAL, walsnap)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer w.Close()
|
||||
wmetadata, state, ents, err := w.ReadAll()
|
||||
switch err {
|
||||
case nil:
|
||||
case wal.ErrSnapshotNotFound:
|
||||
log.Printf("Failed to find the match snapshot record %+v in wal %v.", walsnap, srcWAL)
|
||||
log.Printf("etcdctl will add it back. Start auto fixing...")
|
||||
default:
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
re := path.Join(membership.StoreMembersPrefix, "[[:xdigit:]]{1,16}", "attributes")
|
||||
memberAttrRE := regexp.MustCompile(re)
|
||||
|
||||
removed := uint64(0)
|
||||
i := 0
|
||||
remove := func() {
|
||||
ents = append(ents[:i], ents[i+1:]...)
|
||||
removed++
|
||||
i--
|
||||
}
|
||||
for i = 0; i < len(ents); i++ {
|
||||
ents[i].Index -= removed
|
||||
if ents[i].Type == raftpb.EntryConfChange {
|
||||
log.Println("ignoring EntryConfChange raft entry")
|
||||
remove()
|
||||
continue
|
||||
}
|
||||
|
||||
var raftReq etcdserverpb.InternalRaftRequest
|
||||
var v2Req *etcdserverpb.Request
|
||||
if pbutil.MaybeUnmarshal(&raftReq, ents[i].Data) {
|
||||
v2Req = raftReq.V2
|
||||
} else {
|
||||
v2Req = &etcdserverpb.Request{}
|
||||
pbutil.MustUnmarshal(v2Req, ents[i].Data)
|
||||
}
|
||||
|
||||
if v2Req != nil && v2Req.Method == "PUT" && memberAttrRE.MatchString(v2Req.Path) {
|
||||
log.Println("ignoring member attribute update on", v2Req.Path)
|
||||
remove()
|
||||
continue
|
||||
}
|
||||
|
||||
if v2Req != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if v3 || raftReq.Header == nil {
|
||||
continue
|
||||
}
|
||||
log.Println("ignoring v3 raft entry")
|
||||
remove()
|
||||
}
|
||||
state.Commit -= removed
|
||||
var metadata etcdserverpb.Metadata
|
||||
pbutil.MustUnmarshal(&metadata, wmetadata)
|
||||
return metadata, state, ents
|
||||
}
|
||||
|
||||
// saveDB copies the v3 backend and strips cluster information.
|
||||
func saveDB(destDB, srcDB string, idx uint64, v3 bool) {
|
||||
// open src db to safely copy db state
|
||||
if v3 {
|
||||
var src *bolt.DB
|
||||
ch := make(chan *bolt.DB, 1)
|
||||
go func() {
|
||||
db, err := bolt.Open(srcDB, 0444, &bolt.Options{ReadOnly: true})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
ch <- db
|
||||
}()
|
||||
select {
|
||||
case src = <-ch:
|
||||
case <-time.After(time.Second):
|
||||
log.Println("waiting to acquire lock on", srcDB)
|
||||
src = <-ch
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
tx, err := src.Begin(false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// copy srcDB to destDB
|
||||
dest, err := os.Create(destDB)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if _, err := tx.WriteTo(dest); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
dest.Close()
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
db, err := bolt.Open(destDB, 0644, &bolt.Options{})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
tx, err := db.Begin(true)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// remove membership information; should be clobbered by --force-new-cluster
|
||||
for _, bucket := range []string{"members", "members_removed", "cluster"} {
|
||||
tx.DeleteBucket([]byte(bucket))
|
||||
}
|
||||
|
||||
// update consistent index to match hard state
|
||||
if !v3 {
|
||||
idxBytes := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(idxBytes, idx)
|
||||
b, err := tx.CreateBucketIfNotExists([]byte("meta"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
b.Put([]byte("consistent_index"), idxBytes)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
139
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/cluster_health.go
generated
vendored
Normal file
139
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/cluster_health.go
generated
vendored
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func NewClusterHealthCommand() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "cluster-health",
|
||||
Usage: "check the health of the etcd cluster",
|
||||
ArgsUsage: " ",
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{Name: "forever, f", Usage: "forever check the health every 10 second until CTRL+C"},
|
||||
},
|
||||
Action: handleClusterHealth,
|
||||
}
|
||||
}
|
||||
|
||||
func handleClusterHealth(c *cli.Context) error {
|
||||
forever := c.Bool("forever")
|
||||
if forever {
|
||||
sigch := make(chan os.Signal, 1)
|
||||
signal.Notify(sigch, os.Interrupt)
|
||||
|
||||
go func() {
|
||||
<-sigch
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
|
||||
tr, err := getTransport(c)
|
||||
if err != nil {
|
||||
handleError(c, ExitServerError, err)
|
||||
}
|
||||
|
||||
hc := http.Client{
|
||||
Transport: tr,
|
||||
}
|
||||
|
||||
cln := mustNewClientNoSync(c)
|
||||
mi := client.NewMembersAPI(cln)
|
||||
ms, err := mi.List(context.TODO())
|
||||
if err != nil {
|
||||
fmt.Println("cluster may be unhealthy: failed to list members")
|
||||
handleError(c, ExitServerError, err)
|
||||
}
|
||||
|
||||
for {
|
||||
healthyMembers := 0
|
||||
for _, m := range ms {
|
||||
if len(m.ClientURLs) == 0 {
|
||||
fmt.Printf("member %s is unreachable: no available published client urls\n", m.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
checked := false
|
||||
for _, url := range m.ClientURLs {
|
||||
resp, err := hc.Get(url + "/health")
|
||||
if err != nil {
|
||||
fmt.Printf("failed to check the health of member %s on %s: %v\n", m.ID, url, err)
|
||||
continue
|
||||
}
|
||||
|
||||
result := struct{ Health string }{}
|
||||
nresult := struct{ Health bool }{}
|
||||
bytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to check the health of member %s on %s: %v\n", m.ID, url, err)
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
err = json.Unmarshal(bytes, &result)
|
||||
if err != nil {
|
||||
err = json.Unmarshal(bytes, &nresult)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Printf("failed to check the health of member %s on %s: %v\n", m.ID, url, err)
|
||||
continue
|
||||
}
|
||||
|
||||
checked = true
|
||||
if result.Health == "true" || nresult.Health {
|
||||
fmt.Printf("member %s is healthy: got healthy result from %s\n", m.ID, url)
|
||||
healthyMembers++
|
||||
} else {
|
||||
fmt.Printf("member %s is unhealthy: got unhealthy result from %s\n", m.ID, url)
|
||||
}
|
||||
break
|
||||
}
|
||||
if !checked {
|
||||
fmt.Printf("member %s is unreachable: %v are all unreachable\n", m.ID, m.ClientURLs)
|
||||
}
|
||||
}
|
||||
switch healthyMembers {
|
||||
case len(ms):
|
||||
fmt.Println("cluster is healthy")
|
||||
case 0:
|
||||
fmt.Println("cluster is unavailable")
|
||||
default:
|
||||
fmt.Println("cluster is degraded")
|
||||
}
|
||||
|
||||
if !forever {
|
||||
if healthyMembers == len(ms) {
|
||||
os.Exit(ExitSuccess)
|
||||
}
|
||||
os.Exit(ExitClusterNotHealthy)
|
||||
}
|
||||
|
||||
fmt.Printf("\nnext check after 10 second...\n\n")
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2015 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 command is a set of libraries for etcdctl commands.
|
||||
package command
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const (
|
||||
ExitSuccess = iota
|
||||
ExitBadArgs
|
||||
ExitBadConnection
|
||||
ExitBadAuth
|
||||
ExitServerError
|
||||
ExitClusterNotHealthy
|
||||
)
|
||||
|
||||
func handleError(c *cli.Context, code int, err error) {
|
||||
if c.GlobalString("output") == "json" {
|
||||
if err, ok := err.(*client.Error); ok {
|
||||
b, err := json.Marshal(err)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, string(b))
|
||||
os.Exit(code)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "Error: ", err)
|
||||
if cerr, ok := err.(*client.ClusterError); ok {
|
||||
fmt.Fprintln(os.Stderr, cerr.Detail())
|
||||
}
|
||||
os.Exit(code)
|
||||
}
|
||||
129
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/exec_watch_command.go
generated
vendored
Normal file
129
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/exec_watch_command.go
generated
vendored
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// NewExecWatchCommand returns the CLI command for "exec-watch".
|
||||
func NewExecWatchCommand() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "exec-watch",
|
||||
Usage: "watch a key for changes and exec an executable",
|
||||
ArgsUsage: "<key> <command> [args...]",
|
||||
Flags: []cli.Flag{
|
||||
cli.IntFlag{Name: "after-index", Value: 0, Usage: "watch after the given index"},
|
||||
cli.BoolFlag{Name: "recursive, r", Usage: "watch all values for key and child keys"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
execWatchCommandFunc(c, mustNewKeyAPI(c))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// execWatchCommandFunc executes the "exec-watch" command.
|
||||
func execWatchCommandFunc(c *cli.Context, ki client.KeysAPI) {
|
||||
args := c.Args()
|
||||
argslen := len(args)
|
||||
|
||||
if argslen < 2 {
|
||||
handleError(c, ExitBadArgs, errors.New("key and command to exec required"))
|
||||
}
|
||||
|
||||
var (
|
||||
key string
|
||||
cmdArgs []string
|
||||
)
|
||||
|
||||
foundSep := false
|
||||
for i := range args {
|
||||
if args[i] == "--" && i != 0 {
|
||||
foundSep = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundSep {
|
||||
key = args[0]
|
||||
cmdArgs = args[2:]
|
||||
} else {
|
||||
// If no flag is parsed, the order of key and cmdArgs will be switched and
|
||||
// args will not contain `--`.
|
||||
key = args[argslen-1]
|
||||
cmdArgs = args[:argslen-1]
|
||||
}
|
||||
|
||||
index := 0
|
||||
if c.Int("after-index") != 0 {
|
||||
index = c.Int("after-index")
|
||||
}
|
||||
|
||||
recursive := c.Bool("recursive")
|
||||
|
||||
sigch := make(chan os.Signal, 1)
|
||||
signal.Notify(sigch, os.Interrupt)
|
||||
|
||||
go func() {
|
||||
<-sigch
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
w := ki.Watcher(key, &client.WatcherOptions{AfterIndex: uint64(index), Recursive: recursive})
|
||||
|
||||
for {
|
||||
resp, err := w.Next(context.TODO())
|
||||
if err != nil {
|
||||
handleError(c, ExitServerError, err)
|
||||
}
|
||||
if resp.Node.Dir {
|
||||
fmt.Fprintf(os.Stderr, "Ignored dir %s change\n", resp.Node.Key)
|
||||
continue
|
||||
}
|
||||
|
||||
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
|
||||
cmd.Env = environResponse(resp, os.Environ())
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
go func() {
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
cmd.Wait()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func environResponse(resp *client.Response, env []string) []string {
|
||||
env = append(env, "ETCD_WATCH_ACTION="+resp.Action)
|
||||
env = append(env, "ETCD_WATCH_MODIFIED_INDEX="+fmt.Sprintf("%d", resp.Node.ModifiedIndex))
|
||||
env = append(env, "ETCD_WATCH_KEY="+resp.Node.Key)
|
||||
env = append(env, "ETCD_WATCH_VALUE="+resp.Node.Value)
|
||||
return env
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
)
|
||||
|
||||
// printResponseKey only supports to print key correctly.
|
||||
func printResponseKey(resp *client.Response, format string) {
|
||||
// Format the result.
|
||||
switch format {
|
||||
case "simple":
|
||||
if resp.Action != "delete" {
|
||||
fmt.Println(resp.Node.Value)
|
||||
} else {
|
||||
fmt.Println("PrevNode.Value:", resp.PrevNode.Value)
|
||||
}
|
||||
case "extended":
|
||||
// Extended prints in a rfc2822 style format
|
||||
fmt.Println("Key:", resp.Node.Key)
|
||||
fmt.Println("Created-Index:", resp.Node.CreatedIndex)
|
||||
fmt.Println("Modified-Index:", resp.Node.ModifiedIndex)
|
||||
|
||||
if resp.PrevNode != nil {
|
||||
fmt.Println("PrevNode.Value:", resp.PrevNode.Value)
|
||||
}
|
||||
|
||||
fmt.Println("TTL:", resp.Node.TTL)
|
||||
fmt.Println("Index:", resp.Index)
|
||||
if resp.Action != "delete" {
|
||||
fmt.Println("")
|
||||
fmt.Println(resp.Node.Value)
|
||||
}
|
||||
case "json":
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
default:
|
||||
fmt.Fprintln(os.Stderr, "Unsupported output format:", format)
|
||||
}
|
||||
}
|
||||
66
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/get_command.go
generated
vendored
Normal file
66
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/get_command.go
generated
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// NewGetCommand returns the CLI command for "get".
|
||||
func NewGetCommand() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "get",
|
||||
Usage: "retrieve the value of a key",
|
||||
ArgsUsage: "<key>",
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{Name: "sort", Usage: "returns result in sorted order"},
|
||||
cli.BoolFlag{Name: "quorum, q", Usage: "require quorum for get request"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
getCommandFunc(c, mustNewKeyAPI(c))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// getCommandFunc executes the "get" command.
|
||||
func getCommandFunc(c *cli.Context, ki client.KeysAPI) {
|
||||
if len(c.Args()) == 0 {
|
||||
handleError(c, ExitBadArgs, errors.New("key required"))
|
||||
}
|
||||
|
||||
key := c.Args()[0]
|
||||
sorted := c.Bool("sort")
|
||||
quorum := c.Bool("quorum")
|
||||
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
resp, err := ki.Get(ctx, key, &client.GetOptions{Sort: sorted, Quorum: quorum})
|
||||
cancel()
|
||||
if err != nil {
|
||||
handleError(c, ExitServerError, err)
|
||||
}
|
||||
|
||||
if resp.Node.Dir {
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("%s: is a directory", resp.Node.Key))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
printResponseKey(resp, c.GlobalString("output"))
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func NewLsCommand() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "ls",
|
||||
Usage: "retrieve a directory",
|
||||
ArgsUsage: "[key]",
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{Name: "sort", Usage: "returns result in sorted order"},
|
||||
cli.BoolFlag{Name: "recursive, r", Usage: "returns all key names recursively for the given path"},
|
||||
cli.BoolFlag{Name: "p", Usage: "append slash (/) to directories"},
|
||||
cli.BoolFlag{Name: "quorum, q", Usage: "require quorum for get request"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
lsCommandFunc(c, mustNewKeyAPI(c))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// lsCommandFunc executes the "ls" command.
|
||||
func lsCommandFunc(c *cli.Context, ki client.KeysAPI) {
|
||||
key := "/"
|
||||
if len(c.Args()) != 0 {
|
||||
key = c.Args()[0]
|
||||
}
|
||||
|
||||
sort := c.Bool("sort")
|
||||
recursive := c.Bool("recursive")
|
||||
quorum := c.Bool("quorum")
|
||||
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
resp, err := ki.Get(ctx, key, &client.GetOptions{Sort: sort, Recursive: recursive, Quorum: quorum})
|
||||
cancel()
|
||||
if err != nil {
|
||||
handleError(c, ExitServerError, err)
|
||||
}
|
||||
|
||||
printLs(c, resp)
|
||||
}
|
||||
|
||||
// printLs writes a response out in a manner similar to the `ls` command in unix.
|
||||
// Non-empty directories list their contents and files list their name.
|
||||
func printLs(c *cli.Context, resp *client.Response) {
|
||||
if c.GlobalString("output") == "simple" {
|
||||
if !resp.Node.Dir {
|
||||
fmt.Println(resp.Node.Key)
|
||||
}
|
||||
for _, node := range resp.Node.Nodes {
|
||||
rPrint(c, node)
|
||||
}
|
||||
} else {
|
||||
// user wants JSON or extended output
|
||||
printResponseKey(resp, c.GlobalString("output"))
|
||||
}
|
||||
}
|
||||
|
||||
// rPrint recursively prints out the nodes in the node structure.
|
||||
func rPrint(c *cli.Context, n *client.Node) {
|
||||
if n.Dir && c.Bool("p") {
|
||||
fmt.Println(fmt.Sprintf("%v/", n.Key))
|
||||
} else {
|
||||
fmt.Println(n.Key)
|
||||
}
|
||||
|
||||
for _, node := range n.Nodes {
|
||||
rPrint(c, node)
|
||||
}
|
||||
}
|
||||
207
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/member_commands.go
generated
vendored
Normal file
207
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/member_commands.go
generated
vendored
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func NewMemberCommand() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "member",
|
||||
Usage: "member add, remove and list subcommands",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
Usage: "enumerate existing cluster members",
|
||||
ArgsUsage: " ",
|
||||
Action: actionMemberList,
|
||||
},
|
||||
{
|
||||
Name: "add",
|
||||
Usage: "add a new member to the etcd cluster",
|
||||
ArgsUsage: "<name> <peerURL>",
|
||||
Action: actionMemberAdd,
|
||||
},
|
||||
{
|
||||
Name: "remove",
|
||||
Usage: "remove an existing member from the etcd cluster",
|
||||
ArgsUsage: "<memberID>",
|
||||
Action: actionMemberRemove,
|
||||
},
|
||||
{
|
||||
Name: "update",
|
||||
Usage: "update an existing member in the etcd cluster",
|
||||
ArgsUsage: "<memberID> <peerURLs>",
|
||||
Action: actionMemberUpdate,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func actionMemberList(c *cli.Context) error {
|
||||
if len(c.Args()) != 0 {
|
||||
fmt.Fprintln(os.Stderr, "No arguments accepted")
|
||||
os.Exit(1)
|
||||
}
|
||||
mAPI := mustNewMembersAPI(c)
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
defer cancel()
|
||||
|
||||
members, err := mAPI.List(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
leader, err := mAPI.Leader(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Failed to get leader: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, m := range members {
|
||||
isLeader := false
|
||||
if m.ID == leader.ID {
|
||||
isLeader = true
|
||||
}
|
||||
if len(m.Name) == 0 {
|
||||
fmt.Printf("%s[unstarted]: peerURLs=%s\n", m.ID, strings.Join(m.PeerURLs, ","))
|
||||
} else {
|
||||
fmt.Printf("%s: name=%s peerURLs=%s clientURLs=%s isLeader=%v\n", m.ID, m.Name, strings.Join(m.PeerURLs, ","), strings.Join(m.ClientURLs, ","), isLeader)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func actionMemberAdd(c *cli.Context) error {
|
||||
args := c.Args()
|
||||
if len(args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, "Provide a name and a single member peerURL")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
mAPI := mustNewMembersAPI(c)
|
||||
|
||||
url := args[1]
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
defer cancel()
|
||||
|
||||
m, err := mAPI.Add(ctx, url)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
newID := m.ID
|
||||
newName := args[0]
|
||||
fmt.Printf("Added member named %s with ID %s to cluster\n", newName, newID)
|
||||
|
||||
members, err := mAPI.List(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
conf := []string{}
|
||||
for _, memb := range members {
|
||||
for _, u := range memb.PeerURLs {
|
||||
n := memb.Name
|
||||
if memb.ID == newID {
|
||||
n = newName
|
||||
}
|
||||
conf = append(conf, fmt.Sprintf("%s=%s", n, u))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Print("\n")
|
||||
fmt.Printf("ETCD_NAME=%q\n", newName)
|
||||
fmt.Printf("ETCD_INITIAL_CLUSTER=%q\n", strings.Join(conf, ","))
|
||||
fmt.Printf("ETCD_INITIAL_CLUSTER_STATE=\"existing\"\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func actionMemberRemove(c *cli.Context) error {
|
||||
args := c.Args()
|
||||
if len(args) != 1 {
|
||||
fmt.Fprintln(os.Stderr, "Provide a single member ID")
|
||||
os.Exit(1)
|
||||
}
|
||||
removalID := args[0]
|
||||
|
||||
mAPI := mustNewMembersAPI(c)
|
||||
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
defer cancel()
|
||||
// Get the list of members.
|
||||
members, err := mAPI.List(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error while verifying ID against known members:", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
// Sanity check the input.
|
||||
foundID := false
|
||||
for _, m := range members {
|
||||
if m.ID == removalID {
|
||||
foundID = true
|
||||
}
|
||||
if m.Name == removalID {
|
||||
// Note that, so long as it's not ambiguous, we *could* do the right thing by name here.
|
||||
fmt.Fprintf(os.Stderr, "Found a member named %s; if this is correct, please use its ID, eg:\n\tetcdctl member remove %s\n", m.Name, m.ID)
|
||||
fmt.Fprintf(os.Stderr, "For more details, read the documentation at https://github.com/coreos/etcd/blob/master/Documentation/runtime-configuration.md#remove-a-member\n\n")
|
||||
}
|
||||
}
|
||||
if !foundID {
|
||||
fmt.Fprintf(os.Stderr, "Couldn't find a member in the cluster with an ID of %s.\n", removalID)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Actually attempt to remove the member.
|
||||
err = mAPI.Remove(ctx, removalID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Received an error trying to remove member %s: %s", removalID, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Removed member %s from cluster\n", removalID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func actionMemberUpdate(c *cli.Context) error {
|
||||
args := c.Args()
|
||||
if len(args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, "Provide an ID and a list of comma separated peerURL (0xabcd http://example.com,http://example1.com)")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
mAPI := mustNewMembersAPI(c)
|
||||
|
||||
mid := args[0]
|
||||
urls := args[1]
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
err := mAPI.Update(ctx, mid, strings.Split(urls, ","))
|
||||
cancel()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Updated member with ID %s in cluster\n", mid)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// NewMakeCommand returns the CLI command for "mk".
|
||||
func NewMakeCommand() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "mk",
|
||||
Usage: "make a new key with a given value",
|
||||
ArgsUsage: "<key> <value>",
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{Name: "in-order", Usage: "create in-order key under directory <key>"},
|
||||
cli.IntFlag{Name: "ttl", Value: 0, Usage: "key time-to-live in seconds"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
mkCommandFunc(c, mustNewKeyAPI(c))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// mkCommandFunc executes the "mk" command.
|
||||
func mkCommandFunc(c *cli.Context, ki client.KeysAPI) {
|
||||
if len(c.Args()) == 0 {
|
||||
handleError(c, ExitBadArgs, errors.New("key required"))
|
||||
}
|
||||
key := c.Args()[0]
|
||||
value, err := argOrStdin(c.Args(), os.Stdin, 1)
|
||||
if err != nil {
|
||||
handleError(c, ExitBadArgs, errors.New("value required"))
|
||||
}
|
||||
|
||||
ttl := c.Int("ttl")
|
||||
inorder := c.Bool("in-order")
|
||||
|
||||
var resp *client.Response
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
if !inorder {
|
||||
// Since PrevNoExist means that the Node must not exist previously,
|
||||
// this Set method always creates a new key. Therefore, mk command
|
||||
// succeeds only if the key did not previously exist, and the command
|
||||
// prevents one from overwriting values accidentally.
|
||||
resp, err = ki.Set(ctx, key, value, &client.SetOptions{TTL: time.Duration(ttl) * time.Second, PrevExist: client.PrevNoExist})
|
||||
} else {
|
||||
// If in-order flag is specified then create an inorder key under
|
||||
// the directory identified by the key argument.
|
||||
resp, err = ki.CreateInOrder(ctx, key, value, &client.CreateInOrderOptions{TTL: time.Duration(ttl) * time.Second})
|
||||
}
|
||||
cancel()
|
||||
if err != nil {
|
||||
handleError(c, ExitServerError, err)
|
||||
}
|
||||
|
||||
printResponseKey(resp, c.GlobalString("output"))
|
||||
}
|
||||
59
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/mkdir_command.go
generated
vendored
Normal file
59
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/mkdir_command.go
generated
vendored
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// NewMakeDirCommand returns the CLI command for "mkdir".
|
||||
func NewMakeDirCommand() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "mkdir",
|
||||
Usage: "make a new directory",
|
||||
ArgsUsage: "<key>",
|
||||
Flags: []cli.Flag{
|
||||
cli.IntFlag{Name: "ttl", Value: 0, Usage: "key time-to-live in seconds"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
mkdirCommandFunc(c, mustNewKeyAPI(c), client.PrevNoExist)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// mkdirCommandFunc executes the "mkdir" command.
|
||||
func mkdirCommandFunc(c *cli.Context, ki client.KeysAPI, prevExist client.PrevExistType) {
|
||||
if len(c.Args()) == 0 {
|
||||
handleError(c, ExitBadArgs, errors.New("key required"))
|
||||
}
|
||||
|
||||
key := c.Args()[0]
|
||||
ttl := c.Int("ttl")
|
||||
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
resp, err := ki.Set(ctx, key, "", &client.SetOptions{TTL: time.Duration(ttl) * time.Second, Dir: true, PrevExist: prevExist})
|
||||
cancel()
|
||||
if err != nil {
|
||||
handleError(c, ExitServerError, err)
|
||||
}
|
||||
if c.GlobalString("output") != "simple" {
|
||||
printResponseKey(resp, c.GlobalString("output"))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// NewRemoveCommand returns the CLI command for "rm".
|
||||
func NewRemoveCommand() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "rm",
|
||||
Usage: "remove a key or a directory",
|
||||
ArgsUsage: "<key>",
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{Name: "dir", Usage: "removes the key if it is an empty directory or a key-value pair"},
|
||||
cli.BoolFlag{Name: "recursive, r", Usage: "removes the key and all child keys(if it is a directory)"},
|
||||
cli.StringFlag{Name: "with-value", Value: "", Usage: "previous value"},
|
||||
cli.IntFlag{Name: "with-index", Value: 0, Usage: "previous index"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
rmCommandFunc(c, mustNewKeyAPI(c))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// rmCommandFunc executes the "rm" command.
|
||||
func rmCommandFunc(c *cli.Context, ki client.KeysAPI) {
|
||||
if len(c.Args()) == 0 {
|
||||
handleError(c, ExitBadArgs, errors.New("key required"))
|
||||
}
|
||||
key := c.Args()[0]
|
||||
recursive := c.Bool("recursive")
|
||||
dir := c.Bool("dir")
|
||||
prevValue := c.String("with-value")
|
||||
prevIndex := c.Int("with-index")
|
||||
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
resp, err := ki.Delete(ctx, key, &client.DeleteOptions{PrevIndex: uint64(prevIndex), PrevValue: prevValue, Dir: dir, Recursive: recursive})
|
||||
cancel()
|
||||
if err != nil {
|
||||
handleError(c, ExitServerError, err)
|
||||
}
|
||||
if !resp.Node.Dir || c.GlobalString("output") != "simple" {
|
||||
printResponseKey(resp, c.GlobalString("output"))
|
||||
}
|
||||
}
|
||||
54
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/rmdir_command.go
generated
vendored
Normal file
54
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/rmdir_command.go
generated
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// NewRemoveDirCommand returns the CLI command for "rmdir".
|
||||
func NewRemoveDirCommand() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "rmdir",
|
||||
Usage: "removes the key if it is an empty directory or a key-value pair",
|
||||
ArgsUsage: "<key>",
|
||||
Action: func(c *cli.Context) error {
|
||||
rmdirCommandFunc(c, mustNewKeyAPI(c))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// rmdirCommandFunc executes the "rmdir" command.
|
||||
func rmdirCommandFunc(c *cli.Context, ki client.KeysAPI) {
|
||||
if len(c.Args()) == 0 {
|
||||
handleError(c, ExitBadArgs, errors.New("key required"))
|
||||
}
|
||||
key := c.Args()[0]
|
||||
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
resp, err := ki.Delete(ctx, key, &client.DeleteOptions{Dir: true})
|
||||
cancel()
|
||||
if err != nil {
|
||||
handleError(c, ExitServerError, err)
|
||||
}
|
||||
|
||||
if !resp.Node.Dir || c.GlobalString("output") != "simple" {
|
||||
printResponseKey(resp, c.GlobalString("output"))
|
||||
}
|
||||
}
|
||||
255
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/role_commands.go
generated
vendored
Normal file
255
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/role_commands.go
generated
vendored
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
"github.com/coreos/etcd/pkg/pathutil"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func NewRoleCommands() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "role",
|
||||
Usage: "role add, grant and revoke subcommands",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "add",
|
||||
Usage: "add a new role for the etcd cluster",
|
||||
ArgsUsage: "<role> ",
|
||||
Action: actionRoleAdd,
|
||||
},
|
||||
{
|
||||
Name: "get",
|
||||
Usage: "get details for a role",
|
||||
ArgsUsage: "<role>",
|
||||
Action: actionRoleGet,
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
Usage: "list all roles",
|
||||
ArgsUsage: " ",
|
||||
Action: actionRoleList,
|
||||
},
|
||||
{
|
||||
Name: "remove",
|
||||
Usage: "remove a role from the etcd cluster",
|
||||
ArgsUsage: "<role>",
|
||||
Action: actionRoleRemove,
|
||||
},
|
||||
{
|
||||
Name: "grant",
|
||||
Usage: "grant path matches to an etcd role",
|
||||
ArgsUsage: "<role>",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{Name: "path", Value: "", Usage: "Path granted for the role to access"},
|
||||
cli.BoolFlag{Name: "read", Usage: "Grant read-only access"},
|
||||
cli.BoolFlag{Name: "write", Usage: "Grant write-only access"},
|
||||
cli.BoolFlag{Name: "readwrite, rw", Usage: "Grant read-write access"},
|
||||
},
|
||||
Action: actionRoleGrant,
|
||||
},
|
||||
{
|
||||
Name: "revoke",
|
||||
Usage: "revoke path matches for an etcd role",
|
||||
ArgsUsage: "<role>",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{Name: "path", Value: "", Usage: "Path revoked for the role to access"},
|
||||
cli.BoolFlag{Name: "read", Usage: "Revoke read access"},
|
||||
cli.BoolFlag{Name: "write", Usage: "Revoke write access"},
|
||||
cli.BoolFlag{Name: "readwrite, rw", Usage: "Revoke read-write access"},
|
||||
},
|
||||
Action: actionRoleRevoke,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewAuthRoleAPI(c *cli.Context) client.AuthRoleAPI {
|
||||
hc := mustNewClient(c)
|
||||
|
||||
if c.GlobalBool("debug") {
|
||||
fmt.Fprintf(os.Stderr, "Cluster-Endpoints: %s\n", strings.Join(hc.Endpoints(), ", "))
|
||||
}
|
||||
|
||||
return client.NewAuthRoleAPI(hc)
|
||||
}
|
||||
|
||||
func actionRoleList(c *cli.Context) error {
|
||||
if len(c.Args()) != 0 {
|
||||
fmt.Fprintln(os.Stderr, "No arguments accepted")
|
||||
os.Exit(1)
|
||||
}
|
||||
r := mustNewAuthRoleAPI(c)
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
roles, err := r.ListRoles(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
fmt.Printf("%s\n", role)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func actionRoleAdd(c *cli.Context) error {
|
||||
api, role := mustRoleAPIAndName(c)
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
defer cancel()
|
||||
currentRole, _ := api.GetRole(ctx, role)
|
||||
if currentRole != nil {
|
||||
fmt.Fprintf(os.Stderr, "Role %s already exists\n", role)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err := api.AddRole(ctx, role)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Role %s created\n", role)
|
||||
return nil
|
||||
}
|
||||
|
||||
func actionRoleRemove(c *cli.Context) error {
|
||||
api, role := mustRoleAPIAndName(c)
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
err := api.RemoveRole(ctx, role)
|
||||
cancel()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Role %s removed\n", role)
|
||||
return nil
|
||||
}
|
||||
|
||||
func actionRoleGrant(c *cli.Context) error {
|
||||
roleGrantRevoke(c, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func actionRoleRevoke(c *cli.Context) error {
|
||||
roleGrantRevoke(c, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func roleGrantRevoke(c *cli.Context, grant bool) {
|
||||
path := c.String("path")
|
||||
if path == "" {
|
||||
fmt.Fprintln(os.Stderr, "No path specified; please use `--path`")
|
||||
os.Exit(1)
|
||||
}
|
||||
if pathutil.CanonicalURLPath(path) != path {
|
||||
fmt.Fprintf(os.Stderr, "Not canonical path; please use `--path=%s`\n", pathutil.CanonicalURLPath(path))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
read := c.Bool("read")
|
||||
write := c.Bool("write")
|
||||
rw := c.Bool("readwrite")
|
||||
permcount := 0
|
||||
for _, v := range []bool{read, write, rw} {
|
||||
if v {
|
||||
permcount++
|
||||
}
|
||||
}
|
||||
if permcount != 1 {
|
||||
fmt.Fprintln(os.Stderr, "Please specify exactly one of --read, --write or --readwrite")
|
||||
os.Exit(1)
|
||||
}
|
||||
var permType client.PermissionType
|
||||
switch {
|
||||
case read:
|
||||
permType = client.ReadPermission
|
||||
case write:
|
||||
permType = client.WritePermission
|
||||
case rw:
|
||||
permType = client.ReadWritePermission
|
||||
}
|
||||
|
||||
api, role := mustRoleAPIAndName(c)
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
defer cancel()
|
||||
currentRole, err := api.GetRole(ctx, role)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
var newRole *client.Role
|
||||
if grant {
|
||||
newRole, err = api.GrantRoleKV(ctx, role, []string{path}, permType)
|
||||
} else {
|
||||
newRole, err = api.RevokeRoleKV(ctx, role, []string{path}, permType)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
if reflect.DeepEqual(newRole, currentRole) {
|
||||
if grant {
|
||||
fmt.Printf("Role unchanged; already granted")
|
||||
} else {
|
||||
fmt.Printf("Role unchanged; already revoked")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Role %s updated\n", role)
|
||||
}
|
||||
|
||||
func actionRoleGet(c *cli.Context) error {
|
||||
api, rolename := mustRoleAPIAndName(c)
|
||||
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
role, err := api.GetRole(ctx, rolename)
|
||||
cancel()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Role: %s\n", role.Role)
|
||||
fmt.Printf("KV Read:\n")
|
||||
for _, v := range role.Permissions.KV.Read {
|
||||
fmt.Printf("\t%s\n", v)
|
||||
}
|
||||
fmt.Printf("KV Write:\n")
|
||||
for _, v := range role.Permissions.KV.Write {
|
||||
fmt.Printf("\t%s\n", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustRoleAPIAndName(c *cli.Context) (client.AuthRoleAPI, string) {
|
||||
args := c.Args()
|
||||
if len(args) != 1 {
|
||||
fmt.Fprintln(os.Stderr, "Please provide a role name")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
api := mustNewAuthRoleAPI(c)
|
||||
return api, name
|
||||
}
|
||||
73
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/set_command.go
generated
vendored
Normal file
73
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/set_command.go
generated
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// NewSetCommand returns the CLI command for "set".
|
||||
func NewSetCommand() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "set",
|
||||
Usage: "set the value of a key",
|
||||
ArgsUsage: "<key> <value>",
|
||||
Description: `Set sets the value of a key.
|
||||
|
||||
When <value> begins with '-', <value> is interpreted as a flag.
|
||||
Insert '--' for workaround:
|
||||
|
||||
$ set -- <key> <value>`,
|
||||
Flags: []cli.Flag{
|
||||
cli.IntFlag{Name: "ttl", Value: 0, Usage: "key time-to-live in seconds"},
|
||||
cli.StringFlag{Name: "swap-with-value", Value: "", Usage: "previous value"},
|
||||
cli.IntFlag{Name: "swap-with-index", Value: 0, Usage: "previous index"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
setCommandFunc(c, mustNewKeyAPI(c))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// setCommandFunc executes the "set" command.
|
||||
func setCommandFunc(c *cli.Context, ki client.KeysAPI) {
|
||||
if len(c.Args()) == 0 {
|
||||
handleError(c, ExitBadArgs, errors.New("key required"))
|
||||
}
|
||||
key := c.Args()[0]
|
||||
value, err := argOrStdin(c.Args(), os.Stdin, 1)
|
||||
if err != nil {
|
||||
handleError(c, ExitBadArgs, errors.New("value required"))
|
||||
}
|
||||
|
||||
ttl := c.Int("ttl")
|
||||
prevValue := c.String("swap-with-value")
|
||||
prevIndex := c.Int("swap-with-index")
|
||||
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
resp, err := ki.Set(ctx, key, value, &client.SetOptions{TTL: time.Duration(ttl) * time.Second, PrevIndex: uint64(prevIndex), PrevValue: prevValue})
|
||||
cancel()
|
||||
if err != nil {
|
||||
handleError(c, ExitServerError, err)
|
||||
}
|
||||
|
||||
printResponseKey(resp, c.GlobalString("output"))
|
||||
}
|
||||
36
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/set_dir_command.go
generated
vendored
Normal file
36
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/set_dir_command.go
generated
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/client"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// NewSetDirCommand returns the CLI command for "setDir".
|
||||
func NewSetDirCommand() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "setdir",
|
||||
Usage: "create a new directory or update an existing directory TTL",
|
||||
ArgsUsage: "<key>",
|
||||
Flags: []cli.Flag{
|
||||
cli.IntFlag{Name: "ttl", Value: 0, Usage: "key time-to-live in seconds"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
mkdirCommandFunc(c, mustNewKeyAPI(c), client.PrevIgnore)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
63
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/update_command.go
generated
vendored
Normal file
63
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/update_command.go
generated
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// NewUpdateCommand returns the CLI command for "update".
|
||||
func NewUpdateCommand() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "update",
|
||||
Usage: "update an existing key with a given value",
|
||||
ArgsUsage: "<key> <value>",
|
||||
Flags: []cli.Flag{
|
||||
cli.IntFlag{Name: "ttl", Value: 0, Usage: "key time-to-live in seconds"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
updateCommandFunc(c, mustNewKeyAPI(c))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// updateCommandFunc executes the "update" command.
|
||||
func updateCommandFunc(c *cli.Context, ki client.KeysAPI) {
|
||||
if len(c.Args()) == 0 {
|
||||
handleError(c, ExitBadArgs, errors.New("key required"))
|
||||
}
|
||||
key := c.Args()[0]
|
||||
value, err := argOrStdin(c.Args(), os.Stdin, 1)
|
||||
if err != nil {
|
||||
handleError(c, ExitBadArgs, errors.New("value required"))
|
||||
}
|
||||
|
||||
ttl := c.Int("ttl")
|
||||
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
resp, err := ki.Set(ctx, key, value, &client.SetOptions{TTL: time.Duration(ttl) * time.Second, PrevExist: client.PrevExist})
|
||||
cancel()
|
||||
if err != nil {
|
||||
handleError(c, ExitServerError, err)
|
||||
}
|
||||
|
||||
printResponseKey(resp, c.GlobalString("output"))
|
||||
}
|
||||
57
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/update_dir_command.go
generated
vendored
Normal file
57
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/update_dir_command.go
generated
vendored
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// NewUpdateDirCommand returns the CLI command for "updatedir".
|
||||
func NewUpdateDirCommand() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "updatedir",
|
||||
Usage: "update an existing directory",
|
||||
ArgsUsage: "<key> <value>",
|
||||
Flags: []cli.Flag{
|
||||
cli.IntFlag{Name: "ttl", Value: 0, Usage: "key time-to-live in seconds"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
updatedirCommandFunc(c, mustNewKeyAPI(c))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// updatedirCommandFunc executes the "updatedir" command.
|
||||
func updatedirCommandFunc(c *cli.Context, ki client.KeysAPI) {
|
||||
if len(c.Args()) == 0 {
|
||||
handleError(c, ExitBadArgs, errors.New("key required"))
|
||||
}
|
||||
key := c.Args()[0]
|
||||
ttl := c.Int("ttl")
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
resp, err := ki.Set(ctx, key, "", &client.SetOptions{TTL: time.Duration(ttl) * time.Second, Dir: true, PrevExist: client.PrevExist})
|
||||
cancel()
|
||||
if err != nil {
|
||||
handleError(c, ExitServerError, err)
|
||||
}
|
||||
if c.GlobalString("output") != "simple" {
|
||||
printResponseKey(resp, c.GlobalString("output"))
|
||||
}
|
||||
}
|
||||
225
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/user_commands.go
generated
vendored
Normal file
225
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/user_commands.go
generated
vendored
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/bgentry/speakeasy"
|
||||
"github.com/coreos/etcd/client"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func NewUserCommands() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "user",
|
||||
Usage: "user add, grant and revoke subcommands",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "add",
|
||||
Usage: "add a new user for the etcd cluster",
|
||||
ArgsUsage: "<user>",
|
||||
Action: actionUserAdd,
|
||||
},
|
||||
{
|
||||
Name: "get",
|
||||
Usage: "get details for a user",
|
||||
ArgsUsage: "<user>",
|
||||
Action: actionUserGet,
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
Usage: "list all current users",
|
||||
ArgsUsage: "<user>",
|
||||
Action: actionUserList,
|
||||
},
|
||||
{
|
||||
Name: "remove",
|
||||
Usage: "remove a user for the etcd cluster",
|
||||
ArgsUsage: "<user>",
|
||||
Action: actionUserRemove,
|
||||
},
|
||||
{
|
||||
Name: "grant",
|
||||
Usage: "grant roles to an etcd user",
|
||||
ArgsUsage: "<user>",
|
||||
Flags: []cli.Flag{cli.StringSliceFlag{Name: "roles", Value: new(cli.StringSlice), Usage: "List of roles to grant or revoke"}},
|
||||
Action: actionUserGrant,
|
||||
},
|
||||
{
|
||||
Name: "revoke",
|
||||
Usage: "revoke roles for an etcd user",
|
||||
ArgsUsage: "<user>",
|
||||
Flags: []cli.Flag{cli.StringSliceFlag{Name: "roles", Value: new(cli.StringSlice), Usage: "List of roles to grant or revoke"}},
|
||||
Action: actionUserRevoke,
|
||||
},
|
||||
{
|
||||
Name: "passwd",
|
||||
Usage: "change password for a user",
|
||||
ArgsUsage: "<user>",
|
||||
Action: actionUserPasswd,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewAuthUserAPI(c *cli.Context) client.AuthUserAPI {
|
||||
hc := mustNewClient(c)
|
||||
|
||||
if c.GlobalBool("debug") {
|
||||
fmt.Fprintf(os.Stderr, "Cluster-Endpoints: %s\n", strings.Join(hc.Endpoints(), ", "))
|
||||
}
|
||||
|
||||
return client.NewAuthUserAPI(hc)
|
||||
}
|
||||
|
||||
func actionUserList(c *cli.Context) error {
|
||||
if len(c.Args()) != 0 {
|
||||
fmt.Fprintln(os.Stderr, "No arguments accepted")
|
||||
os.Exit(1)
|
||||
}
|
||||
u := mustNewAuthUserAPI(c)
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
users, err := u.ListUsers(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
fmt.Printf("%s\n", user)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func actionUserAdd(c *cli.Context) error {
|
||||
api, userarg := mustUserAPIAndName(c)
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
defer cancel()
|
||||
user, _, _ := getUsernamePassword("", userarg+":")
|
||||
|
||||
_, pass, err := getUsernamePassword("New password: ", userarg)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error reading password:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = api.AddUser(ctx, user, pass)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("User %s created\n", user)
|
||||
return nil
|
||||
}
|
||||
|
||||
func actionUserRemove(c *cli.Context) error {
|
||||
api, user := mustUserAPIAndName(c)
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
err := api.RemoveUser(ctx, user)
|
||||
cancel()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("User %s removed\n", user)
|
||||
return nil
|
||||
}
|
||||
|
||||
func actionUserPasswd(c *cli.Context) error {
|
||||
api, user := mustUserAPIAndName(c)
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
defer cancel()
|
||||
pass, err := speakeasy.Ask("New password: ")
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error reading password:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
_, err = api.ChangePassword(ctx, user, pass)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Password updated\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func actionUserGrant(c *cli.Context) error {
|
||||
userGrantRevoke(c, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func actionUserRevoke(c *cli.Context) error {
|
||||
userGrantRevoke(c, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func userGrantRevoke(c *cli.Context, grant bool) {
|
||||
roles := c.StringSlice("roles")
|
||||
if len(roles) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "No roles specified; please use `--roles`")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
defer cancel()
|
||||
|
||||
api, user := mustUserAPIAndName(c)
|
||||
var err error
|
||||
if grant {
|
||||
_, err = api.GrantUser(ctx, user, roles)
|
||||
} else {
|
||||
_, err = api.RevokeUser(ctx, user, roles)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("User %s updated\n", user)
|
||||
}
|
||||
|
||||
func actionUserGet(c *cli.Context) error {
|
||||
api, username := mustUserAPIAndName(c)
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
user, err := api.GetUser(ctx, username)
|
||||
cancel()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("User: %s\n", user.User)
|
||||
fmt.Printf("Roles: %s\n", strings.Join(user.Roles, " "))
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustUserAPIAndName(c *cli.Context) (client.AuthUserAPI, string) {
|
||||
args := c.Args()
|
||||
if len(args) != 1 {
|
||||
fmt.Fprintln(os.Stderr, "Please provide a username")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
api := mustNewAuthUserAPI(c)
|
||||
username := args[0]
|
||||
return api, username
|
||||
}
|
||||
|
|
@ -0,0 +1,337 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
"github.com/coreos/etcd/pkg/transport"
|
||||
|
||||
"github.com/bgentry/speakeasy"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoAvailSrc = errors.New("no available argument and stdin")
|
||||
|
||||
// the maximum amount of time a dial will wait for a connection to setup.
|
||||
// 30s is long enough for most of the network conditions.
|
||||
defaultDialTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
func argOrStdin(args []string, stdin io.Reader, i int) (string, error) {
|
||||
if i < len(args) {
|
||||
return args[i], nil
|
||||
}
|
||||
bytes, err := ioutil.ReadAll(stdin)
|
||||
if string(bytes) == "" || err != nil {
|
||||
return "", ErrNoAvailSrc
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
func getPeersFlagValue(c *cli.Context) []string {
|
||||
peerstr := c.GlobalString("endpoints")
|
||||
|
||||
if peerstr == "" {
|
||||
peerstr = os.Getenv("ETCDCTL_ENDPOINTS")
|
||||
}
|
||||
|
||||
if peerstr == "" {
|
||||
peerstr = c.GlobalString("endpoint")
|
||||
}
|
||||
|
||||
if peerstr == "" {
|
||||
peerstr = os.Getenv("ETCDCTL_ENDPOINT")
|
||||
}
|
||||
|
||||
if peerstr == "" {
|
||||
peerstr = c.GlobalString("peers")
|
||||
}
|
||||
|
||||
if peerstr == "" {
|
||||
peerstr = os.Getenv("ETCDCTL_PEERS")
|
||||
}
|
||||
|
||||
// If we still don't have peers, use a default
|
||||
if peerstr == "" {
|
||||
peerstr = "http://127.0.0.1:2379,http://127.0.0.1:4001"
|
||||
}
|
||||
|
||||
return strings.Split(peerstr, ",")
|
||||
}
|
||||
|
||||
func getDomainDiscoveryFlagValue(c *cli.Context) ([]string, error) {
|
||||
domainstr, insecure := getDiscoveryDomain(c)
|
||||
|
||||
// If we still don't have domain discovery, return nothing
|
||||
if domainstr == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
discoverer := client.NewSRVDiscover()
|
||||
eps, err := discoverer.Discover(domainstr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if insecure {
|
||||
return eps, err
|
||||
}
|
||||
// strip insecure connections
|
||||
ret := []string{}
|
||||
for _, ep := range eps {
|
||||
if strings.HasPrefix("http://", ep) {
|
||||
fmt.Fprintf(os.Stderr, "ignoring discovered insecure endpoint %q\n", ep)
|
||||
continue
|
||||
}
|
||||
ret = append(ret, ep)
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func getDiscoveryDomain(c *cli.Context) (domainstr string, insecure bool) {
|
||||
domainstr = c.GlobalString("discovery-srv")
|
||||
// Use an environment variable if nothing was supplied on the
|
||||
// command line
|
||||
if domainstr == "" {
|
||||
domainstr = os.Getenv("ETCDCTL_DISCOVERY_SRV")
|
||||
}
|
||||
insecure = c.GlobalBool("insecure-discovery") || (os.Getenv("ETCDCTL_INSECURE_DISCOVERY") != "")
|
||||
return domainstr, insecure
|
||||
}
|
||||
|
||||
func getEndpoints(c *cli.Context) ([]string, error) {
|
||||
eps, err := getDomainDiscoveryFlagValue(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If domain discovery returns no endpoints, check peer flag
|
||||
if len(eps) == 0 {
|
||||
eps = getPeersFlagValue(c)
|
||||
}
|
||||
|
||||
for i, ep := range eps {
|
||||
u, err := url.Parse(ep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.Scheme == "" {
|
||||
u.Scheme = "http"
|
||||
}
|
||||
|
||||
eps[i] = u.String()
|
||||
}
|
||||
|
||||
return eps, nil
|
||||
}
|
||||
|
||||
func getTransport(c *cli.Context) (*http.Transport, error) {
|
||||
cafile := c.GlobalString("ca-file")
|
||||
certfile := c.GlobalString("cert-file")
|
||||
keyfile := c.GlobalString("key-file")
|
||||
|
||||
// Use an environment variable if nothing was supplied on the
|
||||
// command line
|
||||
if cafile == "" {
|
||||
cafile = os.Getenv("ETCDCTL_CA_FILE")
|
||||
}
|
||||
if certfile == "" {
|
||||
certfile = os.Getenv("ETCDCTL_CERT_FILE")
|
||||
}
|
||||
if keyfile == "" {
|
||||
keyfile = os.Getenv("ETCDCTL_KEY_FILE")
|
||||
}
|
||||
|
||||
discoveryDomain, insecure := getDiscoveryDomain(c)
|
||||
if insecure {
|
||||
discoveryDomain = ""
|
||||
}
|
||||
tls := transport.TLSInfo{
|
||||
CertFile: certfile,
|
||||
KeyFile: keyfile,
|
||||
ServerName: discoveryDomain,
|
||||
TrustedCAFile: cafile,
|
||||
}
|
||||
|
||||
dialTimeout := defaultDialTimeout
|
||||
totalTimeout := c.GlobalDuration("total-timeout")
|
||||
if totalTimeout != 0 && totalTimeout < dialTimeout {
|
||||
dialTimeout = totalTimeout
|
||||
}
|
||||
return transport.NewTransport(tls, dialTimeout)
|
||||
}
|
||||
|
||||
func getUsernamePasswordFromFlag(usernameFlag string) (username string, password string, err error) {
|
||||
return getUsernamePassword("Password: ", usernameFlag)
|
||||
}
|
||||
|
||||
func getUsernamePassword(prompt, usernameFlag string) (username string, password string, err error) {
|
||||
colon := strings.Index(usernameFlag, ":")
|
||||
if colon == -1 {
|
||||
username = usernameFlag
|
||||
// Prompt for the password.
|
||||
password, err = speakeasy.Ask(prompt)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
} else {
|
||||
username = usernameFlag[:colon]
|
||||
password = usernameFlag[colon+1:]
|
||||
}
|
||||
return username, password, nil
|
||||
}
|
||||
|
||||
func mustNewKeyAPI(c *cli.Context) client.KeysAPI {
|
||||
return client.NewKeysAPI(mustNewClient(c))
|
||||
}
|
||||
|
||||
func mustNewMembersAPI(c *cli.Context) client.MembersAPI {
|
||||
return client.NewMembersAPI(mustNewClient(c))
|
||||
}
|
||||
|
||||
func mustNewClient(c *cli.Context) client.Client {
|
||||
hc, err := newClient(c)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
debug := c.GlobalBool("debug")
|
||||
if debug {
|
||||
client.EnablecURLDebug()
|
||||
}
|
||||
|
||||
if !c.GlobalBool("no-sync") {
|
||||
if debug {
|
||||
fmt.Fprintf(os.Stderr, "start to sync cluster using endpoints(%s)\n", strings.Join(hc.Endpoints(), ","))
|
||||
}
|
||||
ctx, cancel := contextWithTotalTimeout(c)
|
||||
err := hc.Sync(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
if err == client.ErrNoEndpoints {
|
||||
fmt.Fprintf(os.Stderr, "etcd cluster has no published client endpoints.\n")
|
||||
fmt.Fprintf(os.Stderr, "Try '--no-sync' if you want to access non-published client endpoints(%s).\n", strings.Join(hc.Endpoints(), ","))
|
||||
handleError(c, ExitServerError, err)
|
||||
}
|
||||
if isConnectionError(err) {
|
||||
handleError(c, ExitBadConnection, err)
|
||||
}
|
||||
}
|
||||
if debug {
|
||||
fmt.Fprintf(os.Stderr, "got endpoints(%s) after sync\n", strings.Join(hc.Endpoints(), ","))
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
fmt.Fprintf(os.Stderr, "Cluster-Endpoints: %s\n", strings.Join(hc.Endpoints(), ", "))
|
||||
}
|
||||
|
||||
return hc
|
||||
}
|
||||
|
||||
func isConnectionError(err error) bool {
|
||||
switch t := err.(type) {
|
||||
case *client.ClusterError:
|
||||
for _, cerr := range t.Errors {
|
||||
if !isConnectionError(cerr) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case *net.OpError:
|
||||
if t.Op == "dial" || t.Op == "read" {
|
||||
return true
|
||||
}
|
||||
return isConnectionError(t.Err)
|
||||
case net.Error:
|
||||
if t.Timeout() {
|
||||
return true
|
||||
}
|
||||
case syscall.Errno:
|
||||
if t == syscall.ECONNREFUSED {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func mustNewClientNoSync(c *cli.Context) client.Client {
|
||||
hc, err := newClient(c)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if c.GlobalBool("debug") {
|
||||
fmt.Fprintf(os.Stderr, "Cluster-Endpoints: %s\n", strings.Join(hc.Endpoints(), ", "))
|
||||
client.EnablecURLDebug()
|
||||
}
|
||||
|
||||
return hc
|
||||
}
|
||||
|
||||
func newClient(c *cli.Context) (client.Client, error) {
|
||||
eps, err := getEndpoints(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tr, err := getTransport(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := client.Config{
|
||||
Transport: tr,
|
||||
Endpoints: eps,
|
||||
HeaderTimeoutPerRequest: c.GlobalDuration("timeout"),
|
||||
}
|
||||
|
||||
uFlag := c.GlobalString("username")
|
||||
|
||||
if uFlag == "" {
|
||||
uFlag = os.Getenv("ETCDCTL_USERNAME")
|
||||
}
|
||||
|
||||
if uFlag != "" {
|
||||
username, password, err := getUsernamePasswordFromFlag(uFlag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.Username = username
|
||||
cfg.Password = password
|
||||
}
|
||||
|
||||
return client.New(cfg)
|
||||
}
|
||||
|
||||
func contextWithTotalTimeout(c *cli.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), c.GlobalDuration("total-timeout"))
|
||||
}
|
||||
86
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/watch_command.go
generated
vendored
Normal file
86
vendor/github.com/coreos/etcd/etcdctl/ctlv2/command/watch_command.go
generated
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright 2015 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 command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// NewWatchCommand returns the CLI command for "watch".
|
||||
func NewWatchCommand() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "watch",
|
||||
Usage: "watch a key for changes",
|
||||
ArgsUsage: "<key>",
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{Name: "forever, f", Usage: "forever watch a key until CTRL+C"},
|
||||
cli.IntFlag{Name: "after-index", Value: 0, Usage: "watch after the given index"},
|
||||
cli.BoolFlag{Name: "recursive, r", Usage: "returns all values for key and child keys"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
watchCommandFunc(c, mustNewKeyAPI(c))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// watchCommandFunc executes the "watch" command.
|
||||
func watchCommandFunc(c *cli.Context, ki client.KeysAPI) {
|
||||
if len(c.Args()) == 0 {
|
||||
handleError(c, ExitBadArgs, errors.New("key required"))
|
||||
}
|
||||
key := c.Args()[0]
|
||||
recursive := c.Bool("recursive")
|
||||
forever := c.Bool("forever")
|
||||
index := c.Int("after-index")
|
||||
|
||||
stop := false
|
||||
w := ki.Watcher(key, &client.WatcherOptions{AfterIndex: uint64(index), Recursive: recursive})
|
||||
|
||||
sigch := make(chan os.Signal, 1)
|
||||
signal.Notify(sigch, os.Interrupt)
|
||||
|
||||
go func() {
|
||||
<-sigch
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
for !stop {
|
||||
resp, err := w.Next(context.TODO())
|
||||
if err != nil {
|
||||
handleError(c, ExitServerError, err)
|
||||
}
|
||||
if resp.Node.Dir {
|
||||
continue
|
||||
}
|
||||
if recursive {
|
||||
fmt.Printf("[%s] %s\n", resp.Action, resp.Node.Key)
|
||||
}
|
||||
|
||||
printResponseKey(resp, c.GlobalString("output"))
|
||||
|
||||
if !forever {
|
||||
stop = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright 2015 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 ctlv2 contains the main entry point for the etcdctl for v2 API.
|
||||
package ctlv2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/etcdctl/ctlv2/command"
|
||||
"github.com/coreos/etcd/version"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func Start() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "etcdctl"
|
||||
app.Version = version.Version
|
||||
cli.VersionPrinter = func(c *cli.Context) {
|
||||
fmt.Fprintf(c.App.Writer, "etcdctl version: %v\n", c.App.Version)
|
||||
fmt.Fprintln(c.App.Writer, "API version: 2")
|
||||
}
|
||||
app.Usage = "A simple command line client for etcd."
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
cli.BoolFlag{Name: "debug", Usage: "output cURL commands which can be used to reproduce the request"},
|
||||
cli.BoolFlag{Name: "no-sync", Usage: "don't synchronize cluster information before sending request"},
|
||||
cli.StringFlag{Name: "output, o", Value: "simple", Usage: "output response in the given format (`simple`, `extended` or `json`)"},
|
||||
cli.StringFlag{Name: "discovery-srv, D", Usage: "domain name to query for SRV records describing cluster endpoints"},
|
||||
cli.BoolFlag{Name: "insecure-discovery", Usage: "accept insecure SRV records describing cluster endpoints"},
|
||||
cli.StringFlag{Name: "peers, C", Value: "", Usage: "DEPRECATED - \"--endpoints\" should be used instead"},
|
||||
cli.StringFlag{Name: "endpoint", Value: "", Usage: "DEPRECATED - \"--endpoints\" should be used instead"},
|
||||
cli.StringFlag{Name: "endpoints", Value: "", Usage: "a comma-delimited list of machine addresses in the cluster (default: \"http://127.0.0.1:2379,http://127.0.0.1:4001\")"},
|
||||
cli.StringFlag{Name: "cert-file", Value: "", Usage: "identify HTTPS client using this SSL certificate file"},
|
||||
cli.StringFlag{Name: "key-file", Value: "", Usage: "identify HTTPS client using this SSL key file"},
|
||||
cli.StringFlag{Name: "ca-file", Value: "", Usage: "verify certificates of HTTPS-enabled servers using this CA bundle"},
|
||||
cli.StringFlag{Name: "username, u", Value: "", Usage: "provide username[:password] and prompt if password is not supplied."},
|
||||
cli.DurationFlag{Name: "timeout", Value: 2 * time.Second, Usage: "connection timeout per request"},
|
||||
cli.DurationFlag{Name: "total-timeout", Value: 5 * time.Second, Usage: "timeout for the command execution (except watch)"},
|
||||
}
|
||||
app.Commands = []cli.Command{
|
||||
command.NewBackupCommand(),
|
||||
command.NewClusterHealthCommand(),
|
||||
command.NewMakeCommand(),
|
||||
command.NewMakeDirCommand(),
|
||||
command.NewRemoveCommand(),
|
||||
command.NewRemoveDirCommand(),
|
||||
command.NewGetCommand(),
|
||||
command.NewLsCommand(),
|
||||
command.NewSetCommand(),
|
||||
command.NewSetDirCommand(),
|
||||
command.NewUpdateCommand(),
|
||||
command.NewUpdateDirCommand(),
|
||||
command.NewWatchCommand(),
|
||||
command.NewExecWatchCommand(),
|
||||
command.NewMemberCommand(),
|
||||
command.NewUserCommands(),
|
||||
command.NewRoleCommands(),
|
||||
command.NewAuthCommands(),
|
||||
}
|
||||
|
||||
err := runCtlV2(app)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2017 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.
|
||||
|
||||
// +build cov
|
||||
|
||||
package ctlv2
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func runCtlV2(app *cli.App) error {
|
||||
return app.Run(strings.Split(os.Getenv("ETCDCTL_ARGS"), "\xe7\xcd"))
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright 2017 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.
|
||||
|
||||
// +build !cov
|
||||
|
||||
package ctlv2
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func runCtlV2(app *cli.App) error {
|
||||
return app.Run(os.Args)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue