mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
We disable fsync more consistently in testing, including using etcd's already-existing UnsafeNoFsync option to disable fsyncs in the backing store boltdb used by etcd, to reduce runtime of our tests on MacOS significantly. Corresponding to this, we update etcd by one patch to pick up a locally-invented patch which turns out to be nearly-identical to the upstream fix for "disabling fsync makes boltdb not even bother to write some data sometimes", which caused crashes galore.
1105 lines
30 KiB
Go
1105 lines
30 KiB
Go
// Copyright 2017 Pilosa Corp.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package etcd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/molecula/featurebase/v2/disco"
|
|
"github.com/molecula/featurebase/v2/logger"
|
|
"github.com/molecula/featurebase/v2/topology"
|
|
"github.com/pkg/errors"
|
|
"go.etcd.io/etcd/clientv3"
|
|
"go.etcd.io/etcd/clientv3/clientv3util"
|
|
"go.etcd.io/etcd/embed"
|
|
"go.etcd.io/etcd/etcdserver"
|
|
"go.etcd.io/etcd/etcdserver/api/v3client"
|
|
"go.etcd.io/etcd/mvcc/mvccpb"
|
|
"go.etcd.io/etcd/pkg/transport"
|
|
"go.etcd.io/etcd/pkg/types"
|
|
)
|
|
|
|
type Options struct {
|
|
Name string `toml:"name"`
|
|
Dir string `toml:"dir"`
|
|
LClientURL string `toml:"listen-client-url"`
|
|
AClientURL string `toml:"advertise-client-url"`
|
|
LPeerURL string `toml:"listen-peer-url"`
|
|
APeerURL string `toml:"advertise-peer-url"`
|
|
ClusterURL string `toml:"cluster-url"`
|
|
InitCluster string `toml:"initial-cluster"`
|
|
ClusterName string `toml:"cluster-name"`
|
|
HeartbeatTTL int64 `toml:"heartbeat-ttl"`
|
|
// TLS provided tls files
|
|
TrustedCAFile string `toml:"tls-trusted-cafile"`
|
|
ClientCertFile string `toml:"tls-cert-file"`
|
|
ClientKeyFile string `toml:"tls-key-file"`
|
|
PeerCertFile string `toml:"tls-peer-cert-file"`
|
|
PeerKeyFile string `toml:"tls-peer-key-file"`
|
|
|
|
LPeerSocket []*net.TCPListener
|
|
LClientSocket []*net.TCPListener
|
|
|
|
BootstrapTimeout time.Duration
|
|
UnsafeNoFsync bool `toml:"no-fsync"`
|
|
}
|
|
|
|
var (
|
|
_ disco.DisCo = &Etcd{}
|
|
_ disco.Schemator = &Etcd{}
|
|
_ disco.Stator = &Etcd{}
|
|
_ disco.Metadator = &Etcd{}
|
|
_ disco.Resizer = &Etcd{}
|
|
_ disco.Sharder = &Etcd{}
|
|
)
|
|
|
|
const (
|
|
// We put all the things the node-watcher watches in /node so we
|
|
// can use a single watcher for them.
|
|
nodePrefix = "/node/"
|
|
heartbeatPrefix = nodePrefix + "heartbeat/"
|
|
schemaPrefix = "/schema/"
|
|
resizePrefix = nodePrefix + "resize/"
|
|
metadataPrefix = nodePrefix + "metadata/"
|
|
shardPrefix = "/shard/"
|
|
)
|
|
|
|
var (
|
|
etcdLeaderChanged = etcdserver.ErrLeaderChanged.Error()
|
|
)
|
|
|
|
// nodeData is an internal tracker of the data we're keeping about
|
|
// nodes in etcd, which we update from data collected either directly
|
|
// from the KV, or via heartbeats.
|
|
//
|
|
// Any change to the topology.Node should create a new Node rather
|
|
// than reusing the old one, so we can return the structure and not worry
|
|
// about data races.
|
|
//
|
|
// We have to track the revisions of individual components so we can
|
|
// discard updates which are genuinely out-of-order for a given field,
|
|
// but still handle cases where we get updates to several fields that
|
|
// reach us out of order.
|
|
type nodeData struct {
|
|
heartbeatState string
|
|
resizeState string
|
|
metadata []byte
|
|
topologyNode *topology.Node
|
|
}
|
|
|
|
func (n *nodeData) computedState() disco.NodeState {
|
|
if n.resizeState != "" {
|
|
return disco.NodeStateResizing
|
|
}
|
|
if n.heartbeatState != "" {
|
|
return disco.NodeState(n.heartbeatState)
|
|
}
|
|
return disco.NodeStateUnknown
|
|
}
|
|
|
|
type Etcd struct {
|
|
options Options
|
|
replicas int
|
|
|
|
e *embed.Etcd
|
|
cli *clientv3.Client
|
|
cliMu sync.Mutex
|
|
|
|
heartbeatLeasedKV, resizeLeasedKV *leasedKV
|
|
|
|
// We have a watcher running. watchCancel() cancels its context.
|
|
watchCancel func()
|
|
|
|
// knownNodes and sortedNodes get updated by data coming in from
|
|
// watchers. Any change to the contents of a *topology.Node here
|
|
// should be implemented by making a new one and replacing the pointer,
|
|
// so the old pointer stays valid and can be used.
|
|
nodeMu sync.Mutex
|
|
nodeRev int64
|
|
knownNodes map[string]*nodeData
|
|
sortedNodes []*topology.Node
|
|
nodeStates map[string]disco.NodeState
|
|
nodeStatesDirty bool // do we need to remake the nodeStates map to use it?
|
|
|
|
// we want to inherit parent's logging functionality
|
|
logger logger.Logger
|
|
}
|
|
|
|
func NewEtcd(opt Options, logger logger.Logger, replicas int) *Etcd {
|
|
e := &Etcd{
|
|
options: opt,
|
|
logger: logger,
|
|
replicas: replicas,
|
|
knownNodes: make(map[string]*nodeData),
|
|
nodeStates: make(map[string]disco.NodeState),
|
|
}
|
|
|
|
if e.options.HeartbeatTTL == 0 {
|
|
e.options.HeartbeatTTL = 5 // seconds
|
|
}
|
|
return e
|
|
}
|
|
|
|
// Close implements io.Closer
|
|
func (e *Etcd) Close() error {
|
|
if e.watchCancel != nil {
|
|
e.watchCancel()
|
|
}
|
|
if e.e != nil {
|
|
if e.resizeLeasedKV != nil {
|
|
e.resizeLeasedKV.Stop()
|
|
e.resizeLeasedKV = nil
|
|
}
|
|
if e.heartbeatLeasedKV != nil {
|
|
e.heartbeatLeasedKV.Stop()
|
|
}
|
|
|
|
e.e.Close()
|
|
<-e.e.Server.StopNotify()
|
|
}
|
|
|
|
if e.cli != nil {
|
|
e.cli.Close()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// retryClient attempts to do a thing, but also tries to handle the
|
|
// specific case where the client fails because of a leader election,
|
|
// in which case we need to restart the client and retry the thing.
|
|
//
|
|
// We have to let go of the lock while calling `fn` because some fn are
|
|
// long-lasting ones, like watchNodesOnce. So we grab a local copy of
|
|
// the client object, then call things on that object. This should error
|
|
// out sanely instead of panicing if we close the client while something
|
|
// is running on it.
|
|
func (e *Etcd) retryClient(fn func(cli *clientv3.Client) error) (err error) {
|
|
e.cliMu.Lock()
|
|
cli := e.cli
|
|
e.cliMu.Unlock()
|
|
if err = fn(cli); err == nil || err.Error() != etcdLeaderChanged {
|
|
// either it's nil or it's an error we don't try to handle here
|
|
return err
|
|
}
|
|
// we can't do much with an error from closing e.cli at this point, so
|
|
// we try again.
|
|
e.cliMu.Lock()
|
|
if cli != e.cli {
|
|
cli = e.cli
|
|
e.cliMu.Unlock()
|
|
return fn(cli)
|
|
}
|
|
_ = cli.Close()
|
|
cli = v3client.New(e.e.Server)
|
|
e.cli = cli
|
|
e.cliMu.Unlock()
|
|
return fn(cli)
|
|
}
|
|
|
|
func parseOptions(opt Options) *embed.Config {
|
|
cfg := embed.NewConfig()
|
|
cfg.Debug = false // true gives data races on grpc.EnableTracing in etcd
|
|
cfg.LogLevel = "error"
|
|
cfg.Logger = "zap"
|
|
cfg.Name = opt.Name
|
|
cfg.Dir = opt.Dir
|
|
cfg.InitialClusterToken = opt.ClusterName
|
|
cfg.BootstrapTimeout = opt.BootstrapTimeout
|
|
cfg.LCUrls = types.MustNewURLs([]string{opt.LClientURL})
|
|
cfg.UnsafeNoFsync = opt.UnsafeNoFsync
|
|
if opt.AClientURL != "" {
|
|
cfg.ACUrls = types.MustNewURLs([]string{opt.AClientURL})
|
|
} else {
|
|
cfg.ACUrls = cfg.LCUrls
|
|
}
|
|
cfg.LPUrls = types.MustNewURLs([]string{opt.LPeerURL})
|
|
if opt.APeerURL != "" {
|
|
cfg.APUrls = types.MustNewURLs([]string{opt.APeerURL})
|
|
} else {
|
|
cfg.APUrls = cfg.LPUrls
|
|
}
|
|
|
|
lps := make([]*net.TCPListener, len(opt.LPeerSocket))
|
|
copy(lps, opt.LPeerSocket)
|
|
cfg.LPeerSocket = lps
|
|
|
|
lcs := make([]*net.TCPListener, len(opt.LPeerSocket))
|
|
copy(lcs, opt.LClientSocket)
|
|
cfg.LClientSocket = lcs
|
|
|
|
if opt.InitCluster != "" {
|
|
cfg.InitialCluster = opt.InitCluster
|
|
cfg.ClusterState = embed.ClusterStateFlagNew
|
|
} else {
|
|
cfg.InitialCluster = cfg.Name + "=" + opt.APeerURL
|
|
}
|
|
|
|
if opt.ClusterURL != "" {
|
|
cfg.ClusterState = embed.ClusterStateFlagExisting
|
|
|
|
cli, err := clientv3.NewFromURL(opt.ClusterURL)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer cli.Close()
|
|
|
|
log.Println("Cluster Members:")
|
|
mIDs, mNames, mURLs := memberList(cli)
|
|
for i, id := range mIDs {
|
|
log.Printf("\tid: %d, name: %s, url: %s\n", id, mNames[i], mURLs[i])
|
|
cfg.InitialCluster += "," + mNames[i] + "=" + mURLs[i]
|
|
}
|
|
|
|
log.Println("Joining Cluster:")
|
|
id, name := memberAdd(cli, opt.APeerURL)
|
|
log.Printf("\tid: %d, name: %s\n", id, name)
|
|
}
|
|
// can only use tls if not using pre-configured listeners
|
|
cfg.ClientTLSInfo = transport.TLSInfo{
|
|
TrustedCAFile: opt.TrustedCAFile,
|
|
CertFile: opt.ClientCertFile,
|
|
KeyFile: opt.ClientKeyFile,
|
|
}
|
|
cfg.PeerTLSInfo = transport.TLSInfo{
|
|
TrustedCAFile: opt.TrustedCAFile,
|
|
CertFile: opt.PeerCertFile,
|
|
KeyFile: opt.PeerKeyFile,
|
|
}
|
|
|
|
return cfg
|
|
}
|
|
|
|
// Start starts etcd and hearbeat
|
|
func (e *Etcd) Start(ctx context.Context) (_ disco.InitialClusterState, err error) {
|
|
opts := parseOptions(e.options)
|
|
state := disco.InitialClusterState(opts.ClusterState)
|
|
|
|
e.e, err = embed.StartEtcd(opts)
|
|
if err != nil {
|
|
return state, errors.Wrap(err, "starting etcd")
|
|
}
|
|
// If we are returning an error, the caller won't be shutting us down
|
|
// later, so we have to stop the server ourselves.
|
|
defer func() {
|
|
if err != nil {
|
|
e.e.Server.Stop()
|
|
}
|
|
}()
|
|
e.cli = v3client.New(e.e.Server)
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return state, ctx.Err()
|
|
|
|
case err := <-e.e.Err():
|
|
return state, err
|
|
|
|
case <-e.e.Server.ReadyNotify():
|
|
members := e.e.Server.Cluster().Members()
|
|
e.nodeMu.Lock()
|
|
defer e.nodeMu.Unlock()
|
|
// mark everything unknown so we show a state for nodes we haven't
|
|
// heard from yet.
|
|
for _, member := range members {
|
|
peerID := member.ID.String()
|
|
e.knownNodes[peerID] = &nodeData{
|
|
topologyNode: &topology.Node{
|
|
ID: peerID,
|
|
State: disco.NodeStateUnknown,
|
|
},
|
|
}
|
|
e.nodeStates[peerID] = disco.NodeStateUnknown
|
|
}
|
|
e.nodeStatesDirty = true
|
|
return state, e.startHeartbeatAndWatcher(ctx)
|
|
}
|
|
}
|
|
|
|
// startHeartbeatAndWatcher spins up the heartbeat, and also a background
|
|
// watcher that watches for changes to events we care about.
|
|
func (e *Etcd) startHeartbeatAndWatcher(ctx context.Context) error {
|
|
key := heartbeatPrefix + e.e.Server.ID().String()
|
|
e.heartbeatLeasedKV = newLeasedKV(e, key, e.options.HeartbeatTTL)
|
|
|
|
if err := e.heartbeatLeasedKV.Start(string(disco.NodeStateStarting)); err != nil {
|
|
return errors.Wrap(err, "startHeartbeat: starting a new heartbeat")
|
|
}
|
|
// WatchNodes does not check for an error, and will need to be shut
|
|
// down later. We only get this far at a point where we're returning
|
|
// a nil error, and thus, the caller is expected to cleanly shut down
|
|
// the server later.
|
|
go e.WatchNodes()
|
|
return nil
|
|
}
|
|
|
|
func (e *Etcd) NodeState(ctx context.Context, peerID string) (disco.NodeState, error) {
|
|
return e.nodeState(ctx, peerID)
|
|
}
|
|
|
|
func (e *Etcd) nodeState(ctx context.Context, peerID string) (disco.NodeState, error) {
|
|
e.nodeMu.Lock()
|
|
defer e.nodeMu.Unlock()
|
|
err := e.populateNodeStates(ctx)
|
|
return e.nodeStates[peerID], err
|
|
}
|
|
|
|
func (e *Etcd) NodeStates(ctx context.Context) (map[string]disco.NodeState, error) {
|
|
e.nodeMu.Lock()
|
|
defer e.nodeMu.Unlock()
|
|
err := e.populateNodeStates(ctx)
|
|
return e.nodeStates, err
|
|
}
|
|
|
|
func (e *Etcd) Started(ctx context.Context) (err error) {
|
|
return e.heartbeatLeasedKV.Set(ctx, string(disco.NodeStateStarted))
|
|
}
|
|
|
|
func (e *Etcd) ID() string {
|
|
if e.e == nil || e.e.Server == nil {
|
|
return ""
|
|
}
|
|
return e.e.Server.ID().String()
|
|
}
|
|
|
|
func (e *Etcd) Peers() []*disco.Peer {
|
|
var peers []*disco.Peer
|
|
for _, member := range e.e.Server.Cluster().Members() {
|
|
peers = append(peers, &disco.Peer{ID: member.ID.String(), URL: member.PickPeerURL()})
|
|
}
|
|
return peers
|
|
}
|
|
|
|
func (e *Etcd) IsLeader() bool {
|
|
if e.e == nil || e.e.Server == nil {
|
|
return false
|
|
}
|
|
return e.e.Server.Leader() == e.e.Server.ID()
|
|
}
|
|
|
|
func (e *Etcd) Leader() *disco.Peer {
|
|
id := e.e.Server.Leader()
|
|
peer := &disco.Peer{ID: id.String()}
|
|
|
|
if m := e.e.Server.Cluster().Member(id); m != nil {
|
|
peer.URL = m.PickPeerURL()
|
|
}
|
|
|
|
return peer
|
|
}
|
|
|
|
func (e *Etcd) ClusterState(ctx context.Context) (out disco.ClusterState, err error) {
|
|
if e.e == nil {
|
|
return disco.ClusterStateUnknown, nil
|
|
}
|
|
|
|
var (
|
|
heartbeats int = 0
|
|
resize bool
|
|
starting bool
|
|
)
|
|
e.nodeMu.Lock()
|
|
err = e.populateNodeStates(ctx)
|
|
states := e.nodeStates
|
|
e.nodeMu.Unlock()
|
|
if err != nil {
|
|
e.logger.Printf("ClusterState %q: getting node states: %v", e.options.Name, states)
|
|
return disco.ClusterStateUnknown, err
|
|
}
|
|
for _, state := range states {
|
|
switch state {
|
|
case disco.NodeStateStarting:
|
|
starting = true
|
|
case disco.NodeStateResizing:
|
|
resize = true
|
|
case disco.NodeStateUnknown:
|
|
continue
|
|
}
|
|
|
|
heartbeats++
|
|
}
|
|
|
|
if resize {
|
|
return disco.ClusterStateResizing, nil
|
|
}
|
|
|
|
if starting {
|
|
return disco.ClusterStateStarting, nil
|
|
}
|
|
|
|
if heartbeats < len(states) {
|
|
if len(states)-heartbeats >= e.replicas {
|
|
return disco.ClusterStateDown, nil
|
|
}
|
|
|
|
return disco.ClusterStateDegraded, nil
|
|
}
|
|
|
|
return disco.ClusterStateNormal, nil
|
|
}
|
|
|
|
func (e *Etcd) Resize(ctx context.Context) (func([]byte) error, error) {
|
|
key := path.Join(resizePrefix, e.e.Server.ID().String())
|
|
if e.resizeLeasedKV == nil {
|
|
e.resizeLeasedKV = newLeasedKV(e, key, e.options.HeartbeatTTL)
|
|
}
|
|
|
|
if err := e.resizeLeasedKV.Start(""); err != nil {
|
|
return nil, errors.Wrap(err, "Resize: creates a new hearbeat")
|
|
}
|
|
|
|
return func(value []byte) error {
|
|
log.Println("Update progress:", key, string(value))
|
|
return e.putKey(ctx, key, string(value), clientv3.WithIgnoreLease())
|
|
}, nil
|
|
}
|
|
|
|
func (e *Etcd) DoneResize() error {
|
|
if e.resizeLeasedKV != nil {
|
|
e.resizeLeasedKV.Stop()
|
|
}
|
|
|
|
e.resizeLeasedKV = nil
|
|
return nil
|
|
}
|
|
|
|
func (e *Etcd) Watch(ctx context.Context, peerID string, onUpdate func([]byte) error) error {
|
|
key := path.Join(resizePrefix, peerID)
|
|
for resp := range e.cli.Watch(ctx, key) {
|
|
if err := resp.Err(); err != nil {
|
|
return errors.Wrapf(err, "Watch: key (%s) response", key)
|
|
}
|
|
|
|
for _, ev := range resp.Events {
|
|
switch ev.Type {
|
|
case mvccpb.PUT:
|
|
if onUpdate != nil && ev.Kv.Value != nil {
|
|
if err := onUpdate(ev.Kv.Value); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
case mvccpb.DELETE:
|
|
// nothing to watch - key was deleted
|
|
return errors.WithMessagef(disco.ErrKeyDeleted, "Watch key %s", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseNodeKey reads heartbeatPrefix + "23" and yields (heartbeatPrefix, "23", nil).
|
|
func parseNodeKey(key []byte) (prefix string, peerID string, err error) {
|
|
// we're looking for things starting with nodePrefix
|
|
if !bytes.HasPrefix(key, []byte(nodePrefix)) {
|
|
return "", "", fmt.Errorf("not a node key: %q", key)
|
|
}
|
|
peerIndex := bytes.LastIndex(key, []byte("/"))
|
|
if peerIndex < 6 {
|
|
return "", "", fmt.Errorf("not a valid node key: %q", key)
|
|
}
|
|
return string(key[:peerIndex+1]), string(key[peerIndex+1:]), nil
|
|
}
|
|
|
|
// deleteNodeData is like putNodeData, but handles deletes rather than cases
|
|
// where a value exists. you should call it with the node mutex locked.
|
|
func (e *Etcd) deleteNodeData(key []byte, revision int64) error {
|
|
prefix, peerID, err := parseNodeKey(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if revision > e.nodeRev {
|
|
e.nodeRev = revision
|
|
}
|
|
switch prefix {
|
|
case heartbeatPrefix:
|
|
if e.knownNodes[peerID] == nil {
|
|
e.knownNodes[peerID] = &nodeData{}
|
|
}
|
|
e.knownNodes[peerID].heartbeatState = ""
|
|
e.nodeStatesDirty = true
|
|
case metadataPrefix:
|
|
if e.knownNodes[peerID] == nil {
|
|
e.knownNodes[peerID] = &nodeData{}
|
|
}
|
|
e.knownNodes[peerID].metadata = nil
|
|
e.knownNodes[peerID].topologyNode = &topology.Node{}
|
|
e.nodeStatesDirty = true
|
|
case resizePrefix:
|
|
if e.knownNodes[peerID] == nil {
|
|
e.knownNodes[peerID] = &nodeData{}
|
|
}
|
|
e.knownNodes[peerID].resizeState = ""
|
|
e.nodeStatesDirty = true
|
|
default:
|
|
return fmt.Errorf("node watch: invalid prefix %q\n", prefix)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// putNodeData does the actual updating of the node state maps, etc,
|
|
// given an incoming heartbeat, metadata, or resizing change. It requires
|
|
// that you already hold the node mutex.
|
|
func (e *Etcd) putNodeData(key []byte, value []byte, revision int64) (err error) {
|
|
prefix, peerID, err := parseNodeKey(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if revision > e.nodeRev {
|
|
e.nodeRev = revision
|
|
}
|
|
switch prefix {
|
|
case heartbeatPrefix:
|
|
if e.knownNodes[peerID] == nil {
|
|
e.knownNodes[peerID] = &nodeData{}
|
|
}
|
|
e.knownNodes[peerID].heartbeatState = string(value)
|
|
e.nodeStatesDirty = true
|
|
case metadataPrefix:
|
|
if e.knownNodes[peerID] == nil {
|
|
e.knownNodes[peerID] = &nodeData{}
|
|
}
|
|
e.knownNodes[peerID].metadata = value
|
|
var newNode topology.Node
|
|
err := json.Unmarshal(value, &newNode)
|
|
if err != nil {
|
|
return fmt.Errorf("json unmarshal of node metadata: %v\n", err)
|
|
}
|
|
e.knownNodes[peerID].topologyNode = &newNode
|
|
// This saves us one remake of the node later, probably.
|
|
e.knownNodes[peerID].topologyNode.State = e.knownNodes[peerID].computedState()
|
|
e.nodeStatesDirty = true
|
|
case resizePrefix:
|
|
if e.knownNodes[peerID] == nil {
|
|
e.knownNodes[peerID] = &nodeData{}
|
|
}
|
|
e.knownNodes[peerID].resizeState = string(value)
|
|
e.nodeStatesDirty = true
|
|
default:
|
|
return fmt.Errorf("node watch: invalid prefix %q\n", prefix)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// compute the states of all the nodes. we compute all of them because
|
|
// we might have returned the old map in response to a query, so we want to
|
|
// make a new one. You should have the node state lock held when you call this.
|
|
func (e *Etcd) populateNodeStates(ctx context.Context) error {
|
|
if !e.nodeStatesDirty {
|
|
return nil
|
|
}
|
|
e.nodeStates = make(map[string]disco.NodeState, len(e.knownNodes))
|
|
e.sortedNodes = make([]*topology.Node, 0, len(e.knownNodes))
|
|
for peerID, data := range e.knownNodes {
|
|
newState := data.computedState()
|
|
e.nodeStates[peerID] = newState
|
|
// update the state with the current state, so we can
|
|
// reuse these nodes later. sortedNodes may end up shorter
|
|
// than the whole node list if we don't have all the nodes
|
|
// yet!
|
|
if data.topologyNode != nil {
|
|
if data.topologyNode.State != newState {
|
|
newNode := *data.topologyNode
|
|
newNode.State = newState
|
|
data.topologyNode = &newNode
|
|
}
|
|
e.sortedNodes = append(e.sortedNodes, data.topologyNode)
|
|
}
|
|
}
|
|
// sort list by ID. list now contains sorted nodes which have their
|
|
// current states.
|
|
sort.Sort(topology.ByID(e.sortedNodes))
|
|
e.nodeStatesDirty = false
|
|
return nil
|
|
}
|
|
|
|
// watchNodesOnce is a helper function to use with the retry logic
|
|
// to let us restart the client if we need to.
|
|
func (e *Etcd) watchNodesOnce(ctx context.Context, cli *clientv3.Client) (err error) {
|
|
e.nodeMu.Lock()
|
|
// we are looking for revisions HIGHER than the highest revision we've
|
|
// currently seen, we don't want one equal to it.
|
|
minRev := e.nodeRev + 1
|
|
e.nodeMu.Unlock()
|
|
for resp := range cli.Watch(ctx, nodePrefix, clientv3.WithPrefix(), clientv3.WithRev(minRev)) {
|
|
if err := resp.Err(); err != nil {
|
|
return err
|
|
}
|
|
// lock the node mutex for this whole process of updating so
|
|
// we never see partial updates; everything that comes into the
|
|
// watcher as a single message will be processed atomically.
|
|
e.nodeMu.Lock()
|
|
for _, ev := range resp.Events {
|
|
switch ev.Type {
|
|
case mvccpb.PUT:
|
|
err := e.putNodeData(ev.Kv.Key, ev.Kv.Value, ev.Kv.ModRevision)
|
|
if err != nil {
|
|
e.logger.Printf("put event: %v", err)
|
|
}
|
|
case mvccpb.DELETE:
|
|
err := e.deleteNodeData(ev.Kv.Key, ev.Kv.ModRevision)
|
|
if err != nil {
|
|
e.logger.Printf("delete event: %v", err)
|
|
}
|
|
default:
|
|
e.logger.Printf("watchp %q: unknown event %#v", e.options.Name, ev)
|
|
}
|
|
}
|
|
e.nodeMu.Unlock()
|
|
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WatchNodes monitors changes to /heartbeat/, /resizing/, and /metadata/;
|
|
// basically, it catches changes to cluster state, but ignores the schema.
|
|
func (e *Etcd) WatchNodes() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
e.watchCancel = cancel
|
|
watchInContext := func(cli *clientv3.Client) error {
|
|
return e.watchNodesOnce(ctx, cli)
|
|
}
|
|
// retryClient will retry on leader failure, but not for other failures
|
|
// such as ErrCompacted which can terminate a watch. But we want to resume
|
|
// watching again as long as our context isn't cancelled. The context
|
|
// should get cancelled when this Etcd gets shut down.
|
|
for ctx.Err() == nil {
|
|
err := e.retryClient(watchInContext)
|
|
if err != nil {
|
|
e.logger.Printf("WatchNodes: error from watch client: %v", err)
|
|
}
|
|
// delay slightly on error so we don't go completely crazy
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
}
|
|
|
|
func (e *Etcd) DeleteNode(ctx context.Context, nodeID string) error {
|
|
id, err := types.IDFromString(nodeID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = e.cli.MemberRemove(ctx, uint64(id))
|
|
if err != nil {
|
|
return errors.Wrap(err, "DeleteNode: removes an existing member from the cluster")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *Etcd) Schema(ctx context.Context) (disco.Schema, error) {
|
|
keys, vals, err := e.getKeyWithPrefix(ctx, schemaPrefix)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// The logic in the following for loop assumes that the list of keys is
|
|
// ordered such that index comes before field, which comes before view.
|
|
// For example:
|
|
// /index1
|
|
// /index1/field1
|
|
// /index1/field1/view1
|
|
// /index1/field1/view2
|
|
// /index1/field2
|
|
// /index2
|
|
// /index2/field1
|
|
//
|
|
m := make(disco.Schema)
|
|
for i, k := range keys {
|
|
tokens := strings.Split(strings.Trim(k, "/"), "/")
|
|
// token[0] contains the schemaPrefix
|
|
|
|
// token[1]: index
|
|
index := tokens[1]
|
|
if _, ok := m[index]; !ok {
|
|
m[index] = &disco.Index{
|
|
Data: vals[i],
|
|
Fields: make(map[string]*disco.Field),
|
|
}
|
|
continue
|
|
}
|
|
flds := m[index].Fields
|
|
|
|
// token[2]: field
|
|
if len(tokens) > 2 {
|
|
field := tokens[2]
|
|
if _, ok := flds[field]; !ok {
|
|
flds[field] = &disco.Field{
|
|
Data: vals[i],
|
|
Views: make(map[string]struct{}),
|
|
}
|
|
continue
|
|
}
|
|
views := flds[field].Views
|
|
|
|
// token[3]: view
|
|
if len(tokens) > 3 {
|
|
view := tokens[3]
|
|
views[view] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (e *Etcd) Metadata(ctx context.Context, peerID string) ([]byte, error) {
|
|
e.nodeMu.Lock()
|
|
defer e.nodeMu.Unlock()
|
|
err := e.populateNodeStates(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, ok := e.knownNodes[peerID]
|
|
if !ok {
|
|
return nil, errors.New("node not found")
|
|
}
|
|
return data.metadata, nil
|
|
}
|
|
|
|
func (e *Etcd) SetMetadata(ctx context.Context, metadata []byte) error {
|
|
err := e.putKey(ctx, path.Join(metadataPrefix,
|
|
e.e.Server.ID().String()),
|
|
string(metadata),
|
|
)
|
|
if err != nil {
|
|
return errors.Wrap(err, "SetMetadata")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *Etcd) CreateIndex(ctx context.Context, name string, val []byte) error {
|
|
key := schemaPrefix + name
|
|
|
|
// Set up Op to write index value as bytes.
|
|
op := clientv3.OpPut(key, "")
|
|
op.WithValueBytes(val)
|
|
|
|
// Check for key existence, and execute Op within a transaction.
|
|
var resp *clientv3.TxnResponse
|
|
err := e.retryClient(func(cli *clientv3.Client) (err error) {
|
|
resp, err = cli.Txn(ctx).
|
|
If(clientv3util.KeyMissing(key)).
|
|
Then(op).
|
|
Commit()
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(err, "executing transaction")
|
|
}
|
|
|
|
if !resp.Succeeded {
|
|
return disco.ErrIndexExists
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *Etcd) Index(ctx context.Context, name string) ([]byte, error) {
|
|
return e.getKeyBytes(ctx, schemaPrefix+name)
|
|
}
|
|
|
|
func (e *Etcd) DeleteIndex(ctx context.Context, name string) (err error) {
|
|
key := schemaPrefix + name
|
|
// Deleting index and fields in one transaction.
|
|
|
|
err = e.retryClient(func(cli *clientv3.Client) error {
|
|
_, err = cli.Txn(ctx).
|
|
If(clientv3.Compare(clientv3.Version(key), ">", -1)).
|
|
Then(
|
|
clientv3.OpDelete(key+"/", clientv3.WithPrefix()), // deleting index fields
|
|
clientv3.OpDelete(key), // deleting index
|
|
).Commit()
|
|
return err
|
|
})
|
|
|
|
return errors.Wrap(err, "DeleteIndex")
|
|
}
|
|
|
|
func (e *Etcd) Field(ctx context.Context, indexName string, name string) ([]byte, error) {
|
|
key := schemaPrefix + indexName + "/" + name
|
|
return e.getKeyBytes(ctx, key)
|
|
}
|
|
|
|
func (e *Etcd) CreateField(ctx context.Context, indexName string, name string, val []byte) error {
|
|
key := schemaPrefix + indexName + "/" + name
|
|
|
|
// Set up Op to write field value as bytes.
|
|
op := clientv3.OpPut(key, "")
|
|
op.WithValueBytes(val)
|
|
|
|
// Check for key existence, and execute Op within a transaction.
|
|
var resp *clientv3.TxnResponse
|
|
|
|
err := e.retryClient(func(cli *clientv3.Client) (err error) {
|
|
resp, err = cli.Txn(ctx).
|
|
If(clientv3util.KeyMissing(key)).
|
|
Then(op).
|
|
Commit()
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(err, "executing transaction")
|
|
}
|
|
|
|
if !resp.Succeeded {
|
|
return disco.ErrFieldExists
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *Etcd) DeleteField(ctx context.Context, indexname string, name string) (err error) {
|
|
key := schemaPrefix + indexname + "/" + name
|
|
// Deleting field and views in one transaction.
|
|
err = e.retryClient(func(cli *clientv3.Client) (err error) {
|
|
_, err = cli.Txn(ctx).
|
|
If(clientv3.Compare(clientv3.Version(key), ">", -1)).
|
|
Then(
|
|
clientv3.OpDelete(key+"/", clientv3.WithPrefix()), // deleting field views
|
|
clientv3.OpDelete(key), // deleting field
|
|
).Commit()
|
|
return err
|
|
})
|
|
|
|
return errors.Wrap(err, "DeleteField")
|
|
}
|
|
|
|
func (e *Etcd) View(ctx context.Context, indexName, fieldName, name string) (bool, error) {
|
|
key := schemaPrefix + indexName + "/" + fieldName + "/" + name
|
|
return e.keyExists(ctx, key)
|
|
}
|
|
|
|
// CreateView differs from CreateIndex and CreateField in that it does not
|
|
// return an error if the view already exists. If this logic needs to be
|
|
// changed, we likely need to return disco.ErrViewExists.
|
|
func (e *Etcd) CreateView(ctx context.Context, indexName, fieldName, name string) (err error) {
|
|
key := schemaPrefix + indexName + "/" + fieldName + "/" + name
|
|
|
|
// Check for key existence, and execute Op within a transaction.
|
|
err = e.retryClient(func(cli *clientv3.Client) (err error) {
|
|
_, err = cli.Txn(ctx).
|
|
If(clientv3util.KeyMissing(key)).
|
|
Then(clientv3.OpPut(key, "")).
|
|
Commit()
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(err, "executing transaction")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *Etcd) DeleteView(ctx context.Context, indexName, fieldName, name string) error {
|
|
return e.delKey(ctx, schemaPrefix+indexName+"/"+fieldName+"/"+name, false)
|
|
}
|
|
|
|
func (e *Etcd) putKey(ctx context.Context, key, val string, opts ...clientv3.OpOption) error {
|
|
err := e.retryClient(func(cli *clientv3.Client) (err error) {
|
|
_, err = cli.Txn(ctx).
|
|
Then(clientv3.OpPut(key, val, opts...)).
|
|
Commit()
|
|
return err
|
|
})
|
|
return errors.Wrapf(err, "putKey: Put(%s, %s)", key, val)
|
|
}
|
|
|
|
func (e *Etcd) getKeyBytes(ctx context.Context, key string) ([]byte, error) {
|
|
// Get the current value for the key.
|
|
op := clientv3.OpGet(key)
|
|
var resp *clientv3.TxnResponse
|
|
err := e.retryClient(func(cli *clientv3.Client) (err error) {
|
|
resp, err = cli.Txn(ctx).Then(op).Commit()
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(resp.Responses) == 0 {
|
|
return nil, disco.ErrKeyDoesNotExist
|
|
}
|
|
|
|
kvs := resp.Responses[0].GetResponseRange().Kvs
|
|
if len(kvs) == 0 {
|
|
return nil, disco.ErrKeyDoesNotExist
|
|
}
|
|
|
|
return kvs[0].Value, nil
|
|
}
|
|
|
|
func (e *Etcd) getKeyWithPrefix(ctx context.Context, key string) (keys []string, values [][]byte, err error) {
|
|
op := clientv3.OpGet(key, clientv3.WithPrefix())
|
|
var resp *clientv3.TxnResponse
|
|
err = e.retryClient(func(cli *clientv3.Client) (err error) {
|
|
resp, err = cli.Txn(ctx).Then(op).Commit()
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, nil, errors.Wrapf(err, "getKeyWithPrefix(%s)", key)
|
|
}
|
|
|
|
if len(resp.Responses) == 0 {
|
|
return nil, nil, disco.ErrKeyDoesNotExist
|
|
}
|
|
|
|
kvs := resp.Responses[0].GetResponseRange().Kvs
|
|
if len(kvs) == 0 {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
keys = make([]string, len(kvs))
|
|
values = make([][]byte, len(kvs))
|
|
for i, kv := range kvs {
|
|
keys[i] = string(kv.Key)
|
|
values[i] = kv.Value
|
|
}
|
|
|
|
return keys, values, nil
|
|
}
|
|
|
|
func (e *Etcd) keyExists(ctx context.Context, key string) (bool, error) {
|
|
var resp *clientv3.TxnResponse
|
|
err := e.retryClient(func(cli *clientv3.Client) (err error) {
|
|
resp, err = cli.Txn(ctx).
|
|
If(clientv3util.KeyExists(key)).
|
|
Then(clientv3.OpGet(key, clientv3.WithCountOnly())).
|
|
Commit()
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !resp.Succeeded {
|
|
return false, nil
|
|
}
|
|
|
|
if len(resp.Responses) == 0 {
|
|
return false, nil
|
|
}
|
|
return resp.Responses[0].GetResponseRange().Count > 0, nil
|
|
}
|
|
|
|
func (e *Etcd) delKey(ctx context.Context, key string, withPrefix bool) (err error) {
|
|
if withPrefix {
|
|
_, err = e.cli.Delete(ctx, key, clientv3.WithPrefix())
|
|
} else {
|
|
_, err = e.cli.Delete(ctx, key)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func memberList(cli *clientv3.Client) (ids []uint64, names []string, urls []string) {
|
|
ml, err := cli.MemberList(context.TODO())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
n := len(ml.Members)
|
|
ids = make([]uint64, n)
|
|
names = make([]string, n)
|
|
urls = make([]string, n)
|
|
|
|
for i, m := range ml.Members {
|
|
ids[i], names[i], urls[i] = m.ID, m.Name, m.PeerURLs[0]
|
|
}
|
|
return
|
|
}
|
|
|
|
func memberAdd(cli *clientv3.Client, peerURL string) (id uint64, name string) {
|
|
ma, err := cli.MemberAdd(context.TODO(), []string{peerURL})
|
|
if err != nil {
|
|
return 0, ""
|
|
}
|
|
|
|
return ma.Member.ID, ma.Member.Name
|
|
}
|
|
|
|
// Shards implements the Sharder interface.
|
|
func (e *Etcd) Shards(ctx context.Context, index, field string) ([][]byte, error) {
|
|
key := path.Join(shardPrefix, index, field)
|
|
_, vals, err := e.getKeyWithPrefix(ctx, key)
|
|
|
|
if errors.Cause(err) == disco.ErrKeyDoesNotExist {
|
|
e.logger.Warnf("key: %s, err: %v", key, err)
|
|
return nil, nil
|
|
}
|
|
|
|
return vals, nil
|
|
}
|
|
|
|
// SetShards implements the Sharder interface.
|
|
func (e *Etcd) SetShards(ctx context.Context, index, field string, shards []byte) error {
|
|
key := path.Join(shardPrefix, index, field, e.e.Server.ID().String())
|
|
|
|
op := clientv3.OpPut(key, "")
|
|
op.WithValueBytes(shards)
|
|
return e.retryClient(func(cli *clientv3.Client) (err error) {
|
|
_, err = cli.Txn(ctx).Then(op).Commit()
|
|
return
|
|
})
|
|
}
|
|
|
|
// Nodes implements the Noder interface. It returns the sorted list of nodes
|
|
// based on the etcd peers.
|
|
func (e *Etcd) Nodes() []*topology.Node {
|
|
e.nodeMu.Lock()
|
|
defer e.nodeMu.Unlock()
|
|
err := e.populateNodeStates(context.TODO())
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return e.sortedNodes
|
|
}
|
|
|
|
// PrimaryNodeID implements the Noder interface.
|
|
func (e *Etcd) PrimaryNodeID(hasher topology.Hasher) string {
|
|
return topology.PrimaryNodeID(e.NodeIDs(), hasher)
|
|
}
|
|
|
|
// NodeIDs returns the list of node IDs in the etcd cluster.
|
|
func (e *Etcd) NodeIDs() []string {
|
|
peers := e.Peers()
|
|
ids := make([]string, len(peers))
|
|
for i, peer := range peers {
|
|
ids[i] = peer.ID
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// SetNodes implements the Noder interface as NOP
|
|
// (because we can't force to set nodes for etcd).
|
|
func (e *Etcd) SetNodes(nodes []*topology.Node) {}
|
|
|
|
// AppendNode implements the Noder interface as NOP
|
|
// (because resizer is responsible for adding new nodes).
|
|
func (e *Etcd) AppendNode(node *topology.Node) {}
|
|
|
|
// RemoveNode implements the Noder interface as NOP
|
|
// (because resizer is responsible for removing existing nodes)
|
|
func (e *Etcd) RemoveNode(nodeID string) bool {
|
|
return false
|
|
}
|