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 }