// Copyright 2022 Molecula Corp. (DBA FeatureBase). // SPDX-License-Identifier: Apache-2.0 package etcd import ( "bytes" "context" "encoding/json" "fmt" "net" "path" "sort" "strings" "sync" "time" "github.com/featurebasedb/featurebase/v3/disco" "github.com/featurebasedb/featurebase/v3/logger" "github.com/featurebasedb/featurebase/v3/monitor" "github.com/pkg/errors" "go.etcd.io/etcd/api/v3/mvccpb" "go.etcd.io/etcd/client/pkg/v3/transport" clientv3 "go.etcd.io/etcd/client/v3" clientv3util "go.etcd.io/etcd/client/v3/clientv3util" "go.etcd.io/etcd/pkg/types" "go.etcd.io/etcd/server/v3/embed" "go.etcd.io/etcd/server/v3/etcdserver" "go.etcd.io/etcd/server/v3/etcdserver/api/v3client" ) 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 UnsafeNoFsync bool `toml:"no-fsync"` Cluster string `toml:"static-cluster"` EtcdHosts string `toml:"etcd-hosts"` Id string } var ( _ disco.DisCo = &Etcd{} _ disco.Schemator = &Etcd{} _ disco.Noder = &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/" metadataPrefix = nodePrefix + "metadata/" shardPrefix = "/shard/" ) var errEtcdShuttingDown = errors.New("etcd shutting down") // 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 disco.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 { heartbeat disco.NodeState metadata []byte node *disco.Node } // EtcdServicer provides access to either an embeded etcd server or an external host type EtcdServicer interface { Shutdown() Peers() []*disco.Peer IsLeader() bool Leader() *disco.Peer ID() string Startup(ctx context.Context, state disco.InitialClusterState) (disco.InitialClusterState, error) NewClient() (*clientv3.Client, error) } type EmbeddedEtcd struct { e *embed.Etcd parent *Etcd } func NewEmbeddedEtcd(p *Etcd, opts *embed.Config) (*EmbeddedEtcd, error) { e, err := embed.StartEtcd(opts) if err != nil { return nil, errors.Wrap(err, "starting etcd") } this := &EmbeddedEtcd{} this.parent = p this.e = e p.e = e return this, nil } func (e *EmbeddedEtcd) ID() string { if e.e == nil || e.e.Server == nil { return "" } return e.e.Server.ID().String() } func (e *EmbeddedEtcd) Shutdown() { if e.e != nil { e.e.Close() <-e.e.Server.StopNotify() } } func (e *EmbeddedEtcd) Peers() []*disco.Peer { members := e.e.Server.Cluster().Members() peers := make([]*disco.Peer, 0, len(members)) for _, member := range members { peers = append(peers, &disco.Peer{ID: member.ID.String(), URL: member.PickPeerURL()}) } return peers } func (e *EmbeddedEtcd) IsLeader() bool { if e.e == nil || e.e.Server == nil { return false } return e.e.Server.Leader() == e.e.Server.ID() } func (e *EmbeddedEtcd) 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 *EmbeddedEtcd) Startup(ctx context.Context, state disco.InitialClusterState) (disco.InitialClusterState, error) { 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.parent.nodeMu.Lock() defer e.parent.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.parent.seeNode(peerID) } e.parent.nodesDirty = true return state, e.parent.startHeartbeatAndWatcher(ctx) } } func (e *EmbeddedEtcd) NewClient() (*clientv3.Client, error) { return v3client.New(e.e.Server), nil } type Etcd struct { options Options replicas int e *embed.Etcd // TODO (twg) factor out service EtcdServicer cli *clientv3.Client cliMu sync.Mutex heartbeatLeasedKV *leasedKV // A context for our children (node watcher, lease keepalive) childContext context.Context // function to cancel the child contexts when we're done childCancel func() // knownNodes and sortedNodes get updated by data coming in from // watchers. Any change to the contents of a *disco.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 []*disco.Node // immutable nodes kept in sorted order nodesDirty bool // do we need to recompute sortedNodes? version string // we want to inherit parent's logging functionality logger logger.Logger } func NewEtcd(opt Options, logger logger.Logger, replicas int, version string) *Etcd { e := &Etcd{ options: opt, logger: logger, replicas: replicas, knownNodes: make(map[string]*nodeData), version: version, } if e.options.HeartbeatTTL == 0 { e.options.HeartbeatTTL = 5 // seconds } // !! DEBUG // e.options.HeartbeatTTL = 3600 // seconds // !! DEBUG return e } // Close implements io.Closer func (e *Etcd) Close() error { // tell the heartbeat to stop. we do this before canceling // the context because we want the heartbeat to get a chance // to notify other nodes that it's down. if e.heartbeatLeasedKV != nil { e.heartbeatLeasedKV.Stop() } // cancel the contexts that heartbeat and watcher are using. if e.childCancel != nil { e.childCancel() } // shut down the server, if we have one. e.service.Shutdown() // shut down the client, if we have one. if something's still // using it, we anticipate the client failing its current call, // and any retry will be checking for the child context being // cancelled, first, we hope. if e.cli != nil { e.cli.Close() } return nil } const etcdRetryTimes = 3 // newClient requests a new client which is different from the one // passed in. if we've already changed our client (say, because someone // else already did that) we just return that new one. func (e *Etcd) newClient(cli *clientv3.Client) *clientv3.Client { e.cliMu.Lock() defer e.cliMu.Unlock() if cli != e.cli { cli = e.cli // someone else already reopened. retry. return cli } _ = cli.Close() e.cli, _ = e.service.NewClient() return e.cli } // 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. // // New feature: retryClient can also retry on errTimeout. func (e *Etcd) retryClient(fn func(cli *clientv3.Client) error) (err error) { e.cliMu.Lock() cli := e.cli e.cliMu.Unlock() for tries := 0; tries < etcdRetryTimes; tries++ { start := time.Now() err = fn(cli) switch err { default: msg := err.Error() // this shouldn't be necessary, but empirically, we sometimes // get an error message which has this text, but the error itself // isn't actually etcdserver.ErrLeaderChanged. if strings.Contains(msg, "etcdserver: leader changed") { cli = e.newClient(cli) break } // check that the request hasn't timed out. this can happen with either of these errors if !strings.Contains(msg, "etcdserver: request timed out") && !strings.Contains(msg, "context deadline exceeded") && !strings.Contains(msg, errEtcdShuttingDown.Error()) { // not a known error, also not a wrapped timeout return errors.Wrap(err, "non-retryable error") } fallthrough // treat this as being one of the ErrTimeout derivatives, possibly wrapped. case etcdserver.ErrTimeout, etcdserver.ErrTimeoutDueToLeaderFail, etcdserver.ErrTimeoutDueToConnectionLost, etcdserver.ErrTimeoutLeaderTransfer: // sporadic timeouts are concerning but not necessarily fatal // and can usually be retried. elapsed := time.Since(start) retrying := "" if tries < etcdRetryTimes { retrying = fmt.Sprintf(" (retrying, n=%d)", tries) } e.logger.Warnf("timeout (%v elapsed) on etcd query%s", elapsed, retrying) // Sleep just a touch longer to give things a time to // stabilize. We're mostly relying on the fact that this is a // timeout to give us a reasonable backoff period and keep us // from spamming these. time.Sleep(100 * time.Millisecond) case etcdserver.ErrLeaderChanged: cli = e.newClient(cli) case nil: return nil } } // if we got here, we got a total of three of some combination of // ErrTimeout or ErrLeaderChanged, and we're giving up. return errors.Wrap(err, "exhausted all retries") } func (e *Etcd) parseOptions() (*embed.Config, error) { cfg := embed.NewConfig() cfg.LogLevel = "error" cfg.Logger = "zap" cfg.Name = e.options.Name cfg.Dir = e.options.Dir cfg.InitialClusterToken = e.options.ClusterName cfg.EnableGRPCGateway = false // TODO(twg) 2022/11/03 fixes tests but unsure ramifications var err error cfg.LCUrls, err = types.NewURLs([]string{e.options.LClientURL}) if err != nil { return nil, fmt.Errorf("parsing listen client URL %q: %v", e.options.LClientURL, err) } cfg.UnsafeNoFsync = e.options.UnsafeNoFsync if e.options.AClientURL != "" { cfg.ACUrls, err = types.NewURLs([]string{e.options.AClientURL}) if err != nil { return nil, fmt.Errorf("parsing advertise client URL %q: %v", e.options.AClientURL, err) } } else { cfg.ACUrls = cfg.LCUrls } cfg.LPUrls, err = types.NewURLs([]string{e.options.LPeerURL}) if err != nil { return nil, fmt.Errorf("parsing listen peer URL %q: %v", e.options.LPeerURL, err) } if e.options.APeerURL != "" { cfg.APUrls, err = types.NewURLs([]string{e.options.APeerURL}) if err != nil { return nil, fmt.Errorf("parsing advertise peer URL %q: %v", e.options.APeerURL, err) } } else { cfg.APUrls = cfg.LPUrls } if e.options.InitCluster != "" { // Checks if FB is running the single-node free version or the multi-node // enterprise version. Sentry.io is enabled on single-node. if !AllowCluster() { // %% begin sonarcloud ignore %% monitor.InitErrorMonitor(e.version) e.logger.Infof("Initializing Monitor: Capturing usage metrics") // check for multiple nodes in the cluster and error if present nodes := strings.Split(e.options.InitCluster, ",") if len(nodes) > 1 { return nil, fmt.Errorf("multiple cluster nodes detected - this version of FeatureBase only supports single node. %+v", e.options.InitCluster) } // %% end sonarcloud ignore %% } cfg.InitialCluster = e.options.InitCluster cfg.ClusterState = embed.ClusterStateFlagNew } else { cfg.InitialCluster = cfg.Name + "=" + e.options.APeerURL } if e.options.ClusterURL != "" { return nil, errors.New("joining an existing cluster is unsupported") } // can only use tls if not using pre-configured listeners cfg.ClientTLSInfo = transport.TLSInfo{ TrustedCAFile: e.options.TrustedCAFile, CertFile: e.options.ClientCertFile, KeyFile: e.options.ClientKeyFile, } cfg.PeerTLSInfo = transport.TLSInfo{ TrustedCAFile: e.options.TrustedCAFile, CertFile: e.options.PeerCertFile, KeyFile: e.options.PeerKeyFile, } // We might get an error from Validate. etcd docs don't tell us what // that error might be, though! return cfg, cfg.Validate() } // Start starts etcd and hearbeat func (e *Etcd) Start(ctx context.Context) (_ disco.InitialClusterState, err error) { opts, err := e.parseOptions() if err != nil { return disco.InitialClusterStateNew, err } state := disco.InitialClusterState(opts.ClusterState) // create a context that can be used for our watch processes, etcetera. e.childContext, e.childCancel = context.WithCancel(context.Background()) if e.options.EtcdHosts != "" { e.service, err = NewExternalEtcd(e, e.options) if err != nil { return state, errors.Wrap(err, "connecting etcd") } e.logger.Infof("using external etcd %v with fixed fb cluster %v", e.options.EtcdHosts, e.options.Cluster) } else { e.service, err = NewEmbeddedEtcd(e, 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 { // shut down everything on our way out. e.Close() } }() } e.cli, _ = e.service.NewClient() return e.service.Startup(ctx, state) } // 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.service.ID() e.heartbeatLeasedKV = newLeasedKV(e, e.childContext, 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) SetState(ctx context.Context, state disco.NodeState) (err error) { return e.heartbeatLeasedKV.Set(ctx, string(state)) } func (e *Etcd) ID() string { return e.service.ID() } func (e *Etcd) Peers() []*disco.Peer { return e.service.Peers() } func (e *Etcd) IsLeader() bool { return e.service.IsLeader() } func (e *Etcd) Leader() *disco.Peer { return e.service.Leader() } func (e *Etcd) ClusterState(ctx context.Context) (out disco.ClusterState, err error) { if len(e.options.EtcdHosts) == 0 { // only valid if not in external etcd mode if e.e == nil { return disco.ClusterStateUnknown, nil } } var ( heartbeats = 0 ) e.nodeMu.Lock() nodes := e.populateNodeStates(ctx) e.nodeMu.Unlock() if err != nil { e.logger.Errorf("requesting cluster state %q: getting node states: %v", e.options.Name, err) return disco.ClusterStateUnknown, err } for _, node := range nodes { if node.State == disco.NodeStateStarted { heartbeats++ } } if heartbeats < len(e.knownNodes) { if len(e.knownNodes)-heartbeats >= e.replicas { return disco.ClusterStateDown, nil } return disco.ClusterStateDegraded, nil } return disco.ClusterStateNormal, 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 } // seeNode encapsulates the practice of creating a new node, when needed, // and checking for duplicates. func (e *Etcd) seeNode(peerID string) *nodeData { var node *nodeData if node = e.knownNodes[peerID]; node == nil { e.logger.Debugf("previously unseen node, peer ID %s", peerID) node = &nodeData{ node: &disco.Node{ ID: peerID, State: disco.NodeStateUnknown, }, heartbeat: disco.NodeStateUnknown, } e.knownNodes[peerID] = node } return node } // 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: node := e.seeNode(peerID) // mark state as unknown because we deleted the heartbeat. node.heartbeat = disco.NodeStateUnknown e.nodesDirty = true case metadataPrefix: e.logger.Infof("deleting a previously-seen node, peer ID %q", peerID) delete(e.knownNodes, peerID) e.nodesDirty = true default: return fmt.Errorf("node watch: invalid prefix %q", prefix) } return nil } // putNodeData does the actual updating of the node state maps, etc, // given an incoming heartbeat or metadata 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: node := e.seeNode(peerID) node.heartbeat = disco.NodeState(value) e.nodesDirty = true case metadataPrefix: node := e.seeNode(peerID) node.metadata = value var newNode disco.Node err := json.Unmarshal(value, &newNode) if err != nil { return fmt.Errorf("json unmarshal of node metadata: %v", err) } // start with the heartbeat state newNode.State = node.heartbeat node.node = &newNode e.nodesDirty = true default: return fmt.Errorf("node watch: invalid prefix %q", 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. // Returns an immutable sorted list of nodes; future updates will not // modify the slice or the nodes in it. func (e *Etcd) populateNodeStates(ctx context.Context) []*disco.Node { if !e.nodesDirty { return e.sortedNodes } e.sortedNodes = make([]*disco.Node, 0, len(e.knownNodes)) for _, data := range e.knownNodes { newState := data.heartbeat // 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.node != nil { // The only part that should ever change is the state, which // will be either "unknown" or the state from a heartbeat. // If that computed state is different, we make a new node // at this point. The reason is that, if we previously returned // the sorted list of nodes, someone else could have a // pointer to the existing node. We don't want to clone these // every time anyone reads them, so instead we make them // immutable and copy-on-write. if data.node.State != newState { newNode := *data.node newNode.State = newState data.node = &newNode } e.sortedNodes = append(e.sortedNodes, data.node) } } // sort list by ID. list now contains sorted nodes which have their // current states. sort.Sort(disco.ByID(e.sortedNodes)) e.nodesDirty = false return e.sortedNodes } // 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(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 done := e.childContext.Done() e.nodeMu.Unlock() watcher := cli.Watch(clientv3.WithRequireLeader(e.childContext), nodePrefix, clientv3.WithPrefix(), clientv3.WithRev(minRev)) for { select { case <-done: // we're done, this is not an error return nil case resp := <-watcher: if err := resp.Err(); err != nil { if resp.CompactRevision > minRev { e.logger.Infof("watching node status, wanted rev %d, minimum now %d", minRev, resp.CompactRevision) // We've been told that any request with a revision under // CompactRevision will always fail. Set nodeRev to one less than that, // so we'll specify it as the minimum when we retry. // // We currently have no obvious way to verify that this will work. e.nodeRev = resp.CompactRevision - 1 } 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.Warnf("put event: %v", err) } case mvccpb.DELETE: err := e.deleteNodeData(ev.Kv.Key, ev.Kv.ModRevision) if err != nil { e.logger.Warnf("delete event: %v", err) } default: e.logger.Warnf("watchp %q: unknown event %#v", e.options.Name, ev) } } e.nodeMu.Unlock() } } } // watchNodes monitors changes to /heartbeat/ and /metadata/; // basically, it catches changes to cluster state, but ignores the schema. func (e *Etcd) watchNodes() { // 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 e.childContext.Err() == nil { err := e.retryClient(func(cli *clientv3.Client) error { return e.watchNodesOnce(cli) }) if err != nil { e.logger.Warnf("watchNodes: error from watch client: %v", err) } // delay slightly on watch termination 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) SetMetadata(ctx context.Context, node *disco.Node) error { // Set metadata for this node. data, err := json.Marshal(node) if err != nil { return errors.Wrap(err, "marshaling json metadata") } err = e.putKey(ctx, path.Join(metadataPrefix, e.service.ID()), string(data), ) 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, fieldVal []byte) error { key := schemaPrefix + indexName + "/" + name // Set up Op to write field value as bytes. op := clientv3.OpPut(key, "") op.WithValueBytes(fieldVal) // 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) UpdateField(ctx context.Context, indexName string, name string, fieldVal []byte) error { key := schemaPrefix + indexName + "/" + name // Set up Op to write field value as bytes. op := clientv3.OpPut(key, "") op.WithValueBytes(fieldVal) // 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.KeyExists(key)). Then(op). Commit() return err }) if err != nil { return errors.Wrap(err, "executing transaction") } if !resp.Succeeded { return disco.ErrFieldDoesNotExist } return nil } func (e *Etcd) DeleteField(ctx context.Context, indexName string, name string) (err error) { key := schemaPrefix + indexName + "/" + name var resp *clientv3.TxnResponse // Deleting field and views in one transaction. err = e.retryClient(func(cli *clientv3.Client) (err error) { resp, 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 }) if err != nil { return errors.Wrap(err, "executing transaction") } if !resp.Succeeded { return errors.New("deleting field from etcd failed") } return nil } 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 } // 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.service.ID()) 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() []*disco.Node { e.nodeMu.Lock() defer e.nodeMu.Unlock() return e.populateNodeStates(context.TODO()) } // PrimaryNodeID implements the Noder interface. func (e *Etcd) PrimaryNodeID(hasher disco.Hasher) string { return disco.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 }