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 }