github.com/m3db/m3@v1.5.0/src/cluster/services/leader/election/client.go (about)

     1  // Copyright (c) 2017 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  // Package election provides a wrapper around a subset of the Election
    22  // functionality of etcd's concurrency package with error handling for common
    23  // failure scenarios such as lease expiration.
    24  package election
    25  
    26  import (
    27  	"errors"
    28  	"sync"
    29  	"sync/atomic"
    30  
    31  	clientv3 "go.etcd.io/etcd/client/v3"
    32  	"go.etcd.io/etcd/client/v3/concurrency"
    33  	"golang.org/x/net/context"
    34  )
    35  
    36  var (
    37  	// ErrSessionExpired is returned by Campaign() if the underlying session
    38  	// (lease) has expired.
    39  	ErrSessionExpired = errors.New("election: session expired")
    40  
    41  	// ErrClientClosed is returned when an election client has been closed and
    42  	// cannot be reused.
    43  	ErrClientClosed = errors.New("election: client has been closed")
    44  )
    45  
    46  // Client encapsulates a client of etcd-backed leader elections.
    47  type Client struct {
    48  	mu  sync.RWMutex
    49  	cMu sync.RWMutex // campaign lock to protect concurrency.Election.leaderSession
    50  
    51  	prefix string
    52  	opts   clientOpts
    53  
    54  	etcdClient *clientv3.Client
    55  	election   *concurrency.Election
    56  	session    *concurrency.Session
    57  
    58  	closed uint32
    59  }
    60  
    61  // NewClient returns an election client based on the given etcd client and
    62  // participating in elections rooted at the given prefix. Optional parameters
    63  // can be configured via options, such as configuration of the etcd session TTL.
    64  func NewClient(cli *clientv3.Client, prefix string, options ...ClientOption) (*Client, error) {
    65  	var opts clientOpts
    66  	for _, opt := range options {
    67  		opt(&opts)
    68  	}
    69  
    70  	cl := &Client{
    71  		prefix:     prefix,
    72  		opts:       opts,
    73  		etcdClient: cli,
    74  	}
    75  
    76  	if err := cl.resetSessionAndElection(); err != nil {
    77  		return nil, err
    78  	}
    79  
    80  	return cl, nil
    81  }
    82  
    83  // Campaign starts a new campaign for val at the prefix configured at client
    84  // creation. It blocks until the etcd Campaign call returns, and returns any
    85  // error encountered or ErrSessionExpired if election.Campaign returned a nil
    86  // error but was due to the underlying session expiring. If the client is
    87  // successfully elected with a valid session, a channel is returned which is
    88  // closed when the session associated with the campaign expires. Callers should
    89  // watch this channel to determine if their presumed leadership from a nil-error
    90  // response is no longer valid.
    91  //
    92  // If the session expires while a Campaign() call is blocking, the campaign will
    93  // be cancelled and return a context.Cancelled error.
    94  //
    95  // If a caller wishes to cancel a current blocking campaign, they must pass a
    96  // context which they are responsible for cancelling otherwise the call to
    97  // Campaign() will block indefinitely until the client is elected (or until the
    98  // associated session expires).
    99  func (c *Client) Campaign(ctx context.Context, val string) (<-chan struct{}, error) {
   100  	if c.isClosed() {
   101  		return nil, ErrClientClosed
   102  	}
   103  
   104  	c.cMu.Lock()
   105  	defer c.cMu.Unlock()
   106  
   107  	c.mu.RLock()
   108  	session := c.session
   109  	election := c.election
   110  	c.mu.RUnlock()
   111  
   112  	// if current session is dead we need to create a new one
   113  	select {
   114  	case <-session.Done():
   115  		err := c.resetSessionAndElection()
   116  		if err != nil {
   117  			return nil, err
   118  		}
   119  
   120  		// if created a new session / election need to grab new one
   121  		c.mu.RLock()
   122  		session = c.session
   123  		election = c.election
   124  		c.mu.RUnlock()
   125  	default:
   126  	}
   127  
   128  	ctx, cancel := context.WithCancel(ctx)
   129  	defer cancel()
   130  
   131  	// if session expires in background cancel ongoing campaign call
   132  	go func() {
   133  		<-session.Done()
   134  		cancel()
   135  	}()
   136  
   137  	if err := election.Campaign(ctx, val); err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	select {
   142  	case <-session.Done():
   143  		return nil, ErrSessionExpired
   144  	default:
   145  	}
   146  
   147  	return session.Done(), nil
   148  }
   149  
   150  // Resign gives up leadership if the caller was elected. If a current call to
   151  // Campaign() is ongoing, Resign() will block until that call completes to avoid
   152  // a race in the concurrency.Election type.
   153  func (c *Client) Resign(ctx context.Context) error {
   154  	if c.isClosed() {
   155  		return ErrClientClosed
   156  	}
   157  
   158  	c.cMu.RLock()
   159  	defer c.cMu.RUnlock()
   160  
   161  	return c.election.Resign(ctx)
   162  }
   163  
   164  // Leader returns the value proposed by the currently elected leader of the
   165  // election.
   166  func (c *Client) Leader(ctx context.Context) (string, error) {
   167  	if c.isClosed() {
   168  		return "", ErrClientClosed
   169  	}
   170  
   171  	c.mu.RLock()
   172  	defer c.mu.RUnlock()
   173  
   174  	resp, err := c.election.Leader(ctx)
   175  	if err != nil {
   176  		return "", err
   177  	}
   178  	// NB(xichen): resp.Kv is guaranteed to have at least one value,
   179  	// otherwise the Leader() call will return ErrElectionNoLeader.
   180  	return string(resp.Kvs[0].Value), nil
   181  }
   182  
   183  // Observe returns a channel which receives that value of the latest leader for
   184  // the election. The channel is closed when the context is cancelled.
   185  func (c *Client) Observe(ctx context.Context) (<-chan string, error) {
   186  	if c.isClosed() {
   187  		return nil, ErrClientClosed
   188  	}
   189  
   190  	c.mu.RLock()
   191  	el := c.election
   192  	c.mu.RUnlock()
   193  
   194  	leaderCh := el.Observe(ctx)
   195  
   196  	ch := make(chan string)
   197  	go func() {
   198  		for {
   199  			select {
   200  			case resp, ok := <-leaderCh:
   201  				if !ok {
   202  					close(ch)
   203  					return
   204  				}
   205  
   206  				// Etcd only sends one value along the receive channel at a time
   207  				// https://git.io/fNipr.
   208  				if len(resp.Kvs) > 0 {
   209  					ch <- string(resp.Kvs[0].Value)
   210  				}
   211  
   212  			case <-ctx.Done():
   213  				close(ch)
   214  				return
   215  			}
   216  		}
   217  	}()
   218  
   219  	return ch, nil
   220  }
   221  
   222  // Close closes the client's underlying session and prevents any further
   223  // campaigns from being started.
   224  func (c *Client) Close() error {
   225  	if c.setClosed() {
   226  		c.mu.RLock()
   227  		defer c.mu.RUnlock()
   228  
   229  		return c.session.Close()
   230  	}
   231  
   232  	return nil
   233  }
   234  
   235  func (c *Client) resetSessionAndElection() error {
   236  	session, err := concurrency.NewSession(c.etcdClient, c.opts.sessionOpts...)
   237  	if err != nil {
   238  		return err
   239  	}
   240  
   241  	c.mu.Lock()
   242  	c.session = session
   243  	c.election = concurrency.NewElection(session, c.prefix)
   244  	c.mu.Unlock()
   245  	return nil
   246  }
   247  
   248  func (c *Client) isClosed() bool {
   249  	return atomic.LoadUint32(&c.closed) == 1
   250  }
   251  
   252  func (c *Client) setClosed() bool {
   253  	return atomic.CompareAndSwapUint32(&c.closed, 0, 1)
   254  }