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  }