Files
elect/voter.go

204 lines
3.5 KiB
Go
Raw Permalink Normal View History

2023-05-30 20:45:13 -07:00
package elect
import (
2023-06-17 14:58:49 -07:00
"context"
2023-06-03 21:12:48 -07:00
"crypto/hmac"
2023-05-30 21:45:54 -07:00
"encoding/json"
"log"
2023-05-30 20:45:13 -07:00
"time"
"github.com/dchest/uniuri"
"github.com/go-resty/resty/v2"
2023-06-17 14:58:49 -07:00
"github.com/gopatchy/event"
2023-05-30 21:45:54 -07:00
"github.com/samber/lo"
2023-05-30 20:45:13 -07:00
)
type Voter struct {
// used by user and loop() goroutines
update chan time.Duration
done chan bool
// used by loop() goroutine only
2023-05-30 20:45:13 -07:00
client *resty.Client
2023-05-30 21:45:54 -07:00
signingKey []byte
vote vote
period time.Duration
2023-05-30 21:45:54 -07:00
}
type vote struct {
2023-06-03 21:51:07 -07:00
VoterID string `json:"voterID"`
LastSeenCandidateID string `json:"lastSeenCandidateID"`
NumPollsSinceChange int `json:"numPollsSinceChange"`
VoteSent time.Time `json:"voteSent"`
2023-05-31 22:40:48 -07:00
// Used internally by Candidate
received time.Time
2023-05-30 21:45:54 -07:00
}
type voteResponse struct {
2023-06-03 21:51:07 -07:00
CandidateID string `json:"candidateID"`
ResponseSent time.Time `json:"responseSent"`
2023-05-30 20:45:13 -07:00
}
2023-06-17 14:58:49 -07:00
func NewVoter(ctx context.Context, ec *event.Client, url string, signingKey string) *Voter {
2023-05-30 20:45:13 -07:00
v := &Voter{
client: resty.New().
2023-05-30 21:45:54 -07:00
SetCloseConnection(true).
2023-05-30 20:45:13 -07:00
SetBaseURL(url),
2023-05-30 21:45:54 -07:00
signingKey: []byte(signingKey),
update: make(chan time.Duration),
done: make(chan bool),
2023-05-30 21:45:54 -07:00
vote: vote{
VoterID: uniuri.New(),
},
period: maxVotePeriod,
2023-05-30 20:45:13 -07:00
}
2023-06-17 14:58:49 -07:00
go v.loop(ctx, ec)
2023-05-30 20:45:13 -07:00
return v
}
func (v *Voter) Stop() {
2023-06-04 14:04:15 -07:00
if v.update == nil {
return
}
2023-05-30 20:45:13 -07:00
close(v.update)
2023-05-30 21:45:54 -07:00
<-v.done
2023-06-04 14:04:15 -07:00
v.update = nil
2023-05-30 20:45:13 -07:00
}
2023-06-17 14:58:49 -07:00
func (v *Voter) loop(ctx context.Context, ec *event.Client) {
defer close(v.done)
2023-05-30 20:45:13 -07:00
2023-06-17 15:09:33 -07:00
defer func() {
v.log(ctx, ec,
"event", "stop",
)
}()
v.log(ctx, ec,
"event", "start",
)
2023-05-30 20:45:13 -07:00
for {
2023-06-17 14:58:49 -07:00
if !v.poll(ctx, ec) {
2023-05-30 21:45:54 -07:00
break
}
}
}
2023-06-17 14:58:49 -07:00
func (v *Voter) poll(ctx context.Context, ec *event.Client) bool {
2023-06-11 09:57:48 -07:00
t := time.NewTimer(RandDurationN(v.period))
defer t.Stop()
2023-05-30 21:45:54 -07:00
t2 := &time.Timer{}
2023-06-01 08:46:56 -07:00
if v.vote.NumPollsSinceChange <= 10 {
2023-06-11 09:57:48 -07:00
t2 = time.NewTimer(RandDurationN(maxFastVotePeriod))
2023-05-31 08:32:29 -07:00
defer t2.Stop()
2023-05-30 21:45:54 -07:00
}
select {
case <-t.C:
2023-06-17 14:58:49 -07:00
v.sendVote(ctx, ec)
2023-05-30 20:45:13 -07:00
case <-t2.C:
2023-06-17 14:58:49 -07:00
v.sendVote(ctx, ec)
2023-05-30 20:45:13 -07:00
case period, ok := <-v.update:
2023-05-30 21:45:54 -07:00
if !ok {
return false
2023-05-30 20:45:13 -07:00
}
2023-05-30 21:45:54 -07:00
v.period = period
2023-05-30 20:45:13 -07:00
}
2023-05-30 21:45:54 -07:00
return true
2023-05-30 20:45:13 -07:00
}
2023-06-17 14:58:49 -07:00
func (v *Voter) sendVote(ctx context.Context, ec *event.Client) {
2023-06-03 21:51:07 -07:00
v.vote.VoteSent = time.Now().UTC()
2023-05-31 22:40:48 -07:00
js := lo.Must(json.Marshal(v.vote))
2023-05-30 21:45:54 -07:00
resp, err := v.client.R().
2023-05-31 22:40:48 -07:00
SetHeader("Signature", mac(js, v.signingKey)).
2023-06-01 06:43:35 -07:00
SetHeader("Content-Type", "application/json").
2023-06-03 21:12:48 -07:00
SetHeader("Accept", "application/json").
2023-05-30 21:45:54 -07:00
SetBody(js).
2023-05-31 22:40:48 -07:00
Post("")
2023-05-30 21:45:54 -07:00
if err != nil {
2023-06-11 20:07:01 -07:00
log.Printf("error: %s", err)
2023-05-31 22:40:48 -07:00
v.vote.NumPollsSinceChange = 0
2023-05-30 21:45:54 -07:00
return
}
if resp.IsError() {
2023-06-17 14:58:49 -07:00
v.log(ctx, ec,
"event", "error",
"response", resp.StatusCode(),
)
2023-05-31 22:40:48 -07:00
v.vote.NumPollsSinceChange = 0
2023-05-30 21:45:54 -07:00
return
}
2023-06-03 21:12:48 -07:00
sig := resp.Header().Get("Signature")
if sig == "" {
2023-06-17 14:58:49 -07:00
v.log(ctx, ec,
"event", "error",
"error", "missing Signature response header",
)
2023-06-03 21:12:48 -07:00
return
}
if !hmac.Equal([]byte(sig), []byte(mac(resp.Body(), v.signingKey))) {
2023-06-17 14:58:49 -07:00
v.log(ctx, ec,
"event", "error",
"error", "invalid Signature response header",
)
2023-06-03 21:12:48 -07:00
return
}
vr := &voteResponse{}
err = json.Unmarshal(resp.Body(), vr)
if err != nil {
2023-06-17 14:58:49 -07:00
v.log(ctx, ec,
"event", "error",
"error", err,
)
2023-06-03 21:12:48 -07:00
return
}
2023-06-03 21:51:07 -07:00
if time.Since(vr.ResponseSent).Abs() > 15*time.Second {
2023-06-17 14:58:49 -07:00
v.log(ctx, ec,
"event", "error",
"error", "excessive time difference",
)
2023-06-03 21:51:07 -07:00
}
2023-05-30 21:45:54 -07:00
if vr.CandidateID == v.vote.LastSeenCandidateID {
v.vote.NumPollsSinceChange++
} else {
v.vote.LastSeenCandidateID = vr.CandidateID
v.vote.NumPollsSinceChange = 0
}
2023-05-30 20:45:13 -07:00
}
2023-06-03 21:12:48 -07:00
2023-06-17 14:58:49 -07:00
func (v *Voter) log(ctx context.Context, ec *event.Client, vals ...any) {
ec.Log(ctx, append([]any{
"library", "elect",
"subsystem", "voter",
"voterID", v.vote.VoterID,
2023-06-17 14:58:49 -07:00
}, vals...)...)
2023-06-03 21:12:48 -07:00
}