diff --git a/CHANGELOG.md b/CHANGELOG.md index 79da341..5cc955c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Handle cases where non-legacy puzzles don't have an `author` attribute - Handle YAML-formatted file and script lists as expected - YAML-formatted example puzzle actually works as expected +- points.log will now always be sorted chronologically ## [3.4.3] - 2019-11-20 ### Fixed diff --git a/src/instance.go b/src/instance.go index f446439..bdb9fdd 100644 --- a/src/instance.go +++ b/src/instance.go @@ -37,6 +37,7 @@ type Instance struct { nextAttempt map[string]time.Time nextAttemptMutex *sync.RWMutex mux *http.ServeMux + PointsMux *sync.RWMutex } func (ctx *Instance) Initialize() error { @@ -54,6 +55,7 @@ func (ctx *Instance) Initialize() error { ctx.nextAttempt = map[string]time.Time{} ctx.nextAttemptMutex = new(sync.RWMutex) ctx.mux = http.NewServeMux() + ctx.PointsMux = new(sync.RWMutex) ctx.BindHandlers() ctx.MaybeInitialize() @@ -83,7 +85,11 @@ func (ctx *Instance) MaybeInitialize() { // Remove any extant control and state files os.Remove(ctx.StatePath("until")) os.Remove(ctx.StatePath("disabled")) + + ctx.PointsMux.Lock() os.Remove(ctx.StatePath("points.log")) + ctx.PointsMux.Unlock() + os.RemoveAll(ctx.StatePath("points.tmp")) os.RemoveAll(ctx.StatePath("points.new")) os.RemoveAll(ctx.StatePath("teams")) @@ -156,6 +162,10 @@ func (ctx *Instance) PointsLog(teamId string) []*Award { var ret []*Award fn := ctx.StatePath("points.log") + + ctx.PointsMux.RLock() + defer ctx.PointsMux.RUnlock() + f, err := os.Open(fn) if err != nil { log.Printf("Unable to open %s: %s", fn, err) diff --git a/src/maintenance.go b/src/maintenance.go index 0174902..65bb70e 100644 --- a/src/maintenance.go +++ b/src/maintenance.go @@ -4,9 +4,11 @@ import ( "bufio" "encoding/json" "fmt" + "io" "io/ioutil" "log" "os" + "sort" "strconv" "strings" "time" @@ -95,7 +97,7 @@ func (ctx *Instance) generatePointsLog(teamId string) []byte { log.Printf("Marshalling points.js: %v", err) return nil } - + if len(teamId) == 0 { ctx.jPointsLog = jpl } @@ -203,6 +205,9 @@ func (ctx *Instance) readTeams() { // collectPoints gathers up files in points.new/ and appends their contents to points.log, // removing each points.new/ file as it goes. func (ctx *Instance) collectPoints() { + ctx.PointsMux.Lock() + defer ctx.PointsMux.Unlock() + logf, err := os.OpenFile(ctx.StatePath("points.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { log.Printf("Can't append to points log: %s", err) @@ -248,6 +253,46 @@ func (ctx *Instance) collectPoints() { } } +// Ensure that points.log is sorted chronologically +func (ctx *Instance) sortPoints() { + var points []*Award + + ctx.PointsMux.Lock() + defer ctx.PointsMux.Unlock() + + logf, err := os.OpenFile(ctx.StatePath("points.log"), os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + log.Printf("Can't sort points.log: %s", err) + return + } + defer logf.Close() + + scanner := bufio.NewScanner(logf) + for scanner.Scan() { + line := scanner.Text() + cur, err := ParseAward(line) + if err != nil { + log.Printf("Encountered malformed award line, not sorting %s: %s", line, err) + return + } + + points = append(points, cur) + } + + // Only sort and write to file if we need to + if ! sort.SliceIsSorted(points, func( i, j int) bool { return points[i].When.Before(points[j].When) }) { + + sort.SliceStable(points, func(i, j int) bool { return points[i].When.Before(points[j].When) }) + + logf.Seek(0, io.SeekStart) + + for i := range points { + point := points[i] + fmt.Fprintf(logf, "%s\n", point.String()) + } + } +} + func (ctx *Instance) isEnabled() bool { // Skip if we've been disabled if _, err := os.Stat(ctx.StatePath("disabled")); err == nil { @@ -293,6 +338,7 @@ func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) { ctx.tidy() ctx.readTeams() ctx.collectPoints() + ctx.sortPoints() ctx.generatePuzzleList() ctx.generatePointsLog("") }