featurebase/etcd/embed.go
Seebs d5b61ee8e8 reduce etcd fsyncs during testing
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.
2021-10-01 10:45:08 -05:00

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
}