From 20d89bcf324b3535f559596b49457290b67a9f3c Mon Sep 17 00:00:00 2001 From: Anthony Romano Date: Tue, 1 Mar 2016 13:36:48 -0800 Subject: [PATCH] etcdctlv3: elect command --- etcdctlv3/README.md | 37 ++++++++ etcdctlv3/command/elect_command.go | 132 +++++++++++++++++++++++++++++ etcdctlv3/main.go | 1 + 3 files changed, 170 insertions(+) create mode 100644 etcdctlv3/command/elect_command.go diff --git a/etcdctlv3/README.md b/etcdctlv3/README.md index a88625a86..2ebc97f97 100644 --- a/etcdctlv3/README.md +++ b/etcdctlv3/README.md @@ -282,6 +282,43 @@ mylock/1234534535445 ``` +### Notes + +The lease length of a lock defaults to 60 seconds. If LOCK is abnormally terminated, lock progress may be delayed +by up to 60 seconds. + + +### ELECT [options] \ [proposal] + +ELECT participates on a named election. A node announces its candidacy in the election by providing +a proposal value. If a node wishes to observe the election, ELECT listens for new leaders values. +Whenever a leader is elected, its proposal is given as output. + +#### Options + +- listen -- observe the election + +#### Return value + +- If a candidate, ELECT displays the GET on the leader key once the node is elected election. + +- If observing, ELECT streams the result for a GET on the leader key for the current election and all future elections. + +- ELECT returns a zero exit code only if it is terminated by a signal and can revoke its candidacy or leadership, if any. + +#### Example +```bash +./etcdctl elect myelection foo +myelection/1456952310051373265 +foo + +``` + +### Notes + +The lease length of a leader defaults to 60 seconds. If a candidate is abnormally terminated, election +progress may be delayed by up to 60 seconds. + ### MAKE-MIRROR [options] \ diff --git a/etcdctlv3/command/elect_command.go b/etcdctlv3/command/elect_command.go new file mode 100644 index 000000000..f7d0242ea --- /dev/null +++ b/etcdctlv3/command/elect_command.go @@ -0,0 +1,132 @@ +// Copyright 2016 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "errors" + "os" + "os/signal" + + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/spf13/cobra" + "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context" + "github.com/coreos/etcd/clientv3" + "github.com/coreos/etcd/clientv3/concurrency" +) + +var ( + electListen bool +) + +// NewElectCommand returns the cobra command for "elect". +func NewElectCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "elect [proposal]", + Short: "elect observes and participates in leader election", + Run: electCommandFunc, + } + cmd.Flags().BoolVarP(&electListen, "listen", "l", false, "observation mode") + return cmd +} + +func electCommandFunc(cmd *cobra.Command, args []string) { + if len(args) != 1 && len(args) != 2 { + ExitWithError(ExitBadArgs, errors.New("elect takes one election name argument and an optional proposal argument.")) + } + c := mustClientFromCmd(cmd) + + var err error + if len(args) == 1 { + if !electListen { + ExitWithError(ExitBadArgs, errors.New("no proposal argument but -l not set")) + } + err = observe(c, args[0]) + } else { + if electListen { + ExitWithError(ExitBadArgs, errors.New("proposal given but -l is set")) + } + err = campaign(c, args[0], args[1]) + } + if err != nil { + ExitWithError(ExitError, err) + } +} + +func observe(c *clientv3.Client, election string) error { + e := concurrency.NewElection(context.TODO(), c, election) + ctx, cancel := context.WithCancel(context.TODO()) + + donec := make(chan struct{}) + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, os.Interrupt, os.Kill) + go func() { + <-sigc + cancel() + }() + + go func() { + for resp := range e.Observe(ctx) { + display.Get(resp) + } + close(donec) + }() + + <-donec + + select { + case <-ctx.Done(): + default: + return errors.New("elect: observer lost") + } + + return nil +} + +func campaign(c *clientv3.Client, election string, prop string) error { + e := concurrency.NewElection(context.TODO(), c, election) + ctx, cancel := context.WithCancel(context.TODO()) + + donec := make(chan struct{}) + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, os.Interrupt, os.Kill) + go func() { + <-sigc + cancel() + close(donec) + }() + + s, serr := concurrency.NewSession(c) + if serr != nil { + return serr + } + + if err := e.Campaign(ctx, prop); err != nil { + return err + } + + // print key since elected + resp, err := c.Get(ctx, e.Key()) + if err != nil { + return err + } + display.Get(*resp) + + select { + case <-donec: + case <-s.Done(): + return errors.New("elect: session expired") + } + + return e.Resign() +} diff --git a/etcdctlv3/main.go b/etcdctlv3/main.go index 771a9c5b9..c76b795bc 100644 --- a/etcdctlv3/main.go +++ b/etcdctlv3/main.go @@ -64,6 +64,7 @@ func init() { command.NewMakeMirrorCommand(), command.NewLockCommand(), command.NewAuthCommand(), + command.NewElectCommand(), ) }