diff --git a/balancer/pkg/scoring/scoring.go b/balancer/pkg/scoring/scoring.go index 493eccf2f..4b47ff5ff 100644 --- a/balancer/pkg/scoring/scoring.go +++ b/balancer/pkg/scoring/scoring.go @@ -36,7 +36,6 @@ type ScoringService struct { currentScoresMutex *sync.Mutex lastSeenUpdate time.Time - newUpdate sync.Cond challengesMap map[string](bundle.JuiceShopChallenge) } @@ -59,7 +58,6 @@ func NewScoringServiceWithInitialScores(b *bundle.Bundle, initialScores map[stri currentScoresMutex: &sync.Mutex{}, lastSeenUpdate: time.Now(), - newUpdate: *sync.NewCond(&sync.Mutex{}), challengesMap: cachedChallengesMap, } @@ -80,23 +78,24 @@ func (s *ScoringService) WaitForUpdatesNewerThan(ctx context.Context, lastSeenUp } const maxWaitTime = 25 * time.Second - done := make(chan struct{}) - go func() { - s.newUpdate.Wait() - close(done) - }() - - select { - // check for update by subscribing to the newUpdate condition - case <-done: - // new update was received - return s.currentScoresSorted - case <-time.After(maxWaitTime): - // timeout was reached - return nil - case <-ctx.Done(): - // request was aborted - return nil + timeout := time.NewTimer(maxWaitTime) + ticker := time.NewTicker(50 * time.Millisecond) + defer timeout.Stop() + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if s.lastSeenUpdate.After(lastSeenUpdate) { + return s.currentScoresSorted + } + case <-timeout.C: + // Timeout was reached + return nil + case <-ctx.Done(): + // Context was canceled + return nil + } } } @@ -134,7 +133,7 @@ func (s *ScoringService) StartingScoringWorker(ctx context.Context) { s.currentScoresMutex.Lock() s.currentScores[score.Name] = score s.currentScoresSorted = sortTeamsByScoreAndCalculatePositions(s.currentScores) - s.newUpdate.Broadcast() + s.lastSeenUpdate = time.Now() s.currentScoresMutex.Unlock() case watch.Deleted: deployment := event.Object.(*appsv1.Deployment) @@ -142,7 +141,7 @@ func (s *ScoringService) StartingScoringWorker(ctx context.Context) { s.currentScoresMutex.Lock() delete(s.currentScores, team) s.currentScoresSorted = sortTeamsByScoreAndCalculatePositions(s.currentScores) - s.newUpdate.Broadcast() + s.lastSeenUpdate = time.Now() s.currentScoresMutex.Unlock() default: s.bundle.Log.Printf("Unknown event type: %v", event.Type) diff --git a/balancer/routes/score-board.go b/balancer/routes/score-board.go index 5b0f1bd2b..26b507b9e 100644 --- a/balancer/routes/score-board.go +++ b/balancer/routes/score-board.go @@ -19,20 +19,25 @@ func handleScoreBoard(bundle *b.Bundle, scoringService *scoring.ScoringService) func(responseWriter http.ResponseWriter, req *http.Request) { var totalTeams []*scoring.TeamScore + bundle.Log.Printf("handling score board request") if req.URL.Query().Get("wait-for-update-after") != "" { lastSeenUpdate, err := time.Parse(time.RFC3339, req.URL.Query().Get("wait-for-update-after")) if err != nil { + bundle.Log.Printf("Invalid time format") http.Error(responseWriter, "Invalid time format", http.StatusBadRequest) return } totalTeams = scoringService.WaitForUpdatesNewerThan(req.Context(), lastSeenUpdate) if totalTeams == nil { + bundle.Log.Printf("Got nothing from waiting for updates") responseWriter.WriteHeader(http.StatusNoContent) + responseWriter.Write([]byte{}) return } } else { totalTeams = scoringService.GetTopScores() } + bundle.Log.Printf("Got %d teams", len(totalTeams)) var topTeams []*scoring.TeamScore // limit score-board to calculate score for the top 24 teams only diff --git a/balancer/ui/src/pages/ScoreOverview.tsx b/balancer/ui/src/pages/ScoreOverview.tsx index 581254640..355b5eeba 100644 --- a/balancer/ui/src/pages/ScoreOverview.tsx +++ b/balancer/ui/src/pages/ScoreOverview.tsx @@ -39,10 +39,16 @@ interface Team { challenges: string[]; } -async function fetchTeams(): Promise { - const response = await fetch( - `/balancer/api/score-board/top?wait-for-update-after=${new Date().toISOString()}` - ); +async function fetchTeams(lastSeen: Date | null): Promise { + const url = lastSeen + ? `/balancer/api/score-board/top?wait-for-update-after=${lastSeen.toISOString()}` + : "/balancer/api/score-board/top"; + const response = await fetch(url); + + if (response.status === 204) { + return null; + } + const { teams } = await response.json(); return teams; } @@ -53,43 +59,36 @@ export function ScoreOverviewPage({ activeTeam: string | null; }) { const [teams, setTeams] = useState([]); - useEffect(() => { - fetchTeams().then(setTeams); - - const timer = setInterval(() => { - fetchTeams().then(setTeams); - }, 5000); - - return () => { - clearInterval(timer); - }; - }, []); - const [lastUpdateStarted, setLastUpdateStarted] = useState(Date.now()); let timeout: number | null = null; - async function updateScoreData() { + const updateScoreData = async (lastSuccessfulUpdate: Date | null) => { try { - setLastUpdateStarted(Date.now()); - const status = await fetchTeams(); - setTeams(status); + const lastUpdateStarted = new Date(); + const status = await fetchTeams(lastSuccessfulUpdate); + if (status !== null) { + setTeams(status); + } // the request is using a http long polling mechanism to get the updates as soon as possible // in case the request returns immediatly we wait for at least 3 seconds to ensure we aren't spamming the server - const waitTime = Math.max(3000, 5000 - (Date.now() - lastUpdateStarted)); + const waitTime = Math.max( + 3000, + 5000 - (Date.now() - lastUpdateStarted.getTime()) + ); console.log( "Waited for", - Date.now() - lastUpdateStarted, + Date.now() - lastUpdateStarted.getTime(), "ms for status update" ); console.log("Waiting for", waitTime, "ms until starting next request"); - timeout = window.setTimeout(() => updateScoreData(), waitTime); + timeout = window.setTimeout(() => updateScoreData(new Date()), waitTime); } catch (err) { console.error("Failed to fetch current teams!", err); } - } + }; useEffect(() => { - updateScoreData(); + updateScoreData(null); return () => { if (timeout !== null) { clearTimeout(timeout);