summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--courses.go53
-rw-r--r--err.go1
-rw-r--r--index.go23
-rw-r--r--wsc.go43
-rw-r--r--wsm.go53
-rw-r--r--wsp.go15
6 files changed, 115 insertions, 73 deletions
diff --git a/courses.go b/courses.go
index 45e6fd9..80f0506 100644
--- a/courses.go
+++ b/courses.go
@@ -94,24 +94,9 @@ func checkCourseGroup(cg courseGroupT) bool {
return courseGroups[cg]
}
-/*
- * The courses are simply stored in a map indexed by the course ID, although
- * the course struct itself also contains an ID field. A lock is embedded
- * inside the struct; we use a lock here instead of a pointer to a lock as
- * it would be easy to forget to initialize the lock when creating the
- * struct. However, this means that the struct could not be copied (though
- * this should only ever happen during creation anyways), therefore we use a
- * pointer to the struct as the value of the map, instead of the struct itself.
- */
-var courses map[int](*courseT)
+var courses sync.Map /* int, *courseT */
-/*
- * This RWMutex is only for massive modifications of the course struct, since
- * locking it on every write would be inefficient; in normal operation the only
- * write that could occur to the courses struct is changing the Selected
- * number, which should be handled with courseT.SelectedLock.
- */
-var coursesLock sync.RWMutex
+var numCourses uint32
/*
* Read course information from the database. This should be called during
@@ -119,11 +104,6 @@ var coursesLock sync.RWMutex
* a null pointer dereference.
*/
func setupCourses() error {
- coursesLock.Lock()
- defer coursesLock.Unlock()
-
- courses = make(map[int](*courseT))
-
rows, err := db.Query(
context.Background(),
"SELECT id, nmax, title, ctype, cgroup, teacher, location FROM courses",
@@ -184,7 +164,8 @@ func setupCourses() error {
err,
)
}
- courses[currentCourse.ID] = &currentCourse
+ courses.Store(currentCourse.ID, &currentCourse)
+ atomic.AddUint32(&numCourses, 1)
}
return nil
@@ -231,11 +212,19 @@ func populateUserCourseGroups(
)
}
var thisGroupName courseGroupT
- func() {
- coursesLock.RLock()
- defer coursesLock.RUnlock()
- thisGroupName = courses[thisCourseID].Group
- }()
+ _course, ok := courses.Load(thisCourseID)
+ if !ok {
+ return fmt.Errorf(
+ "%w: %d",
+ errNoSuchCourse,
+ thisCourseID,
+ )
+ }
+ course, ok := _course.(*courseT)
+ if !ok {
+ panic("courses map has non-\"*courseT\" items")
+ }
+ thisGroupName = course.Group
if _, ok := (*userCourseGroups)[thisGroupName]; ok {
return fmt.Errorf(
"%w: user %v, group %v",
@@ -258,7 +247,7 @@ func (course *courseT) decrementSelectedAndPropagate(
defer course.SelectedLock.Unlock()
atomic.AddUint32(&course.Selected, ^uint32(0))
}()
- go propagateSelectedUpdate(course.ID)
+ go propagateSelectedUpdate(course)
err := sendSelectedUpdate(ctx, conn, course.ID)
if err != nil {
return fmt.Errorf(
@@ -269,9 +258,3 @@ func (course *courseT) decrementSelectedAndPropagate(
}
return nil
}
-
-func getCourseByID(courseID int) *courseT {
- coursesLock.RLock()
- defer coursesLock.RUnlock()
- return courses[courseID]
-}
diff --git a/err.go b/err.go
index 59e22e6..44576ed 100644
--- a/err.go
+++ b/err.go
@@ -44,4 +44,5 @@ var (
errCannotGenerateRandomString = errors.New("cannot generate random string")
errContextCancelled = errors.New("context cancelled")
errCannotReceiveMessage = errors.New("cannot receive message")
+ errNoSuchCourse = errors.New("no such course")
)
diff --git a/index.go b/index.go
index 931cd2d..1050207 100644
--- a/index.go
+++ b/index.go
@@ -109,9 +109,26 @@ func handleIndex(w http.ResponseWriter, req *http.Request) {
return
}
+ /*
+ * Copy courses to _courses. The former is a sync.Map and the latter is
+ * a map[int]*courseT, and the former is very difficult to access from
+ * HTML templates.
+ */
+ _courses := make(map[int]*courseT)
+ courses.Range(func(key, value interface{}) bool {
+ courseID, ok := key.(int)
+ if !ok {
+ panic("courses map has non-\"int\" keys")
+ }
+ course, ok := value.(*courseT)
+ if !ok {
+ panic("courses map has non-\"*courseT\" items")
+ }
+ _courses[courseID] = course
+ return true
+ })
+
err = func() error {
- coursesLock.RLock()
- defer coursesLock.RUnlock()
return tmpl.ExecuteTemplate(
w,
"student",
@@ -121,7 +138,7 @@ func handleIndex(w http.ResponseWriter, req *http.Request) {
"Name": userName,
"Department": userDepartment,
},
- "courses": courses,
+ "courses": &_courses,
},
)
}()
diff --git a/wsc.go b/wsc.go
index ffcb6da..419a817 100644
--- a/wsc.go
+++ b/wsc.go
@@ -86,24 +86,37 @@ func handleConn(
/* TODO: Tell the user their current choices here. Deprecate HELLO. */
usems := make(map[int]*usemT)
- func() {
- atomic.AddInt64(&usemCount, int64(len(courses)))
- coursesLock.RLock()
- defer coursesLock.RUnlock()
- for courseID, course := range courses {
- usem := &usemT{} //exhaustruct:ignore
- usem.init()
- course.Usems.Store(userID, usem)
- usems[courseID] = usem
+
+ /* TODO: Check if the LoadUint32 here is a bit too much overhead */
+ atomic.AddInt64(&usemCount, int64(atomic.LoadUint32(&numCourses)))
+ courses.Range(func(key, value interface{}) bool {
+ /* TODO: Remember to change this too when changing the courseID type */
+ courseID, ok := key.(int)
+ if !ok {
+ panic("courses map has non-\"int\" keys")
}
- }()
+ course, ok := value.(*courseT)
+ if !ok {
+ panic("courses map has non-\"*courseT\" items")
+ }
+ usem := &usemT{} //exhaustruct:ignore
+ usem.init()
+ course.Usems.Store(userID, usem)
+ usems[courseID] = usem
+ return true
+ })
+
defer func() {
- coursesLock.RLock()
- defer coursesLock.RUnlock()
- for _, course := range courses {
+ courses.Range(func(key, value interface{}) bool {
+ _ = key
+ course, ok := value.(*courseT)
+ if !ok {
+ panic("courses map has non-\"*courseT\" items")
+ }
course.Usems.Delete(userID)
- }
- atomic.AddInt64(&usemCount, -int64(len(courses)))
+ return true
+ })
+ atomic.AddInt64(&usemCount, -int64(atomic.LoadUint32(&numCourses)))
}()
usemParent := make(chan int)
diff --git a/wsm.go b/wsm.go
index c163868..2398457 100644
--- a/wsm.go
+++ b/wsm.go
@@ -104,15 +104,20 @@ func messageChooseCourse(
return reportError("Course ID must be an integer")
}
courseID := int(_courseID)
- course := getCourseByID(courseID)
+ _course, ok := courses.Load(courseID)
+ if !ok {
+ return reportError("no such course")
+ }
+ course, ok := _course.(*courseT)
+ if !ok {
+ panic("courses map has non-\"*courseT\" items")
+ }
if course == nil {
- return reportError("nil course")
+ return reportError("couse is nil")
}
- thisCourseGroup := course.Group
-
- if _, ok := (*userCourseGroups)[thisCourseGroup]; ok {
+ if _, ok := (*userCourseGroups)[course.Group]; ok {
err := writeText(ctx, c, "R "+mar[1]+" :Group conflict")
if err != nil {
return fmt.Errorf(
@@ -181,7 +186,7 @@ func messageChooseCourse(
}()
if ok {
- go propagateSelectedUpdate(courseID)
+ go propagateSelectedUpdate(course)
err := tx.Commit(ctx)
if err != nil {
err := course.decrementSelectedAndPropagate(ctx, c)
@@ -201,7 +206,7 @@ func messageChooseCourse(
* This would race if message handlers could run
* concurrently for one connection.
*/
- (*userCourseGroups)[thisCourseGroup] = struct{}{}
+ (*userCourseGroups)[course.Group] = struct{}{}
err = writeText(ctx, c, "Y "+mar[1])
if err != nil {
@@ -275,10 +280,17 @@ func messageUnchooseCourse(
return reportError("Course ID must be an integer")
}
courseID := int(_courseID)
- course := getCourseByID(courseID)
+ _course, ok := courses.Load(courseID)
+ if !ok {
+ return reportError("no such course")
+ }
+ course, ok := _course.(*courseT)
+ if !ok {
+ panic("courses map has non-\"*courseT\" items")
+ }
if course == nil {
- return reportError("nil course")
+ return reportError("couse is nil")
}
ct, err := db.Exec(
@@ -302,16 +314,23 @@ func messageUnchooseCourse(
err,
)
}
- var thisCourseGroup courseGroupT
- func() {
- coursesLock.RLock()
- defer coursesLock.RUnlock()
- thisCourseGroup = courses[courseID].Group
- }()
- if _, ok := (*userCourseGroups)[thisCourseGroup]; !ok {
+
+ _course, ok := courses.Load(courseID)
+ if !ok {
+ return reportError("no such course")
+ }
+ course, ok := _course.(*courseT)
+ if !ok {
+ panic("courses map has non-\"*courseT\" items")
+ }
+ if course == nil {
+ return reportError("couse is nil")
+ }
+
+ if _, ok := (*userCourseGroups)[course.Group]; !ok {
return reportError("inconsistent user course groups")
}
- delete(*userCourseGroups, thisCourseGroup)
+ delete(*userCourseGroups, course.Group)
}
err = writeText(ctx, c, "N "+mar[1])
diff --git a/wsp.go b/wsp.go
index 125608b..2fab9fb 100644
--- a/wsp.go
+++ b/wsp.go
@@ -80,8 +80,7 @@ func makeReportError(ctx context.Context, conn *websocket.Conn) reportErrorT {
}
}
-func propagateSelectedUpdate(courseID int) {
- course := courses[courseID]
+func propagateSelectedUpdate(course *courseT) {
course.Usems.Range(func(key, value interface{}) bool {
_ = key
usem, ok := value.(*usemT)
@@ -98,7 +97,17 @@ func sendSelectedUpdate(
conn *websocket.Conn,
courseID int,
) error {
- course := courses[courseID]
+ _course, ok := courses.Load(courseID)
+ if !ok {
+ return fmt.Errorf("%w: %d", errNoSuchCourse, courseID)
+ }
+ course, ok := _course.(*courseT)
+ if !ok {
+ panic("courses map has non-\"*courseT\" items")
+ }
+ if course == nil {
+ return fmt.Errorf("%w: %d", errNoSuchCourse, courseID)
+ }
selected := atomic.LoadUint32(&course.Selected)
err := writeText(ctx, conn, fmt.Sprintf("M %d %d", courseID, selected))
if err != nil {