Skip to content

Commit d74e502

Browse files
committed
test(badgerd/gossip): cover election flow with deterministic peers
Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
1 parent 753b782 commit d74e502

2 files changed

Lines changed: 184 additions & 7 deletions

File tree

internal/badgerd/replication/gossip/election.go

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,47 @@ import (
2323
pb2 "go.linka.cloud/protodb/internal/badgerd/replication/gossip/pb"
2424
)
2525

26+
type electionClient interface {
27+
Election(ctx context.Context, in *pb2.Message, opts ...any) (*pb2.Message, error)
28+
}
29+
30+
type electionPeer struct {
31+
name string
32+
meta *pb2.Meta
33+
repl electionClient
34+
}
35+
2636
func (r *Gossip) Elect(ctx context.Context) {
2737
meta := r.meta.Load().CloneVT()
38+
r.elect(ctx, r.electionPeers(), meta)
39+
}
40+
41+
func (r *Gossip) electionPeers() []electionPeer {
42+
peers := make([]electionPeer, 0)
43+
for _, v := range r.clients() {
44+
peers = append(peers, electionPeer{name: v.name, meta: v.meta, repl: &electionClientAdapter{c: v.repl}})
45+
}
46+
return peers
47+
}
48+
49+
func (r *Gossip) elect(ctx context.Context, peers []electionPeer, meta *pb2.Meta) {
2850
log := logger.C(ctx)
29-
nodes := r.clients()
30-
log.Debugf("replication clients: %d", len(nodes))
51+
log.Debugf("replication clients: %d", len(peers))
3152
log.Info("starting election")
3253
count := 0
33-
var n *node
34-
for _, v := range nodes {
54+
var n *electionPeer
55+
for _, v := range peers {
3556
if v.meta.IsLeader {
36-
n = v
57+
v := v
58+
n = &v
3759
}
3860
}
3961
if n != nil {
4062
log.Infof("found leader %s", n.name)
4163
r.onNewLeader(ctx, n.name)
4264
return
4365
}
44-
for _, v := range nodes {
66+
for _, v := range peers {
4567
if v.meta.LocalVersion < meta.LocalVersion || strings.Compare(v.name, r.name) > 0 {
4668
continue
4769
}
@@ -78,14 +100,22 @@ func (r *Gossip) Elect(ctx context.Context) {
78100
// we are the leader
79101
log.Infof("we are the leader")
80102
r.onNewLeader(ctx, r.name)
81-
for _, v := range nodes {
103+
for _, v := range peers {
82104
log.Infof("sending leader message to %s", v.name)
83105
if _, err := v.repl.Election(ctx, &pb2.Message{Type: pb2.ElectionTypeLeader, Name: r.name, Meta: meta.CloneVT()}); err != nil {
84106
log.WithError(err).Errorf("failed to send leader message to %s", v.name)
85107
}
86108
}
87109
}
88110

111+
type electionClientAdapter struct {
112+
c pb2.ReplicationServiceClient
113+
}
114+
115+
func (a *electionClientAdapter) Election(ctx context.Context, in *pb2.Message, _ ...any) (*pb2.Message, error) {
116+
return a.c.Election(ctx, in)
117+
}
118+
89119
func (r *Gossip) Election(_ context.Context, req *pb2.Message) (*pb2.Message, error) {
90120
meta := r.meta.Load().CloneVT()
91121
log := logger.C(r.ctx)
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package gossip
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
"time"
8+
9+
pubsub "go.linka.cloud/pubsub/typed"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
14+
pb2 "go.linka.cloud/protodb/internal/badgerd/replication/gossip/pb"
15+
)
16+
17+
func TestElectFoundLeaderPeerUsesIt(t *testing.T) {
18+
r := newElectionTestGossip("self", 10)
19+
r.elect(context.Background(), []electionPeer{
20+
{name: "n1", meta: &pb2.Meta{IsLeader: true, LocalVersion: 12}, repl: &fakeElectionClient{}},
21+
}, &pb2.Meta{LocalVersion: 10})
22+
23+
assert.Equal(t, "n1", r.CurrentLeader())
24+
assert.False(t, r.IsLeader())
25+
requireReady(t, r.ready)
26+
}
27+
28+
func TestElectOkResponseStopsElection(t *testing.T) {
29+
r := newElectionTestGossip("b", 10)
30+
r.elect(context.Background(), []electionPeer{
31+
{name: "a", meta: &pb2.Meta{LocalVersion: 10}, repl: &fakeElectionClient{res: &pb2.Message{Name: "a", Type: pb2.ElectionTypeOk}}},
32+
}, &pb2.Meta{LocalVersion: 10})
33+
34+
assert.Equal(t, "", r.CurrentLeader())
35+
assert.False(t, r.IsLeader())
36+
notReady(t, r.ready)
37+
}
38+
39+
func TestElectSkipThenLeaderUsesLeaderResponse(t *testing.T) {
40+
r := newElectionTestGossip("c", 10)
41+
leaderClient := &fakeElectionClient{res: &pb2.Message{Name: "a", Type: pb2.ElectionTypeLeader}}
42+
r.elect(context.Background(), []electionPeer{
43+
{name: "b", meta: &pb2.Meta{LocalVersion: 10}, repl: &fakeElectionClient{res: &pb2.Message{Name: "b", Type: pb2.ElectionTypeSkip}}},
44+
{name: "a", meta: &pb2.Meta{LocalVersion: 10}, repl: leaderClient},
45+
}, &pb2.Meta{LocalVersion: 10})
46+
47+
assert.Equal(t, "a", r.CurrentLeader())
48+
assert.False(t, r.IsLeader())
49+
assert.Equal(t, 1, leaderClient.calls)
50+
requireReady(t, r.ready)
51+
}
52+
53+
func TestElectErrorStopsElection(t *testing.T) {
54+
r := newElectionTestGossip("b", 10)
55+
errClient := &fakeElectionClient{err: errors.New("boom")}
56+
second := &fakeElectionClient{res: &pb2.Message{Name: "a", Type: pb2.ElectionTypeLeader}}
57+
r.elect(context.Background(), []electionPeer{
58+
{name: "a", meta: &pb2.Meta{LocalVersion: 10}, repl: errClient},
59+
{name: "aa", meta: &pb2.Meta{LocalVersion: 10}, repl: second},
60+
}, &pb2.Meta{LocalVersion: 10})
61+
62+
assert.Equal(t, 1, errClient.calls)
63+
assert.Equal(t, 0, second.calls)
64+
assert.Equal(t, "", r.CurrentLeader())
65+
notReady(t, r.ready)
66+
}
67+
68+
func TestElectionWhenNotReadyReturnsSkip(t *testing.T) {
69+
r := newElectionTestGossip("self", 3)
70+
res, err := r.Election(context.Background(), &pb2.Message{Type: pb2.ElectionTypeElection, Name: "peer", Meta: &pb2.Meta{LocalVersion: 3}})
71+
require.NoError(t, err)
72+
assert.Equal(t, pb2.ElectionTypeSkip, res.Type)
73+
assert.Equal(t, "self", res.Name)
74+
}
75+
76+
func TestElectionWhenLeaderReturnsLeaderMessage(t *testing.T) {
77+
r := newElectionTestGossip("self", 7)
78+
close(r.ready)
79+
r.leading.Store(true)
80+
r.leaderName.Store("self")
81+
82+
res, err := r.Election(context.Background(), &pb2.Message{Type: pb2.ElectionTypeElection, Name: "peer", Meta: &pb2.Meta{LocalVersion: 7}})
83+
require.NoError(t, err)
84+
assert.Equal(t, pb2.ElectionTypeLeader, res.Type)
85+
assert.Equal(t, "self", res.Name)
86+
require.NotNil(t, res.Meta)
87+
assert.EqualValues(t, 7, res.Meta.LocalVersion)
88+
}
89+
90+
func TestElectionLeaderMessageSetsLeaderWhenNotReady(t *testing.T) {
91+
r := newElectionTestGossip("self", 1)
92+
93+
res, err := r.Election(context.Background(), &pb2.Message{Type: pb2.ElectionTypeLeader, Name: "peer", Meta: &pb2.Meta{LocalVersion: 2}})
94+
require.NoError(t, err)
95+
assert.Equal(t, "self", res.Name)
96+
assert.Equal(t, "peer", r.CurrentLeader())
97+
assert.False(t, r.IsLeader())
98+
requireReady(t, r.ready)
99+
}
100+
101+
type fakeElectionClient struct {
102+
res *pb2.Message
103+
err error
104+
calls int
105+
}
106+
107+
func (f *fakeElectionClient) Election(_ context.Context, _ *pb2.Message, _ ...any) (*pb2.Message, error) {
108+
f.calls++
109+
if f.err != nil {
110+
return nil, f.err
111+
}
112+
if f.res == nil {
113+
return &pb2.Message{}, nil
114+
}
115+
return f.res.CloneVT(), nil
116+
}
117+
118+
func newElectionTestGossip(name string, version uint64) *Gossip {
119+
r := &Gossip{
120+
ctx: context.Background(),
121+
name: name,
122+
leading: NewAtomic(false),
123+
leaderName: NewAtomic(""),
124+
ready: make(chan struct{}),
125+
pub: pubsub.NewPublisher[string](time.Second, 2),
126+
}
127+
r.meta.Store(&pb2.Meta{LocalVersion: version})
128+
return r
129+
}
130+
131+
func requireReady(t *testing.T, ch <-chan struct{}) {
132+
t.Helper()
133+
select {
134+
case <-ch:
135+
default:
136+
t.Fatal("expected ready channel to be closed")
137+
}
138+
}
139+
140+
func notReady(t *testing.T, ch <-chan struct{}) {
141+
t.Helper()
142+
select {
143+
case <-ch:
144+
t.Fatal("expected ready channel to stay open")
145+
default:
146+
}
147+
}

0 commit comments

Comments
 (0)