github.com/mailgun/holster/v4@v4.20.0/etcdutil/election_test.go (about) 1 package etcdutil_test 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "strings" 8 "testing" 9 "time" 10 11 "github.com/Shopify/toxiproxy" 12 "github.com/mailgun/holster/v4/clock" 13 "github.com/mailgun/holster/v4/etcdutil" 14 "github.com/sirupsen/logrus" 15 "github.com/stretchr/testify/assert" 16 "github.com/stretchr/testify/require" 17 "github.com/stretchr/testify/suite" 18 etcd "go.etcd.io/etcd/client/v3" 19 ) 20 21 func TestElection(t *testing.T) { 22 ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 23 defer cancel() 24 25 election, err := etcdutil.NewElection(ctx, client, etcdutil.ElectionConfig{ 26 EventObserver: func(e etcdutil.ElectionEvent) { 27 if e.Err != nil { 28 t.Fatal(e.Err.Error()) 29 } 30 }, 31 Election: "/my-election", 32 Candidate: "me", 33 }) 34 require.Nil(t, err) 35 36 assert.Equal(t, true, election.IsLeader()) 37 election.Close() 38 assert.Equal(t, false, election.IsLeader()) 39 } 40 41 func TestTwoCampaigns(t *testing.T) { 42 ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 43 defer cancel() 44 45 logrus.SetLevel(logrus.DebugLevel) 46 47 c1, err := etcdutil.NewElection(ctx, client, etcdutil.ElectionConfig{ 48 EventObserver: func(e etcdutil.ElectionEvent) { 49 if e.Err != nil { 50 t.Fatal(e.Err.Error()) 51 } 52 }, 53 Election: "/my-election", 54 Candidate: "c1", 55 }) 56 require.Nil(t, err) 57 58 c2Chan := make(chan etcdutil.ElectionEvent, 5) 59 c2, err := etcdutil.NewElection(ctx, client, etcdutil.ElectionConfig{ 60 EventObserver: func(e etcdutil.ElectionEvent) { 61 if err != nil { 62 t.Fatal(err.Error()) 63 } 64 c2Chan <- e 65 }, 66 Election: "/my-election", 67 Candidate: "c2", 68 }) 69 require.Nil(t, err) 70 71 assert.Equal(t, true, c1.IsLeader()) 72 assert.Equal(t, false, c2.IsLeader()) 73 74 // Cancel first candidate 75 c1.Close() 76 assert.Equal(t, false, c1.IsLeader()) 77 78 // Second campaign should become leader 79 e := <-c2Chan 80 assert.Equal(t, false, e.IsLeader) 81 e = <-c2Chan 82 assert.Equal(t, true, e.IsLeader) 83 assert.Equal(t, false, e.IsDone) 84 85 c2.Close() 86 e = <-c2Chan 87 assert.Equal(t, false, e.IsLeader) 88 assert.Equal(t, false, e.IsDone) 89 90 e = <-c2Chan 91 assert.Equal(t, false, e.IsLeader) 92 assert.Equal(t, true, e.IsDone) 93 } 94 95 func TestElectionsSuite(t *testing.T) { 96 etcdCAPath := os.Getenv("ETCD3_CA") 97 if etcdCAPath != "" { 98 t.Skip("Tests featuring toxiproxy cannot deal with TLS") 99 } 100 suite.Run(t, new(ElectionsSuite)) 101 } 102 103 type ElectionsSuite struct { 104 suite.Suite 105 toxiProxies []*toxiproxy.Proxy 106 proxiedClients []*etcd.Client 107 } 108 109 func (s *ElectionsSuite) SetupTest() { 110 etcdEndpoint := os.Getenv("ETCD3_ENDPOINT") 111 if etcdEndpoint == "" { 112 etcdEndpoint = "127.0.0.1:2379" 113 } 114 115 s.toxiProxies = make([]*toxiproxy.Proxy, 2) 116 s.proxiedClients = make([]*etcd.Client, 2) 117 for i := range s.toxiProxies { 118 toxiProxy := toxiproxy.NewProxy() 119 toxiProxy.Name = fmt.Sprintf("etcd_clt_%d", i) 120 toxiProxy.Upstream = etcdEndpoint 121 s.Require().Nil(toxiProxy.Start()) 122 s.toxiProxies[i] = toxiProxy 123 124 var err error 125 // Make sure to access proxy via 127.0.0.1 otherwise TLS verification fails. 126 proxyEndpoint := toxiProxy.Listen 127 if strings.HasPrefix(proxyEndpoint, "[::]:") { 128 proxyEndpoint = "127.0.0.1:" + proxyEndpoint[5:] 129 } 130 s.proxiedClients[i], err = etcd.New(etcd.Config{ 131 Endpoints: []string{proxyEndpoint}, 132 DialTimeout: 1 * clock.Second, 133 }) 134 s.Require().Nil(err) 135 } 136 137 ctx, cancel := context.WithTimeout(context.Background(), 3*clock.Second) 138 defer cancel() 139 _, err := s.proxiedClients[0].Delete(ctx, "/elections", etcd.WithPrefix()) 140 s.Require().Nil(err) 141 } 142 143 func (s *ElectionsSuite) TearDownTest() { 144 for _, proxy := range s.toxiProxies { 145 proxy.Stop() 146 } 147 for _, etcdClt := range s.proxiedClients { 148 _ = etcdClt.Close() 149 } 150 } 151 152 // When the leader is stopped then another candidate is elected. 153 func (s *ElectionsSuite) TestLeaderStops() { 154 campaign := "LeadershipTransferOnStop" 155 e0, ch0 := s.newElection(campaign, 0) 156 s.assertElectionWinner(ch0, 3*clock.Second) 157 158 e1, ch1 := s.newElection(campaign, 1) 159 defer e1.Close() 160 s.assertElectionLooser(ch1, 200*clock.Millisecond) 161 162 // When 163 e0.Close() 164 165 // Then 166 s.assertElectionWinner(ch1, 3*clock.Second) 167 } 168 169 // A candidate may never be elected. 170 func (s *ElectionsSuite) TestNeverElected() { 171 campaign := "NeverElected" 172 e0, ch0 := s.newElection(campaign, 0) 173 defer e0.Close() 174 s.assertElectionWinner(ch0, 3*clock.Second) 175 176 e1, ch1 := s.newElection(campaign, 1) 177 s.assertElectionLooser(ch1, 200*clock.Millisecond) 178 179 // When 180 e1.Close() 181 182 // Then 183 s.assertElectionClosed(ch1, 200*clock.Millisecond) 184 } 185 186 // When the leader is loosing connection with etcd, then another candidate gets 187 // promoted. 188 func (s *ElectionsSuite) TestLeaderConnLost() { 189 campaign := "LeadershipLost" 190 e0, ch0 := s.newElection(campaign, 0) 191 defer e0.Close() 192 s.assertElectionWinner(ch0, 3*clock.Second) 193 194 e1, ch1 := s.newElection(campaign, 1) 195 defer e1.Close() 196 s.assertElectionLooser(ch1, 200*clock.Millisecond) 197 198 // When 199 s.toxiProxies[0].Stop() 200 201 // Then 202 s.assertElectionLooser(ch0, 5*clock.Second) 203 s.assertElectionWinner(ch1, 5*clock.Second) 204 } 205 206 // It is possible to stop a former leader while it is trying to reconnect with 207 // Etcd. 208 func (s *ElectionsSuite) TestLostLeaderStop() { 209 campaign := "LostLeaderStop" 210 e0, ch0 := s.newElection(campaign, 0) 211 s.assertElectionWinner(ch0, 3*clock.Second) 212 213 e1, ch1 := s.newElection(campaign, 1) 214 defer e1.Close() 215 s.assertElectionLooser(ch1, 200*clock.Millisecond) 216 217 // Given 218 s.toxiProxies[0].Stop() 219 clock.Sleep(2 * clock.Second) 220 221 // When 222 e0.Close() 223 224 // Then 225 s.assertElectionClosed(ch0, 3*clock.Second) 226 } 227 228 // FIXME: This test gets stuck on e0.Stop(). 229 // // If Etcd is down on start the candidate keeps trying to connect. 230 // func (s *ElectionsSuite) TestEtcdDownOnStart() { 231 // s.toxiProxies[0].Stop() 232 // campaign := "EtcdDownOnStart" 233 // e0, ch0 := s.newElection(campaign, 0) 234 // 235 // // When 236 // _ = s.toxiProxies[0].Start() 237 // 238 // // Then 239 // s.assertElectionWinner(ch0, 3*clock.Second) 240 // e0.Stop() 241 // } 242 243 // If provided etcd endpoint candidate keeps trying to connect until it is 244 // stopped. 245 func (s *ElectionsSuite) TestBadEtcdEndpoint() { 246 s.toxiProxies[0].Stop() 247 campaign := "/BadEtcdEndpoint" 248 e0, ch0 := s.newElection(campaign, 0) 249 250 // When 251 e0.Close() 252 253 // Then 254 s.assertElectionClosed(ch0, 3*clock.Second) 255 } 256 257 func (s *ElectionsSuite) assertElectionWinner(ch chan bool, timeout clock.Duration) { 258 timeoutCh := clock.After(timeout) 259 for { 260 select { 261 case elected := <-ch: 262 if elected { 263 return 264 } 265 case <-timeoutCh: 266 s.Fail("Timeout waiting for election winning") 267 } 268 } 269 } 270 271 func (s *ElectionsSuite) assertElectionLooser(ch chan bool, timeout clock.Duration) { 272 timeoutCh := clock.After(timeout) 273 for { 274 select { 275 case elected := <-ch: 276 if !elected { 277 return 278 } 279 case <-timeoutCh: 280 s.Fail("Timeout waiting for election loss") 281 } 282 } 283 } 284 285 func (s *ElectionsSuite) assertElectionClosed(ch chan bool, timeout clock.Duration) { 286 timeoutCh := clock.After(timeout) 287 for { 288 select { 289 case _, ok := <-ch: 290 if !ok { 291 return 292 } 293 case <-timeoutCh: 294 s.Fail("Timeout waiting for election closed") 295 } 296 } 297 } 298 299 func (s *ElectionsSuite) newElection(campaign string, id int) (election *etcdutil.Election, electedCh chan bool) { 300 eCh := make(chan bool, 32) 301 candidate := fmt.Sprintf("candidate-%d", id) 302 electionCfg := etcdutil.ElectionConfig{ 303 EventObserver: func(e etcdutil.ElectionEvent) { 304 logrus.Infof("%s got %#v", candidate, e) 305 if e.IsDone { 306 close(eCh) 307 return 308 } 309 eCh <- e.IsLeader 310 }, 311 Election: campaign, 312 Candidate: candidate, 313 TTL: 1, 314 } 315 e := etcdutil.NewElectionAsync(s.proxiedClients[id], electionCfg) 316 return e, eCh 317 }