Test failover

This commit is contained in:
Ian Gulliver
2023-06-04 13:44:26 -07:00
parent bce6488620
commit 7c3e760759
5 changed files with 67 additions and 23 deletions

View File

@@ -42,3 +42,5 @@ linters-settings:
use-field-name: true use-field-name: true
rules: rules:
json: goCamel json: goCamel
wsl:
allow-separated-leading-comment: true

View File

@@ -13,8 +13,6 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
) )
// TODO: Ensure promotion takes longer than demotion
type Candidate struct { type Candidate struct {
C <-chan CandidateState C <-chan CandidateState
@@ -59,6 +57,8 @@ func NewCandidate(numVoters int, signingKey string) *Candidate {
} }
func (c *Candidate) Stop() { func (c *Candidate) Stop() {
// Lock not required
close(c.stop) close(c.stop)
<-c.done <-c.done
} }
@@ -71,10 +71,15 @@ func (c *Candidate) State() CandidateState {
} }
func (c *Candidate) IsLeader() bool { func (c *Candidate) IsLeader() bool {
// Lock not required
return c.State() == StateLeader return c.State() == StateLeader
} }
func (c *Candidate) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (c *Candidate) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.mu.Lock()
defer c.mu.Unlock()
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error( http.Error(
w, w,
@@ -169,34 +174,34 @@ func (c *Candidate) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.vote(v) c.vote(v)
} }
func (c *Candidate) vote(v *vote) { func (c *Candidate) VoteIfNo(v vote) {
v.received = time.Now() c.mu.Lock()
defer c.mu.Unlock()
{
c.mu.Lock()
c.votes[v.VoterID] = v
c.mu.Unlock()
}
c.elect()
}
func (c *Candidate) voteIfNo(v *vote) {
if v.LastSeenCandidateID == c.resp.CandidateID { if v.LastSeenCandidateID == c.resp.CandidateID {
return return
} }
c.vote(v) c.vote(&v)
}
func (c *Candidate) vote(v *vote) {
// Must hold lock to call
v.received = time.Now()
c.votes[v.VoterID] = v
c.elect()
} }
func (c *Candidate) elect() { func (c *Candidate) elect() {
// Must hold lock to call
no := 0 no := 0
yes := 0 yes := 0
cutoff := time.Now().Add(-10 * time.Second) cutoff := time.Now().Add(-10 * time.Second)
c.mu.Lock() /////////////
for key, vote := range c.votes { for key, vote := range c.votes {
if vote.received.Before(cutoff) { if vote.received.Before(cutoff) {
delete(c.votes, key) delete(c.votes, key)
@@ -214,8 +219,6 @@ func (c *Candidate) elect() {
yes++ yes++
} }
c.mu.Unlock() ////////////
if no == 0 && yes > c.numVoters/2 { if no == 0 && yes > c.numVoters/2 {
c.update(StateLeader) c.update(StateLeader)
} else { } else {
@@ -224,8 +227,7 @@ func (c *Candidate) elect() {
} }
func (c *Candidate) update(state CandidateState) { func (c *Candidate) update(state CandidateState) {
c.mu.Lock() // Must hold lock to call
defer c.mu.Unlock()
if c.state == state { if c.state == state {
return return
@@ -236,6 +238,8 @@ func (c *Candidate) update(state CandidateState) {
} }
func (c *Candidate) loop() { func (c *Candidate) loop() {
// Lock not required
t := time.NewTicker(1 * time.Second) t := time.NewTicker(1 * time.Second)
defer t.Stop() defer t.Stop()
defer close(c.done) defer close(c.done)
@@ -246,7 +250,14 @@ func (c *Candidate) loop() {
return return
case <-t.C: case <-t.C:
c.elect() c.lockElect()
} }
} }
} }
func (c *Candidate) lockElect() {
c.mu.Lock()
defer c.mu.Unlock()
c.elect()
}

View File

@@ -15,3 +15,30 @@ func TestOne(t *testing.T) {
require.Eventually(t, ts.Candidate(0).IsLeader, 15*time.Second, 100*time.Millisecond) require.Eventually(t, ts.Candidate(0).IsLeader, 15*time.Second, 100*time.Millisecond)
} }
func TestThree(t *testing.T) {
t.Parallel()
ts := NewTestSystem(t, 3)
defer ts.Stop()
require.Eventually(t, ts.Candidate(0).IsLeader, 15*time.Second, 100*time.Millisecond)
}
func TestFailover(t *testing.T) {
t.Parallel()
ts := NewTestSystem(t, 3)
defer ts.Stop()
require.Eventually(t, ts.Candidate(0).IsLeader, 15*time.Second, 100*time.Millisecond)
ts.SetServer(1)
require.Eventually(t, func() bool { return !ts.Candidate(0).IsLeader() }, 15*time.Second, 100*time.Millisecond)
// TODO: Check that new candidate doesn't become leader before old candidate loses it
// require.False(t, ts.Candidate(1).IsLeader())
require.Eventually(t, ts.Candidate(1).IsLeader, 15*time.Second, 100*time.Millisecond)
}

View File

@@ -88,6 +88,10 @@ func (ts *TestSystem) Stop() {
ts.proxy.Close() ts.proxy.Close()
} }
func (ts *TestSystem) SetServer(i int) {
ts.proxy.SetBackend(ts.Server(i).Addr())
}
func (ts *TestSystem) Server(i int) *TestServer { func (ts *TestSystem) Server(i int) *TestServer {
return ts.servers[i] return ts.servers[i]
} }

View File

@@ -108,7 +108,7 @@ func (v *Voter) poll() bool {
func (v *Voter) sendVote() { func (v *Voter) sendVote() {
v.vote.VoteSent = time.Now().UTC() v.vote.VoteSent = time.Now().UTC()
v.candidate.voteIfNo(&v.vote) v.candidate.VoteIfNo(v.vote)
js := lo.Must(json.Marshal(v.vote)) js := lo.Must(json.Marshal(v.vote))