github.com/m3db/m3@v1.5.0/src/cluster/services/leader/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 leader
    22  
    23  import (
    24  	"errors"
    25  	"fmt"
    26  	"sync"
    27  
    28  	"github.com/m3db/m3/src/cluster/services"
    29  	"github.com/m3db/m3/src/cluster/services/leader/campaign"
    30  	"github.com/m3db/m3/src/cluster/services/leader/election"
    31  
    32  	clientv3 "go.etcd.io/etcd/client/v3"
    33  	"go.etcd.io/etcd/client/v3/concurrency"
    34  	"golang.org/x/net/context"
    35  )
    36  
    37  // Appended to elections with an empty string for electionID to make it easier
    38  // for user to debug etcd keys.
    39  const defaultElectionID = "default"
    40  
    41  var (
    42  	// ErrNoLeader is returned when a call to Leader() is made to an election
    43  	// with no leader. We duplicate this error so the user doesn't have to
    44  	// import etcd's concurrency package in order to check the cause of the
    45  	// error.
    46  	ErrNoLeader = concurrency.ErrElectionNoLeader
    47  
    48  	// ErrCampaignInProgress is returned when a call to Campaign() is made while
    49  	// the caller is either already (a) campaigning or (b) the leader.
    50  	ErrCampaignInProgress = errors.New("campaign in progress")
    51  )
    52  
    53  // NB(mschalle): when an etcd leader failover occurs, all current leases have
    54  // their TTLs refreshed: https://github.com/coreos/etcd/issues/2660
    55  
    56  type client struct {
    57  	sync.RWMutex
    58  
    59  	client           *election.Client
    60  	opts             services.ElectionOptions
    61  	campaignCancelFn context.CancelFunc
    62  	observeCancelFn  context.CancelFunc
    63  	observeCtx       context.Context
    64  	resignCh         chan struct{}
    65  	campaigning      bool
    66  	closed           bool
    67  }
    68  
    69  // newClient returns an instance of an client bound to a single election.
    70  func newClient(cli *clientv3.Client, opts Options, electionID string) (*client, error) {
    71  	if err := opts.Validate(); err != nil {
    72  		return nil, err
    73  	}
    74  
    75  	ttl := opts.ElectionOpts().TTLSecs()
    76  	pfx := electionPrefix(opts.ServiceID(), electionID)
    77  	ec, err := election.NewClient(cli, pfx, election.WithSessionOptions(concurrency.WithTTL(ttl)))
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	// Allow multiple observe calls with the same parent context, to be cancelled
    83  	// when the client is closed.
    84  	ctx, cancel := context.WithCancel(context.Background())
    85  
    86  	return &client{
    87  		client:          ec,
    88  		opts:            opts.ElectionOpts(),
    89  		resignCh:        make(chan struct{}),
    90  		observeCtx:      ctx,
    91  		observeCancelFn: cancel,
    92  	}, nil
    93  }
    94  
    95  func (c *client) campaign(opts services.CampaignOptions) (<-chan campaign.Status, error) {
    96  	if c.isClosed() {
    97  		return nil, errClientClosed
    98  	}
    99  
   100  	if !c.startCampaign() {
   101  		return nil, ErrCampaignInProgress
   102  	}
   103  
   104  	ctx, cancel := context.WithCancel(context.Background())
   105  	c.Lock()
   106  	c.campaignCancelFn = cancel
   107  	c.Unlock()
   108  
   109  	// buffer 1 to not block initial follower update
   110  	sc := make(chan campaign.Status, 1)
   111  
   112  	sc <- campaign.NewStatus(campaign.Follower)
   113  
   114  	go func() {
   115  		defer func() {
   116  			close(sc)
   117  			cancel()
   118  			c.stopCampaign()
   119  		}()
   120  
   121  		// Campaign blocks until elected. Once we are elected, we get a channel
   122  		// that's closed if our session dies.
   123  		ch, err := c.client.Campaign(ctx, opts.LeaderValue())
   124  		if err != nil {
   125  			sc <- campaign.NewErrorStatus(err)
   126  			return
   127  		}
   128  
   129  		sc <- campaign.NewStatus(campaign.Leader)
   130  		select {
   131  		case <-ch:
   132  			sc <- campaign.NewErrorStatus(election.ErrSessionExpired)
   133  		case <-c.resignCh:
   134  			sc <- campaign.NewStatus(campaign.Follower)
   135  		}
   136  	}()
   137  
   138  	return sc, nil
   139  }
   140  
   141  func (c *client) resign() error {
   142  	if c.isClosed() {
   143  		return errClientClosed
   144  	}
   145  
   146  	// if there's an active blocking call to Campaign() stop it
   147  	c.Lock()
   148  	if c.campaignCancelFn != nil {
   149  		c.campaignCancelFn()
   150  		c.campaignCancelFn = nil
   151  	}
   152  	c.Unlock()
   153  
   154  	ctx, cancel := context.WithTimeout(context.Background(), c.opts.ResignTimeout())
   155  	defer cancel()
   156  	if err := c.client.Resign(ctx); err != nil {
   157  		return err
   158  	}
   159  
   160  	// if successfully resigned and there was a campaign in Leader state cancel
   161  	// it
   162  	select {
   163  	case c.resignCh <- struct{}{}:
   164  	default:
   165  	}
   166  
   167  	c.stopCampaign()
   168  
   169  	return nil
   170  }
   171  
   172  func (c *client) leader() (string, error) {
   173  	if c.isClosed() {
   174  		return "", errClientClosed
   175  	}
   176  
   177  	ctx, cancel := context.WithTimeout(context.Background(), c.opts.LeaderTimeout())
   178  	defer cancel()
   179  	ld, err := c.client.Leader(ctx)
   180  	if err == concurrency.ErrElectionNoLeader {
   181  		return ld, ErrNoLeader
   182  	}
   183  	return ld, err
   184  }
   185  
   186  func (c *client) observe() (<-chan string, error) {
   187  	if c.isClosed() {
   188  		return nil, errClientClosed
   189  	}
   190  
   191  	c.RLock()
   192  	pCtx := c.observeCtx
   193  	c.RUnlock()
   194  
   195  	ctx, cancel := context.WithCancel(pCtx)
   196  	ch, err := c.client.Observe(ctx)
   197  	if err != nil {
   198  		cancel()
   199  		return nil, err
   200  	}
   201  
   202  	go func() {
   203  		<-ctx.Done()
   204  		cancel()
   205  	}()
   206  
   207  	return ch, nil
   208  }
   209  
   210  func (c *client) startCampaign() bool {
   211  	c.Lock()
   212  	defer c.Unlock()
   213  
   214  	if c.campaigning {
   215  		return false
   216  	}
   217  
   218  	c.campaigning = true
   219  	return true
   220  }
   221  
   222  func (c *client) stopCampaign() {
   223  	c.Lock()
   224  	c.campaigning = false
   225  	c.Unlock()
   226  }
   227  
   228  // Close closes the election service client entirely. No more campaigns can be
   229  // started and any outstanding campaigns are closed.
   230  func (c *client) close() error {
   231  	c.Lock()
   232  	if c.closed {
   233  		c.Unlock()
   234  		return nil
   235  	}
   236  	c.observeCancelFn()
   237  	c.closed = true
   238  	c.Unlock()
   239  	return c.client.Close()
   240  }
   241  
   242  func (c *client) isClosed() bool {
   243  	c.RLock()
   244  	defer c.RUnlock()
   245  	return c.closed
   246  }
   247  
   248  // elections for a service "svc" in env "test" should be stored under
   249  // "_ld/test/svc". A service "svc" with no environment will be stored under
   250  // "_ld/svc".
   251  func servicePrefix(sid services.ServiceID) string {
   252  	env := sid.Environment()
   253  	if env == "" {
   254  		return fmt.Sprintf(keyFormat, leaderKeyPrefix, sid.Name())
   255  	}
   256  
   257  	return fmt.Sprintf(
   258  		keyFormat,
   259  		leaderKeyPrefix,
   260  		fmt.Sprintf(keyFormat, env, sid.Name()))
   261  }
   262  
   263  func electionPrefix(sid services.ServiceID, electionID string) string {
   264  	eid := electionID
   265  	if eid == "" {
   266  		eid = defaultElectionID
   267  	}
   268  
   269  	return fmt.Sprintf(
   270  		keyFormat,
   271  		servicePrefix(sid),
   272  		eid)
   273  }