2023-05-30 20:45:13 -07:00
|
|
|
package elect
|
|
|
|
|
|
|
|
|
|
import (
|
2023-05-30 21:45:54 -07:00
|
|
|
"crypto/hmac"
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
2023-05-30 20:45:13 -07:00
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/dchest/uniuri"
|
|
|
|
|
"github.com/go-resty/resty/v2"
|
2023-05-30 21:45:54 -07:00
|
|
|
"github.com/samber/lo"
|
2023-05-30 20:45:13 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Voter struct {
|
|
|
|
|
client *resty.Client
|
2023-05-30 21:45:54 -07:00
|
|
|
signingKey []byte
|
2023-05-30 20:45:13 -07:00
|
|
|
update chan<- time.Duration
|
2023-05-30 21:45:54 -07:00
|
|
|
done <-chan bool
|
|
|
|
|
vote vote
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type vote struct {
|
|
|
|
|
VoterID string `json:"voterID"`
|
|
|
|
|
LastSeenCandidateID string `json:"lastSeenCandidateID"`
|
|
|
|
|
NumPollsSinceChange int `json:"numPollsSinceChange"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type voteResponse struct {
|
|
|
|
|
CandidateID string `json:"candidateID"`
|
2023-05-30 20:45:13 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewVoter(url string, signingKey string) *Voter {
|
2023-05-30 21:45:54 -07:00
|
|
|
update := make(chan time.Duration)
|
|
|
|
|
done := make(chan bool)
|
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-05-30 20:45:13 -07:00
|
|
|
update: update,
|
2023-05-30 21:45:54 -07:00
|
|
|
done: done,
|
|
|
|
|
vote: vote{
|
|
|
|
|
VoterID: uniuri.New(),
|
|
|
|
|
},
|
2023-05-30 20:45:13 -07:00
|
|
|
}
|
|
|
|
|
|
2023-05-30 21:45:54 -07:00
|
|
|
go v.loop(update, done)
|
2023-05-30 20:45:13 -07:00
|
|
|
|
|
|
|
|
return v
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (v *Voter) Stop() {
|
|
|
|
|
close(v.update)
|
2023-05-30 21:45:54 -07:00
|
|
|
<-v.done
|
2023-05-30 20:45:13 -07:00
|
|
|
}
|
|
|
|
|
|
2023-05-30 21:45:54 -07:00
|
|
|
func (v *Voter) loop(update <-chan time.Duration, done chan<- bool) {
|
2023-05-30 20:45:13 -07:00
|
|
|
t := time.NewTicker(5 * time.Second)
|
|
|
|
|
defer t.Stop()
|
2023-05-30 21:45:54 -07:00
|
|
|
defer close(done)
|
2023-05-30 20:45:13 -07:00
|
|
|
|
|
|
|
|
for {
|
2023-05-30 21:45:54 -07:00
|
|
|
if !v.poll(update, t) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (v *Voter) poll(update <-chan time.Duration, t *time.Ticker) bool {
|
|
|
|
|
t2 := &time.Timer{}
|
|
|
|
|
|
|
|
|
|
if v.vote.NumPollsSinceChange < 10 {
|
|
|
|
|
t2 = time.NewTimer(100 * time.Millisecond)
|
2023-05-31 08:32:29 -07:00
|
|
|
defer t2.Stop()
|
2023-05-30 21:45:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case <-t2.C:
|
|
|
|
|
v.sendVote()
|
2023-05-30 20:45:13 -07:00
|
|
|
|
2023-05-30 21:45:54 -07:00
|
|
|
case <-t.C:
|
|
|
|
|
v.sendVote()
|
2023-05-30 20:45:13 -07:00
|
|
|
|
2023-05-30 21:45:54 -07:00
|
|
|
case period, ok := <-update:
|
|
|
|
|
if !ok {
|
|
|
|
|
return false
|
2023-05-30 20:45:13 -07:00
|
|
|
}
|
2023-05-30 21:45:54 -07:00
|
|
|
|
|
|
|
|
t.Reset(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-05-30 21:45:54 -07:00
|
|
|
func (v *Voter) sendVote() {
|
|
|
|
|
js := lo.Must(json.Marshal(v.vote))
|
|
|
|
|
|
|
|
|
|
genMAC := hmac.New(sha256.New, v.signingKey)
|
|
|
|
|
genMAC.Write(js)
|
|
|
|
|
mac := fmt.Sprintf("%x", genMAC.Sum(nil))
|
|
|
|
|
|
|
|
|
|
vr := &voteResponse{}
|
|
|
|
|
|
|
|
|
|
resp, err := v.client.R().
|
|
|
|
|
SetHeader("Signature", mac).
|
|
|
|
|
SetBody(js).
|
|
|
|
|
SetResult(vr).
|
|
|
|
|
Post("_vote")
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("_vote response: %s", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if resp.IsError() {
|
|
|
|
|
log.Printf("_vote response: [%d] %s\n%s", resp.StatusCode(), resp.Status(), resp.String())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
}
|