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 {
|
2023-06-03 22:31:09 -07:00
|
|
|
// 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
|
2023-06-03 22:31:09 -07:00
|
|
|
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),
|
2023-06-03 22:31:09 -07:00
|
|
|
update: make(chan time.Duration),
|
|
|
|
|
done: make(chan bool),
|
2023-05-30 21:45:54 -07:00
|
|
|
vote: vote{
|
|
|
|
|
VoterID: uniuri.New(),
|
|
|
|
|
},
|
2023-06-04 13:59:06 -07:00
|
|
|
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) {
|
2023-06-03 22:31:09 -07:00
|
|
|
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))
|
2023-06-03 22:31:09 -07:00
|
|
|
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 {
|
2023-06-03 22:31:09 -07:00
|
|
|
case <-t.C:
|
2023-06-17 14:58:49 -07:00
|
|
|
v.sendVote(ctx, ec)
|
2023-05-30 20:45:13 -07:00
|
|
|
|
2023-06-03 22:31:09 -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
|
|
|
|
2023-06-03 22:31:09 -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
|
|
|
|
2023-06-03 22:31:09 -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",
|
2023-06-18 08:18:33 -07:00
|
|
|
"voterID", v.vote.VoterID,
|
2023-06-17 14:58:49 -07:00
|
|
|
}, vals...)...)
|
2023-06-03 21:12:48 -07:00
|
|
|
}
|