Test failover
This commit is contained in:
@@ -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
|
||||||
|
|||||||
55
candidate.go
55
candidate.go
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
}
|
}
|
||||||
|
|||||||
2
voter.go
2
voter.go
@@ -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))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user