Prototype voter
This commit is contained in:
@@ -36,3 +36,9 @@ linters:
|
|||||||
- thelper
|
- thelper
|
||||||
- varcheck
|
- varcheck
|
||||||
- varnamelen
|
- varnamelen
|
||||||
|
linters-settings:
|
||||||
|
tagliatelle:
|
||||||
|
case:
|
||||||
|
use-field-name: true
|
||||||
|
rules:
|
||||||
|
json: goCamel
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -5,6 +5,7 @@ go 1.20
|
|||||||
require (
|
require (
|
||||||
github.com/dchest/uniuri v1.2.0
|
github.com/dchest/uniuri v1.2.0
|
||||||
github.com/go-resty/resty/v2 v2.7.0
|
github.com/go-resty/resty/v2 v2.7.0
|
||||||
|
github.com/samber/lo v1.38.1
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
go.uber.org/goleak v1.2.1
|
go.uber.org/goleak v1.2.1
|
||||||
)
|
)
|
||||||
@@ -13,6 +14,7 @@ require (
|
|||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
|
||||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect
|
golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -10,10 +10,14 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
||||||
|
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||||
|
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
|
||||||
|
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM=
|
golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM=
|
||||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
|||||||
106
voter.go
106
voter.go
@@ -1,57 +1,127 @@
|
|||||||
package elect
|
package elect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dchest/uniuri"
|
"github.com/dchest/uniuri"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Voter struct {
|
type Voter struct {
|
||||||
client *resty.Client
|
client *resty.Client
|
||||||
instanceID string
|
signingKey []byte
|
||||||
signingKey string
|
|
||||||
update chan<- time.Duration
|
update chan<- time.Duration
|
||||||
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewVoter(url string, signingKey string) *Voter {
|
func NewVoter(url string, signingKey string) *Voter {
|
||||||
update := make(chan time.Duration) // intentionally 0-capacity
|
update := make(chan time.Duration)
|
||||||
|
done := make(chan bool)
|
||||||
|
|
||||||
v := &Voter{
|
v := &Voter{
|
||||||
client: resty.New().
|
client: resty.New().
|
||||||
|
SetCloseConnection(true).
|
||||||
SetBaseURL(url),
|
SetBaseURL(url),
|
||||||
instanceID: uniuri.New(),
|
signingKey: []byte(signingKey),
|
||||||
signingKey: signingKey,
|
|
||||||
update: update,
|
update: update,
|
||||||
|
done: done,
|
||||||
|
vote: vote{
|
||||||
|
VoterID: uniuri.New(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
go v.loop(update)
|
go v.loop(update, done)
|
||||||
|
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Voter) Stop() {
|
func (v *Voter) Stop() {
|
||||||
close(v.update)
|
close(v.update)
|
||||||
|
<-v.done
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Voter) loop(update <-chan time.Duration) {
|
func (v *Voter) loop(update <-chan time.Duration, done chan<- bool) {
|
||||||
t := time.NewTicker(5 * time.Second)
|
t := time.NewTicker(5 * time.Second)
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
if !v.poll(update, t) {
|
||||||
case <-t.C:
|
break
|
||||||
v.vote()
|
|
||||||
|
|
||||||
case period, ok := <-update:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Reset(period)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Voter) vote() {
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-t2.C:
|
||||||
|
v.sendVote()
|
||||||
|
|
||||||
|
case <-t.C:
|
||||||
|
v.sendVote()
|
||||||
|
|
||||||
|
case period, ok := <-update:
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Reset(period)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package elect_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gopatchy/elect"
|
"github.com/gopatchy/elect"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -10,8 +11,10 @@ import (
|
|||||||
func TestNew(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
v := elect.NewVoter("[::1]:1234", "abc123")
|
v := elect.NewVoter("https://[::1]:1234", "abc123")
|
||||||
require.NotNil(t, v)
|
require.NotNil(t, v)
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
defer v.Stop()
|
defer v.Stop()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user