-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
Summary
In LocalLeaderboardScheduler, tournaments can occasionally miss either the end or reset callback when the tournament end (endActive) and reset/expiry (expiry) fall on the same Unix second. This is a race between two timers, not a deterministic bug.
Regular leaderboards (non-tournament) are not affected.
Details
For tournaments, Update() sets two timers:
ls.endActiveTimer = time.AfterFunc(endActiveDuration, func() {
ls.queueEndActiveElapse(time.Unix(earliestEndActive, 0).UTC(), endActiveLeaderboardIds)
})
ls.expiryTimer = time.AfterFunc(expiryDuration, func() {
ls.queueExpiryElapse(time.Unix(earliestExpiry, 0).UTC(), expiryLeaderboardIds)
})
Both callbacks then call Update():
func (ls *LocalLeaderboardScheduler) queueEndActiveElapse(...) {
...
ls.Update()
}
func (ls *LocalLeaderboardScheduler) queueExpiryElapse(...) {
...
ls.Update()
}
Update() stops both timers:
if ls.endActiveTimer != nil { ls.endActiveTimer.Stop() }
if ls.expiryTimer != nil { ls.expiryTimer.Stop() }
If endActive == expiry and one timer’s callback runs first while the other has not yet fired, Update() can Stop() the other timer before its callback executes. Then only one of:
- the tournament end callback (
fnTournamentEnd), or - the tournament reset callback (
fnTournamentReset/ reset logic)
runs for that second. If both timers have already fired, both callbacks run and there is no issue, so the behavior is intermittent.
Impact
If a tournament’s end time is configured to land exactly on a reset boundary (e.g. cron * * * * * plus matching end_time), the final reset or final end callback may sometimes be skipped, depending on timing.
Suggestions
Possible fixes:
- Ensure
endActiveandexpiryare never equal (e.g. offset one by 1 second), or - Avoid stopping the “other” timer in
Update()when called from a timer callback, or - Use a single timer and process all due actions (end + reset) when it fires.