fix for #107 that doesn't require a mutex

I didn't look into why the mutex was causing some sort of deadlock condition
on retrieving points. This fixes it, as long as the underlying filesystem
supports atomic file renames. So don't run this with NFS.
This commit is contained in:
Neale Pickett 2020-03-10 20:50:03 -06:00
parent be448eadc5
commit b896969d80
4 changed files with 76 additions and 64 deletions

View File

@ -14,6 +14,24 @@ type Award struct {
Points int Points int
} }
type AwardList []*Award
// Implement sort.Interface on AwardList
func (awards AwardList) Len() int {
return len(awards)
}
func (awards AwardList) Less(i, j int) bool {
return awards[i].When.Before(awards[j].When)
}
func (awards AwardList) Swap(i, j int) {
tmp := awards[i]
awards[i] = awards[j]
awards[j] = tmp
}
func ParseAward(s string) (*Award, error) { func ParseAward(s string) (*Award, error) {
ret := Award{} ret := Award{}

View File

@ -2,6 +2,7 @@ package main
import ( import (
"testing" "testing"
"sort"
) )
func TestAward(t *testing.T) { func TestAward(t *testing.T) {
@ -32,3 +33,23 @@ func TestAward(t *testing.T) {
t.Error("Not throwing error on bad points") t.Error("Not throwing error on bad points")
} }
} }
func TestAwardList(t *testing.T) {
a, _ := ParseAward("1536958399 1a2b3c4d counting 1")
b, _ := ParseAward("1536958400 1a2b3c4d counting 1")
c, _ := ParseAward("1536958300 1a2b3c4d counting 1")
list := AwardList{a, b, c}
if sort.IsSorted(list) {
t.Error("Unsorted list thinks it's sorted")
}
sort.Stable(list)
if (list[0] != c) || (list[1] != a) || (list[2] != b) {
t.Error("Sorting didn't")
}
if ! sort.IsSorted(list) {
t.Error("Sorted list thinks it isn't")
}
}

View File

@ -37,7 +37,6 @@ type Instance struct {
nextAttempt map[string]time.Time nextAttempt map[string]time.Time
nextAttemptMutex *sync.RWMutex nextAttemptMutex *sync.RWMutex
mux *http.ServeMux mux *http.ServeMux
PointsMux *sync.RWMutex
} }
func (ctx *Instance) Initialize() error { func (ctx *Instance) Initialize() error {
@ -55,7 +54,6 @@ func (ctx *Instance) Initialize() error {
ctx.nextAttempt = map[string]time.Time{} ctx.nextAttempt = map[string]time.Time{}
ctx.nextAttemptMutex = new(sync.RWMutex) ctx.nextAttemptMutex = new(sync.RWMutex)
ctx.mux = http.NewServeMux() ctx.mux = http.NewServeMux()
ctx.PointsMux = new(sync.RWMutex)
ctx.BindHandlers() ctx.BindHandlers()
ctx.MaybeInitialize() ctx.MaybeInitialize()
@ -85,10 +83,7 @@ func (ctx *Instance) MaybeInitialize() {
// Remove any extant control and state files // Remove any extant control and state files
os.Remove(ctx.StatePath("until")) os.Remove(ctx.StatePath("until"))
os.Remove(ctx.StatePath("disabled")) os.Remove(ctx.StatePath("disabled"))
ctx.PointsMux.Lock()
os.Remove(ctx.StatePath("points.log")) os.Remove(ctx.StatePath("points.log"))
ctx.PointsMux.Unlock()
os.RemoveAll(ctx.StatePath("points.tmp")) os.RemoveAll(ctx.StatePath("points.tmp"))
os.RemoveAll(ctx.StatePath("points.new")) os.RemoveAll(ctx.StatePath("points.new"))
@ -158,18 +153,15 @@ func (ctx *Instance) TooFast(teamId string) bool {
return now.Before(next) return now.Before(next)
} }
func (ctx *Instance) PointsLog(teamId string) []*Award { func (ctx *Instance) PointsLog(teamId string) AwardList {
var ret []*Award awardlist := AwardList{}
fn := ctx.StatePath("points.log") fn := ctx.StatePath("points.log")
ctx.PointsMux.RLock()
defer ctx.PointsMux.RUnlock()
f, err := os.Open(fn) f, err := os.Open(fn)
if err != nil { if err != nil {
log.Printf("Unable to open %s: %s", fn, err) log.Printf("Unable to open %s: %s", fn, err)
return ret return awardlist
} }
defer f.Close() defer f.Close()
@ -184,10 +176,10 @@ func (ctx *Instance) PointsLog(teamId string) []*Award {
if len(teamId) > 0 && cur.TeamId != teamId { if len(teamId) > 0 && cur.TeamId != teamId {
continue continue
} }
ret = append(ret, cur) awardlist = append(awardlist, cur)
} }
return ret return awardlist
} }
// AwardPoints gives points to teamId in category. // AwardPoints gives points to teamId in category.

View File

@ -4,7 +4,6 @@ import (
"bufio" "bufio"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
@ -205,20 +204,26 @@ func (ctx *Instance) readTeams() {
// collectPoints gathers up files in points.new/ and appends their contents to points.log, // collectPoints gathers up files in points.new/ and appends their contents to points.log,
// removing each points.new/ file as it goes. // removing each points.new/ file as it goes.
func (ctx *Instance) collectPoints() { func (ctx *Instance) collectPoints() {
ctx.PointsMux.Lock() points := ctx.PointsLog("")
defer ctx.PointsMux.Unlock()
logf, err := os.OpenFile(ctx.StatePath("points.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) pointsFilename := ctx.StatePath("points.log")
pointsNewFilename := ctx.StatePath("points.log.new")
// Yo, this is delicate.
// If we have to return early, we must remove this file.
// If the file's written and we move it successfully,
// we need to remove all the little points files that built it.
newPoints, err := os.OpenFile(pointsNewFilename, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644)
if err != nil { if err != nil {
log.Printf("Can't append to points log: %s", err) log.Printf("Can't append to points log: %s", err)
return return
} }
defer logf.Close()
files, err := ioutil.ReadDir(ctx.StatePath("points.new")) files, err := ioutil.ReadDir(ctx.StatePath("points.new"))
if err != nil { if err != nil {
log.Printf("Error reading packages: %s", err) log.Printf("Error reading packages: %s", err)
} }
removearino := make([]string, 0, len(files))
for _, f := range files { for _, f := range files {
filename := ctx.StatePath("points.new", f.Name()) filename := ctx.StatePath("points.new", f.Name())
s, err := ioutil.ReadFile(filename) s, err := ioutil.ReadFile(filename)
@ -233,7 +238,7 @@ func (ctx *Instance) collectPoints() {
} }
duplicate := false duplicate := false
for _, e := range ctx.PointsLog("") { for _, e := range points {
if award.Same(e) { if award.Same(e) {
duplicate = true duplicate = true
break break
@ -243,56 +248,33 @@ func (ctx *Instance) collectPoints() {
if duplicate { if duplicate {
log.Printf("Skipping duplicate points: %s", award.String()) log.Printf("Skipping duplicate points: %s", award.String())
} else { } else {
fmt.Fprintf(logf, "%s\n", award.String()) points = append(points, award)
}
removearino = append(removearino, filename)
} }
logf.Sync() sort.Stable(points)
for _, point := range points {
fmt.Fprintln(newPoints, point.String())
}
newPoints.Close()
if err := os.Rename(pointsNewFilename, pointsFilename); err != nil {
log.Printf("Unable to move %s to %s: %s", pointsFilename, pointsNewFilename, err)
if err := os.Remove(pointsNewFilename); err != nil {
log.Printf("Also couldn't remove %s: %s", pointsNewFilename, err)
}
return
}
for _, filename := range removearino {
if err := os.Remove(filename); err != nil { if err := os.Remove(filename); err != nil {
log.Printf("Unable to remove %s: %s", filename, err) log.Printf("Unable to remove %s: %s", filename, err)
} }
} }
} }
// 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 { func (ctx *Instance) isEnabled() bool {
// Skip if we've been disabled // Skip if we've been disabled
if _, err := os.Stat(ctx.StatePath("disabled")); err == nil { if _, err := os.Stat(ctx.StatePath("disabled")); err == nil {
@ -338,7 +320,6 @@ func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
ctx.tidy() ctx.tidy()
ctx.readTeams() ctx.readTeams()
ctx.collectPoints() ctx.collectPoints()
ctx.sortPoints()
ctx.generatePuzzleList() ctx.generatePuzzleList()
ctx.generatePointsLog("") ctx.generatePointsLog("")
} }