diff --git a/.golangci.yaml b/.golangci.yaml index 9aa3e2e..ec9f2dc 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -36,3 +36,9 @@ linters: - thelper - varcheck - varnamelen +linters-settings: + tagliatelle: + case: + use-field-name: true + rules: + json: goCamel diff --git a/go.mod b/go.mod index 0d11776..267a09e 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.20 require ( github.com/dchest/uniuri v1.2.0 github.com/go-resty/resty/v2 v2.7.0 + github.com/samber/lo v1.38.1 github.com/stretchr/testify v1.8.4 go.uber.org/goleak v1.2.1 ) @@ -13,6 +14,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/text v0.2.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 gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 170cb07..0620610 100644 --- a/go.sum +++ b/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 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/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/voter.go b/voter.go index dfdfbd9..bd0e950 100644 --- a/voter.go +++ b/voter.go @@ -1,57 +1,127 @@ package elect import ( + "crypto/hmac" + "crypto/sha256" + "encoding/json" + "fmt" + "log" "time" "github.com/dchest/uniuri" "github.com/go-resty/resty/v2" + "github.com/samber/lo" ) type Voter struct { client *resty.Client - instanceID string - signingKey string + signingKey []byte 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 { - update := make(chan time.Duration) // intentionally 0-capacity + update := make(chan time.Duration) + done := make(chan bool) v := &Voter{ client: resty.New(). + SetCloseConnection(true). SetBaseURL(url), - instanceID: uniuri.New(), - signingKey: signingKey, + signingKey: []byte(signingKey), update: update, + done: done, + vote: vote{ + VoterID: uniuri.New(), + }, } - go v.loop(update) + go v.loop(update, done) return v } func (v *Voter) Stop() { 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) defer t.Stop() + defer close(done) for { - select { - case <-t.C: - v.vote() - - case period, ok := <-update: - if !ok { - return - } - - t.Reset(period) + if !v.poll(update, t) { + break } } } -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 + } } diff --git a/voter_test.go b/voter_test.go index 362baf8..13820e2 100644 --- a/voter_test.go +++ b/voter_test.go @@ -2,6 +2,7 @@ package elect_test import ( "testing" + "time" "github.com/gopatchy/elect" "github.com/stretchr/testify/require" @@ -10,8 +11,10 @@ import ( func TestNew(t *testing.T) { t.Parallel() - v := elect.NewVoter("[::1]:1234", "abc123") + v := elect.NewVoter("https://[::1]:1234", "abc123") require.NotNil(t, v) + time.Sleep(1 * time.Second) + defer v.Stop() }