From 2c54ea4a14f09451511952683748b78edef7a8d2 Mon Sep 17 00:00:00 2001 From: LeafHacker Date: Sun, 13 Oct 2019 13:49:02 +0100 Subject: [PATCH] runner utils and minecraft purge cache --- go.sum | 3 ++ src/api/v1/minecraft.go | 37 ++++++++++------- src/api/v1/minecraft_test.go | 10 ----- src/api/v1/motd.go | 21 +++++----- src/util/runners.go | 31 ++++++++++++++ src/util/runners_test.go | 80 ++++++++++++++++++++++++++++++++++++ 6 files changed, 146 insertions(+), 36 deletions(-) create mode 100644 src/util/runners.go create mode 100644 src/util/runners_test.go diff --git a/go.sum b/go.sum index a188865..09bab99 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ github.com/aws/aws-sdk-go v1.25.9 h1:WtVzerf5wSgPwlTTwl+ktCq/0GCS5MI9ZlLIcjsTr+Q github.com/aws/aws-sdk-go v1.25.9/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= @@ -73,6 +74,7 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191009170203-06d7bd2c5f4f h1:hjzMYz/7Ea1mNKfOnFOfktR0mlA5jqhvywClCMHM/qw= golang.org/x/sys v0.0.0-20191009170203-06d7bd2c5f4f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -86,4 +88,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/src/api/v1/minecraft.go b/src/api/v1/minecraft.go index 607933c..9337eff 100644 --- a/src/api/v1/minecraft.go +++ b/src/api/v1/minecraft.go @@ -5,9 +5,12 @@ import ( "encoding/hex" "errors" "fmt" + "github.com/ImpactDevelopment/ImpactServer/src/cloudflare" + "github.com/ImpactDevelopment/ImpactServer/src/util" "io/ioutil" "log" "net/http" + "reflect" "sort" "strings" "time" @@ -35,30 +38,34 @@ type ( var loginData map[string]*userInfo func init() { - err := updateData() + _, err := updateData() if err != nil { panic(err) } - go func() { - ticker := time.NewTicker(5 * time.Minute) - for range ticker.C { - err := updateData() - if err != nil { - log.Println("MC ERROR", err) - } + util.DoRepeatedly(5*time.Minute, func() { + updated, err := updateData() + if err != nil { + log.Println("MC ERROR", err) } - }() + if updated { + log.Println("MC UPDATE: Updated user info") + cloudflare.PurgeURLs([]string{"https://api.impactclient.net/v1/minecraft/user/info"}) + } + }) } -func updateData() error { +func updateData() (updated bool, err error) { lists, err := getLegacyUUIDLists() if err != nil { - return err + return } - loginData = mapLegacyListsToUserInfoList(lists) - // TODO if new data != old data, tell cloudflare to purge this URL lmao - // unironic btw - return nil + newLoginData := mapLegacyListsToUserInfoList(lists) + // reflect.DeepEqual is slow, especially since this map is big + if loginData != nil && !reflect.DeepEqual(newLoginData, loginData) { + loginData = newLoginData + updated = true + } + return } // API Handler diff --git a/src/api/v1/minecraft_test.go b/src/api/v1/minecraft_test.go index d5d1461..e07f4b8 100644 --- a/src/api/v1/minecraft_test.go +++ b/src/api/v1/minecraft_test.go @@ -55,14 +55,4 @@ func TestMapLegacyListsToUserInfoList(t *testing.T) { for _, role := range fred.Roles { assert.Contains(t, fredRoleIDs, role.ID) } - -} - -func hasRole(info *userInfo, id string) bool { - for _, role := range info.Roles { - if role.ID == id { - return true - } - } - return false } diff --git a/src/api/v1/motd.go b/src/api/v1/motd.go index 1c37379..9faec15 100644 --- a/src/api/v1/motd.go +++ b/src/api/v1/motd.go @@ -1,6 +1,7 @@ package v1 import ( + "github.com/ImpactDevelopment/ImpactServer/src/util" "io/ioutil" "log" "net/http" @@ -18,24 +19,22 @@ func init() { var err error motd, err = fetchMotd() if err != nil { - panic(err) + log.Println("MOTD ERROR", err) + motd = "Ok, so our MOTD service may or may not be semi-broken right now..." } - go func() { - ticker := time.NewTicker(3 * time.Minute) - for range ticker.C { - newer, err := fetchMotd() - if err != nil { - log.Println("MOTD ERROR", err) - } - newMotd(newer) + util.DoRepeatedly(3*time.Minute, func() { + newer, err := fetchMotd() + if err != nil { + log.Println("MOTD ERROR", err) } - }() + newMotd(newer) + }) } func newMotd(newer string) { if newer != motd { - motd = newer log.Println("MOTD UPDATE from", motd, "to", newer) + motd = newer cloudflare.PurgeURLs([]string{"https://api.impactclient.net/v1/motd"}) } } diff --git a/src/util/runners.go b/src/util/runners.go new file mode 100644 index 0000000..0fc4ce1 --- /dev/null +++ b/src/util/runners.go @@ -0,0 +1,31 @@ +package util + +import ( + "time" +) + +// DoLater runs a callback function once, after the specified delay +func DoLater(delay time.Duration, f func()) { + go func() { + time.Sleep(delay) + f() + }() +} + +// DoRepeatedly runs a callback function after each interval of delay +// it continues until quit is closed +func DoRepeatedly(interval time.Duration, f func()) (quit chan struct{}) { + quit = make(chan struct{}) + go func() { + ticker := time.NewTicker(interval) + for range ticker.C { + select { + case <-quit: + return + default: + f() + } + } + }() + return +} diff --git a/src/util/runners_test.go b/src/util/runners_test.go new file mode 100644 index 0000000..be0187e --- /dev/null +++ b/src/util/runners_test.go @@ -0,0 +1,80 @@ +package util + +import ( + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestDoLater(t *testing.T) { + runs := 0 + start := time.Now() + delay := 2 * time.Millisecond + + // Test in the goroutine for epic memes, sleep on the main thread to give it time to iterate a bit + DoLater(delay, callback(t, start, delay, &runs)) + + time.Sleep(100 * delay) + assert.Equal(t, 1, runs, "Should only run once") + + // Reset the test to iterate over DoLater a few times + runs = 0 + var i int64 + for i = 1; i < 100; i++ { + // Give each iteration its own time.Now() just in case this loop crosses into a new ms + // Use an offset delay so they don't all run at once + offsetDelay := time.Duration(i*delay.Milliseconds()) * time.Millisecond + DoLater(offsetDelay, callback(t, time.Now(), delay, &runs)) + } + time.Sleep(time.Duration(i*delay.Milliseconds()) * time.Millisecond) + assert.Equal(t, i-1, int64(runs), "Expected the number of runs to match the number of loop iterations") +} + +func TestDoRepeatedly(t *testing.T) { + runs := 0 + start := time.Now() + interval := 2 * time.Millisecond + + // Test in the goroutine for epic memes, sleep on the main thread to give it time to iterate a bit + quit := DoRepeatedly(interval, callback(t, start, interval, &runs)) + + // Let the test run for approx 100 iterations + // Slow, but ensures the timer doesn't go out of sync after a few iterations + time.Sleep(100 * interval) + + // Test closing channel + close(quit) + timesRun := runs + time.Sleep(50 * interval) + assert.Equal(t, timesRun, runs, "Should not run again after closing channel") + + // Reset the test to do a couple other bits + runs = 0 + timesRun = 0 + quit = DoRepeatedly(interval/2, func() { + runs++ + }) + + assert.Equal(t, 0, runs, "Should not run immediately") + time.Sleep(2 * interval) + assert.Greater(t, runs, 1, "Should have run more than once") + + // Quit by sending to channel + quit <- struct{}{} + timesRun = runs + + time.Sleep(50 * interval) + assert.Equal(t, timesRun, runs, "Should not run again sending to channel") +} + +// callback generates a callback function that tests it is run +// at the expected interval(s) after the start time and increments +// `runs` each time the callback is invoked +func callback(t *testing.T, start time.Time, interval time.Duration, runs *int) func() { + return func() { + *runs++ + delay := time.Now().Sub(start).Milliseconds() + expect := int64(*runs) * interval.Milliseconds() + assert.Equal(t, expect, delay, "Expected %d delay after %d runs", expect, *runs) + } +}