Loading CHANGELOG.md +3 −0 Original line number Diff line number Diff line Loading @@ -7,6 +7,9 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr ### Changed - Upgrade devconsole handlebars (4.3.0) dependency. ### Fixed - Ensure tournament listing correctly uses the cursor on paginated requests. ## [2.9.0] - 2019-12-23 ### Added - New runtime functions to retrieve tournaments by ID. Loading server/api_tournament.go +3 −3 Original line number Diff line number Diff line Loading @@ -199,14 +199,14 @@ func (s *ApiServer) ListTournaments(ctx context.Context, in *api.ListTournaments } } var incomingCursor *tournamentListCursor var incomingCursor *TournamentListCursor if in.GetCursor() != "" { cb, err := base64.StdEncoding.DecodeString(in.GetCursor()) if err != nil { return nil, ErrLeaderboardInvalidCursor } incomingCursor = &tournamentListCursor{} incomingCursor = &TournamentListCursor{} if err := gob.NewDecoder(bytes.NewReader(cb)).Decode(incomingCursor); err != nil { return nil, ErrLeaderboardInvalidCursor } Loading Loading @@ -249,7 +249,7 @@ func (s *ApiServer) ListTournaments(ctx context.Context, in *api.ListTournaments } } records, err := TournamentList(ctx, s.logger, s.db, categoryStart, categoryEnd, startTime, endTime, limit, incomingCursor) records, err := TournamentList(ctx, s.logger, s.db, s.leaderboardCache, categoryStart, categoryEnd, startTime, endTime, limit, incomingCursor) if err != nil { return nil, status.Error(codes.Internal, "Error listing tournaments.") } Loading server/core_tournament.go +58 −57 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import ( "encoding/gob" "errors" "fmt" "strconv" "strings" "time" Loading @@ -42,9 +43,8 @@ var ( ErrTournamentWriteJoinRequired = errors.New("required to join before writing tournament record") ) type tournamentListCursor struct { // ID fields. TournamentId string type TournamentListCursor struct { Id string } func TournamentCreate(ctx context.Context, logger *zap.Logger, cache LeaderboardCache, scheduler LeaderboardScheduler, leaderboardId string, sortOrder, operator int, resetSchedule, metadata, Loading Loading @@ -234,53 +234,30 @@ WHERE id IN (` + strings.Join(statements, ",") + `) AND duration > 0` return records, nil } func TournamentList(ctx context.Context, logger *zap.Logger, db *sql.DB, categoryStart, categoryEnd, startTime, endTime, limit int, cursor *tournamentListCursor) (*api.TournamentList, error) { func TournamentList(ctx context.Context, logger *zap.Logger, db *sql.DB, leaderboardCache LeaderboardCache, categoryStart, categoryEnd, startTime, endTime, limit int, cursor *TournamentListCursor) (*api.TournamentList, error) { now := time.Now().UTC() nowUnix := now.Unix() query := `SELECT id, sort_order, reset_schedule, metadata, create_time, category, description, duration, end_time, max_size, max_num_score, title, size, start_time FROM leaderboard WHERE duration > 0 AND category >= $1 AND category <= $2 AND start_time >= $3` params := make([]interface{}, 0, 6) if categoryStart >= 0 && categoryStart <= 127 { params = append(params, categoryStart) } else { params = append(params, 0) } if categoryEnd >= 0 && categoryEnd <= 127 { params = append(params, categoryEnd) } else { params = append(params, 127) } if startTime >= 0 { params = append(params, time.Unix(int64(startTime), 0).UTC()) } else { params = append(params, time.Unix(0, 0).UTC()) list, newCursor, err := leaderboardCache.ListTournaments(nowUnix, categoryStart, categoryEnd, int64(startTime), int64(endTime), limit, cursor) if err != nil { logger.Error("Could not retrieve tournaments", zap.Error(err)) return nil, err } if endTime == 0 { query += " AND end_time = $4" params = append(params, time.Unix(0, 0).UTC()) } else if int64(endTime) < now.Unix() { // if end time is set explicitly in the past query += " AND end_time <= $4" params = append(params, time.Unix(int64(endTime), 0).UTC()) } else { // if end time is in the future, return both tournaments that end in the future as well as the ones that never end. query += " AND (end_time <= $4 OR end_time = '1970-01-01 00:00:00 UTC')" params = append(params, time.Unix(int64(endTime), 0).UTC()) if len(list) == 0 { return &api.TournamentList{ Tournaments: []*api.Tournament{}, }, nil } // To ensure that there are more records, so the cursor is returned params = append(params, limit+1) if cursor != nil { query += " AND id > $6" params = append(params, cursor.TournamentId) // Read most up to date sizes from database. statements := make([]string, 0, len(list)) params := make([]interface{}, 0, len(list)) for i, leaderboard := range list { params = append(params, leaderboard.Id) statements = append(statements, "$"+strconv.Itoa(i+1)) } query += " LIMIT $5" query := "SELECT id, size FROM leaderboard WHERE id IN (" + strings.Join(statements, ",") + ")" logger.Debug("Tournament listing query", zap.String("query", query), zap.Any("params", params)) rows, err := db.QueryContext(ctx, query, params...) if err != nil { Loading @@ -288,32 +265,56 @@ WHERE duration > 0 AND category >= $1 AND category <= $2 AND start_time >= $3` return nil, err } records := make([]*api.Tournament, 0) newCursor := &tournamentListCursor{} count := 0 sizes := make(map[string]int, len(list)) var dbID string var dbSize int for rows.Next() { tournament, err := parseTournament(rows, now) if err != nil { if err := rows.Scan(&dbID, &dbSize); err != nil { _ = rows.Close() logger.Error("Error parsing listed tournament records", zap.Error(err)) return nil, err } count++ sizes[dbID] = dbSize } _ = rows.Close() if count <= limit { records = append(records, tournament) } else if count > limit { newCursor.TournamentId = records[limit-1].Id records := make([]*api.Tournament, 0, len(list)) for _, leaderboard := range list { size := sizes[leaderboard.Id] startActive, endActiveUnix, expiryUnix := calculateTournamentDeadlines(leaderboard.StartTime, leaderboard.EndTime, int64(leaderboard.Duration), leaderboard.ResetSchedule, now) canEnter := true if startActive > nowUnix || endActiveUnix < nowUnix { canEnter = false } if canEnter && size >= leaderboard.MaxSize { canEnter = false } records = append(records, &api.Tournament{ Id: leaderboard.Id, Title: leaderboard.Title, Description: leaderboard.Description, Category: uint32(leaderboard.Category), SortOrder: uint32(leaderboard.SortOrder), Size: uint32(size), MaxSize: uint32(leaderboard.MaxSize), MaxNumScore: uint32(leaderboard.MaxNumScore), CanEnter: canEnter, EndActive: uint32(endActiveUnix), NextReset: uint32(expiryUnix), Metadata: leaderboard.Metadata, CreateTime: ×tamp.Timestamp{Seconds: leaderboard.CreateTime}, StartTime: ×tamp.Timestamp{Seconds: leaderboard.StartTime}, Duration: uint32(leaderboard.Duration), StartActive: uint32(startActive), }) } _ = rows.Close() tournamentList := &api.TournamentList{ Tournaments: records, } if newCursor.TournamentId != "" { if newCursor != nil { cursorBuf := new(bytes.Buffer) if err := gob.NewEncoder(cursorBuf).Encode(newCursor); err != nil { logger.Error("Error creating tournament records list cursor", zap.Error(err)) Loading server/leaderboard_cache.go +143 −21 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "log" "sort" "strconv" "sync" "time" Loading Loading @@ -109,6 +110,34 @@ func (l *Leaderboard) GetCreateTime() int64 { return l.CreateTime } // Type alias for a list of tournaments that binds together sorting functions. // Not intended to be used for sorting lists of non-tournament leaderboards. type OrderedTournaments []*Leaderboard func (t OrderedTournaments) Len() int { return len(t) } func (t OrderedTournaments) Swap(i, j int) { t[i], t[j] = t[j], t[i] } func (t OrderedTournaments) Less(i, j int) bool { ti, tj := t[i], t[j] if ti.StartTime < tj.StartTime { return true } else if ti.StartTime == tj.StartTime { if ti.EndTime > tj.EndTime { return true } else if ti.EndTime == tj.EndTime { if ti.Category < tj.Category { return true } else if ti.Category == tj.Category { return ti.Id < tj.Id } } } return false } type LeaderboardCache interface { Get(id string) *Leaderboard GetAllLeaderboards() []*Leaderboard Loading @@ -117,6 +146,7 @@ type LeaderboardCache interface { Insert(id string, authoritative bool, sortOrder, operator int, resetSchedule, metadata string, createTime int64) CreateTournament(ctx context.Context, id string, sortOrder, operator int, resetSchedule, metadata, title, description string, category, startTime, endTime, duration, maxSize, maxNumScore int, joinRequired bool) (*Leaderboard, error) InsertTournament(id string, sortOrder, operator int, resetSchedule, metadata, title, description string, category, duration, maxSize, maxNumScore int, joinRequired bool, createTime, startTime, endTime int64) ListTournaments(now int64, categoryStart, categoryEnd int, startTime, endTime int64, limit int, cursor *TournamentListCursor) ([]*Leaderboard, *TournamentListCursor, error) Delete(ctx context.Context, id string) error Remove(id string) } Loading @@ -126,6 +156,8 @@ type LocalLeaderboardCache struct { logger *zap.Logger db *sql.DB leaderboards map[string]*Leaderboard tournamentList []*Leaderboard } func NewLocalLeaderboardCache(logger, startupLogger *zap.Logger, db *sql.DB) LeaderboardCache { Loading @@ -133,6 +165,8 @@ func NewLocalLeaderboardCache(logger, startupLogger *zap.Logger, db *sql.DB) Lea logger: logger, db: db, leaderboards: make(map[string]*Leaderboard), tournamentList: make([]*Leaderboard, 0), } err := l.RefreshAllLeaderboards(context.Background()) Loading @@ -157,6 +191,7 @@ FROM leaderboard` } leaderboards := make(map[string]*Leaderboard) tournamentList := make([]*Leaderboard, 0) for rows.Next() { var id string Loading Loading @@ -217,11 +252,18 @@ FROM leaderboard` } leaderboards[id] = leaderboard if leaderboard.IsTournament() { tournamentList = append(tournamentList, leaderboard) } } _ = rows.Close() sort.Sort(OrderedTournaments(tournamentList)) l.Lock() l.leaderboards = leaderboards l.tournamentList = tournamentList l.Unlock() return nil Loading Loading @@ -252,6 +294,7 @@ func (l *LocalLeaderboardCache) Create(ctx context.Context, id string, authorita l.Unlock() return leaderboard, nil } l.Unlock() var expr *cronexpr.Expression var err error Loading Loading @@ -280,7 +323,6 @@ func (l *LocalLeaderboardCache) Create(ctx context.Context, id string, authorita var createTime pgtype.Timestamptz err = l.db.QueryRowContext(ctx, query, params...).Scan(&createTime) if err != nil { l.Unlock() l.logger.Error("Error creating leaderboard", zap.Error(err)) return nil, err } Loading @@ -296,8 +338,14 @@ func (l *LocalLeaderboardCache) Create(ctx context.Context, id string, authorita Metadata: metadata, CreateTime: createTime.Time.Unix(), } l.leaderboards[id] = leaderboard l.Lock() if leaderboard, ok := l.leaderboards[id]; ok { // Maybe multiple concurrent creations for this ID. l.Unlock() return leaderboard, nil } l.leaderboards[id] = leaderboard l.Unlock() return leaderboard, nil Loading @@ -315,8 +363,7 @@ func (l *LocalLeaderboardCache) Insert(id string, authoritative bool, sortOrder, } } l.Lock() l.leaderboards[id] = &Leaderboard{ leaderboard := &Leaderboard{ Id: id, Authoritative: authoritative, SortOrder: sortOrder, Loading @@ -326,11 +373,15 @@ func (l *LocalLeaderboardCache) Insert(id string, authoritative bool, sortOrder, Metadata: metadata, CreateTime: createTime, } l.Lock() l.leaderboards[id] = leaderboard l.Unlock() } func (l *LocalLeaderboardCache) CreateTournament(ctx context.Context, id string, sortOrder, operator int, resetSchedule, metadata, title, description string, category, startTime, endTime, duration, maxSize, maxNumScore int, joinRequired bool) (*Leaderboard, error) { if err := checkTournamentConfig(resetSchedule, startTime, endTime, duration, maxSize, maxNumScore); err != nil { resetCron, err := checkTournamentConfig(resetSchedule, startTime, endTime, duration, maxSize, maxNumScore) if err != nil { l.logger.Error("Error while creating tournament", zap.Error(err)) return nil, err } Loading Loading @@ -433,20 +484,19 @@ func (l *LocalLeaderboardCache) CreateTournament(ctx context.Context, id string, var createTime pgtype.Timestamptz var dbStartTime pgtype.Timestamptz var dbEndTime pgtype.Timestamptz err := l.db.QueryRowContext(ctx, query, params...).Scan(&createTime, &dbStartTime, &dbEndTime) err = l.db.QueryRowContext(ctx, query, params...).Scan(&createTime, &dbStartTime, &dbEndTime) if err != nil { l.logger.Error("Error creating tournament", zap.Error(err)) return nil, err } cron, _ := cronexpr.Parse(resetSchedule) leaderboard = &Leaderboard{ Id: id, Authoritative: true, SortOrder: sortOrder, Operator: operator, ResetScheduleStr: resetSchedule, ResetSchedule: cron, ResetSchedule: resetCron, Metadata: metadata, CreateTime: createTime.Time.Unix(), Category: category, Loading @@ -465,6 +515,8 @@ func (l *LocalLeaderboardCache) CreateTournament(ctx context.Context, id string, l.Lock() l.leaderboards[id] = leaderboard l.tournamentList = append(l.tournamentList, leaderboard) sort.Sort(OrderedTournaments(l.tournamentList)) l.Unlock() return leaderboard, nil Loading @@ -482,8 +534,7 @@ func (l *LocalLeaderboardCache) InsertTournament(id string, sortOrder, operator } } l.Lock() l.leaderboards[id] = &Leaderboard{ leaderboard := &Leaderboard{ Id: id, Authoritative: true, SortOrder: sortOrder, Loading @@ -502,12 +553,65 @@ func (l *LocalLeaderboardCache) InsertTournament(id string, sortOrder, operator StartTime: startTime, EndTime: endTime, } l.Lock() l.leaderboards[id] = leaderboard l.tournamentList = append(l.tournamentList, leaderboard) sort.Sort(OrderedTournaments(l.tournamentList)) l.Unlock() } func (l *LocalLeaderboardCache) ListTournaments(now int64, categoryStart, categoryEnd int, startTime, endTime int64, limit int, cursor *TournamentListCursor) ([]*Leaderboard, *TournamentListCursor, error) { list := make([]*Leaderboard, 0, limit) var newCursor *TournamentListCursor skip := cursor != nil l.RLock() for _, leaderboard := range l.tournamentList { if skip { if leaderboard.Id == cursor.Id { skip = false } continue } if leaderboard.Category < categoryStart { // Skip tournaments with category before start boundary. continue } if leaderboard.Category > categoryEnd { // Skip tournaments with category after end boundary. continue } if leaderboard.StartTime < startTime { // Skip tournaments with start time before filter. continue } if endTime == 0 && leaderboard.EndTime != 0 || (endTime < now && (leaderboard.EndTime > endTime || leaderboard.EndTime == 0)) || leaderboard.EndTime > endTime { // SKIP tournaments where: // - If end time filter is == 0, tournament end time is non-0. // - If end time filter is in the past, tournament end time is after the filter or 0 (never end). // - If end time is in the future, tournament end time is after the filter. continue } if ln := len(list); ln >= limit { newCursor = &TournamentListCursor{ Id: list[ln-1].Id, } break } list = append(list, leaderboard) } l.RUnlock() return list, newCursor, nil } func (l *LocalLeaderboardCache) Delete(ctx context.Context, id string) error { l.Lock() _, leaderboardFound := l.leaderboards[id] leaderboard, leaderboardFound := l.leaderboards[id] l.Unlock() if !leaderboardFound { Loading @@ -526,42 +630,60 @@ func (l *LocalLeaderboardCache) Delete(ctx context.Context, id string) error { l.Lock() // Then delete from cache. delete(l.leaderboards, id) if leaderboard.IsTournament() { for i, currentLeaderboard := range l.tournamentList { if currentLeaderboard.Id == id { l.tournamentList = append(l.tournamentList[:i], l.tournamentList[i+1:]...) break } } } l.Unlock() return nil } func (l *LocalLeaderboardCache) Remove(id string) { l.Lock() if leaderboard, ok := l.leaderboards[id]; ok { delete(l.leaderboards, id) if leaderboard.IsTournament() { for i, currentLeaderboard := range l.tournamentList { if currentLeaderboard.Id == id { l.tournamentList = append(l.tournamentList[:i], l.tournamentList[i+1:]...) break } } } } l.Unlock() } func checkTournamentConfig(resetSchedule string, startTime, endTime, duration, maxSize, maxNumScore int) error { func checkTournamentConfig(resetSchedule string, startTime, endTime, duration, maxSize, maxNumScore int) (*cronexpr.Expression, error) { if startTime < 0 { return fmt.Errorf("tournament start time must be a unix UTC time in the future") return nil, fmt.Errorf("tournament start time must be a unix UTC time in the future") } if duration <= 0 { return fmt.Errorf("tournament duration must be greater than zero") return nil, fmt.Errorf("tournament duration must be greater than zero") } if maxSize < 0 { return fmt.Errorf("tournament max must be greater than zero") return nil, fmt.Errorf("tournament max must be greater than zero") } if maxNumScore < 0 { return fmt.Errorf("tournament m num score must be greater than zero") return nil, fmt.Errorf("tournament m num score must be greater than zero") } if (endTime > 0) && (endTime < startTime) { return fmt.Errorf("tournament end time cannot be before start time") return nil, fmt.Errorf("tournament end time cannot be before start time") } var cron *cronexpr.Expression if resetSchedule != "" { expr, err := cronexpr.Parse(resetSchedule) if err != nil { return fmt.Errorf("could not parse reset schedule: %s", err.Error()) return nil, fmt.Errorf("could not parse reset schedule: %s", err.Error()) } cron = expr } Loading @@ -572,9 +694,9 @@ func checkTournamentConfig(resetSchedule string, startTime, endTime, duration, m // Check that the end time (if specified) is at least strictly after the first active period start time. if (endTime > 0) && (int64(endTime) <= firstResetUnix) { return fmt.Errorf("tournament end time cannot be before first reset schedule - either increase end time or change/disable reset schedule") return nil, fmt.Errorf("tournament end time cannot be before first reset schedule - either increase end time or change/disable reset schedule") } } return nil return cron, nil } server/runtime_go_nakama.go +3 −3 Original line number Diff line number Diff line Loading @@ -1473,19 +1473,19 @@ func (n *RuntimeGoNakamaModule) TournamentList(ctx context.Context, categoryStar return nil, errors.New("limit must be 1-100") } var cursorPtr *tournamentListCursor var cursorPtr *TournamentListCursor if cursor != "" { cb, err := base64.StdEncoding.DecodeString(cursor) if err != nil { return nil, errors.New("expects cursor to be valid when provided") } cursorPtr = &tournamentListCursor{} cursorPtr = &TournamentListCursor{} if err := gob.NewDecoder(bytes.NewReader(cb)).Decode(cursorPtr); err != nil { return nil, errors.New("expects cursor to be valid when provided") } } return TournamentList(ctx, n.logger, n.db, categoryStart, categoryEnd, startTime, endTime, limit, cursorPtr) return TournamentList(ctx, n.logger, n.db, n.leaderboardCache, categoryStart, categoryEnd, startTime, endTime, limit, cursorPtr) } func (n *RuntimeGoNakamaModule) TournamentRecordWrite(ctx context.Context, id, ownerID, username string, score, subscore int64, metadata map[string]interface{}) (*api.LeaderboardRecord, error) { Loading Loading
CHANGELOG.md +3 −0 Original line number Diff line number Diff line Loading @@ -7,6 +7,9 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr ### Changed - Upgrade devconsole handlebars (4.3.0) dependency. ### Fixed - Ensure tournament listing correctly uses the cursor on paginated requests. ## [2.9.0] - 2019-12-23 ### Added - New runtime functions to retrieve tournaments by ID. Loading
server/api_tournament.go +3 −3 Original line number Diff line number Diff line Loading @@ -199,14 +199,14 @@ func (s *ApiServer) ListTournaments(ctx context.Context, in *api.ListTournaments } } var incomingCursor *tournamentListCursor var incomingCursor *TournamentListCursor if in.GetCursor() != "" { cb, err := base64.StdEncoding.DecodeString(in.GetCursor()) if err != nil { return nil, ErrLeaderboardInvalidCursor } incomingCursor = &tournamentListCursor{} incomingCursor = &TournamentListCursor{} if err := gob.NewDecoder(bytes.NewReader(cb)).Decode(incomingCursor); err != nil { return nil, ErrLeaderboardInvalidCursor } Loading Loading @@ -249,7 +249,7 @@ func (s *ApiServer) ListTournaments(ctx context.Context, in *api.ListTournaments } } records, err := TournamentList(ctx, s.logger, s.db, categoryStart, categoryEnd, startTime, endTime, limit, incomingCursor) records, err := TournamentList(ctx, s.logger, s.db, s.leaderboardCache, categoryStart, categoryEnd, startTime, endTime, limit, incomingCursor) if err != nil { return nil, status.Error(codes.Internal, "Error listing tournaments.") } Loading
server/core_tournament.go +58 −57 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import ( "encoding/gob" "errors" "fmt" "strconv" "strings" "time" Loading @@ -42,9 +43,8 @@ var ( ErrTournamentWriteJoinRequired = errors.New("required to join before writing tournament record") ) type tournamentListCursor struct { // ID fields. TournamentId string type TournamentListCursor struct { Id string } func TournamentCreate(ctx context.Context, logger *zap.Logger, cache LeaderboardCache, scheduler LeaderboardScheduler, leaderboardId string, sortOrder, operator int, resetSchedule, metadata, Loading Loading @@ -234,53 +234,30 @@ WHERE id IN (` + strings.Join(statements, ",") + `) AND duration > 0` return records, nil } func TournamentList(ctx context.Context, logger *zap.Logger, db *sql.DB, categoryStart, categoryEnd, startTime, endTime, limit int, cursor *tournamentListCursor) (*api.TournamentList, error) { func TournamentList(ctx context.Context, logger *zap.Logger, db *sql.DB, leaderboardCache LeaderboardCache, categoryStart, categoryEnd, startTime, endTime, limit int, cursor *TournamentListCursor) (*api.TournamentList, error) { now := time.Now().UTC() nowUnix := now.Unix() query := `SELECT id, sort_order, reset_schedule, metadata, create_time, category, description, duration, end_time, max_size, max_num_score, title, size, start_time FROM leaderboard WHERE duration > 0 AND category >= $1 AND category <= $2 AND start_time >= $3` params := make([]interface{}, 0, 6) if categoryStart >= 0 && categoryStart <= 127 { params = append(params, categoryStart) } else { params = append(params, 0) } if categoryEnd >= 0 && categoryEnd <= 127 { params = append(params, categoryEnd) } else { params = append(params, 127) } if startTime >= 0 { params = append(params, time.Unix(int64(startTime), 0).UTC()) } else { params = append(params, time.Unix(0, 0).UTC()) list, newCursor, err := leaderboardCache.ListTournaments(nowUnix, categoryStart, categoryEnd, int64(startTime), int64(endTime), limit, cursor) if err != nil { logger.Error("Could not retrieve tournaments", zap.Error(err)) return nil, err } if endTime == 0 { query += " AND end_time = $4" params = append(params, time.Unix(0, 0).UTC()) } else if int64(endTime) < now.Unix() { // if end time is set explicitly in the past query += " AND end_time <= $4" params = append(params, time.Unix(int64(endTime), 0).UTC()) } else { // if end time is in the future, return both tournaments that end in the future as well as the ones that never end. query += " AND (end_time <= $4 OR end_time = '1970-01-01 00:00:00 UTC')" params = append(params, time.Unix(int64(endTime), 0).UTC()) if len(list) == 0 { return &api.TournamentList{ Tournaments: []*api.Tournament{}, }, nil } // To ensure that there are more records, so the cursor is returned params = append(params, limit+1) if cursor != nil { query += " AND id > $6" params = append(params, cursor.TournamentId) // Read most up to date sizes from database. statements := make([]string, 0, len(list)) params := make([]interface{}, 0, len(list)) for i, leaderboard := range list { params = append(params, leaderboard.Id) statements = append(statements, "$"+strconv.Itoa(i+1)) } query += " LIMIT $5" query := "SELECT id, size FROM leaderboard WHERE id IN (" + strings.Join(statements, ",") + ")" logger.Debug("Tournament listing query", zap.String("query", query), zap.Any("params", params)) rows, err := db.QueryContext(ctx, query, params...) if err != nil { Loading @@ -288,32 +265,56 @@ WHERE duration > 0 AND category >= $1 AND category <= $2 AND start_time >= $3` return nil, err } records := make([]*api.Tournament, 0) newCursor := &tournamentListCursor{} count := 0 sizes := make(map[string]int, len(list)) var dbID string var dbSize int for rows.Next() { tournament, err := parseTournament(rows, now) if err != nil { if err := rows.Scan(&dbID, &dbSize); err != nil { _ = rows.Close() logger.Error("Error parsing listed tournament records", zap.Error(err)) return nil, err } count++ sizes[dbID] = dbSize } _ = rows.Close() if count <= limit { records = append(records, tournament) } else if count > limit { newCursor.TournamentId = records[limit-1].Id records := make([]*api.Tournament, 0, len(list)) for _, leaderboard := range list { size := sizes[leaderboard.Id] startActive, endActiveUnix, expiryUnix := calculateTournamentDeadlines(leaderboard.StartTime, leaderboard.EndTime, int64(leaderboard.Duration), leaderboard.ResetSchedule, now) canEnter := true if startActive > nowUnix || endActiveUnix < nowUnix { canEnter = false } if canEnter && size >= leaderboard.MaxSize { canEnter = false } records = append(records, &api.Tournament{ Id: leaderboard.Id, Title: leaderboard.Title, Description: leaderboard.Description, Category: uint32(leaderboard.Category), SortOrder: uint32(leaderboard.SortOrder), Size: uint32(size), MaxSize: uint32(leaderboard.MaxSize), MaxNumScore: uint32(leaderboard.MaxNumScore), CanEnter: canEnter, EndActive: uint32(endActiveUnix), NextReset: uint32(expiryUnix), Metadata: leaderboard.Metadata, CreateTime: ×tamp.Timestamp{Seconds: leaderboard.CreateTime}, StartTime: ×tamp.Timestamp{Seconds: leaderboard.StartTime}, Duration: uint32(leaderboard.Duration), StartActive: uint32(startActive), }) } _ = rows.Close() tournamentList := &api.TournamentList{ Tournaments: records, } if newCursor.TournamentId != "" { if newCursor != nil { cursorBuf := new(bytes.Buffer) if err := gob.NewEncoder(cursorBuf).Encode(newCursor); err != nil { logger.Error("Error creating tournament records list cursor", zap.Error(err)) Loading
server/leaderboard_cache.go +143 −21 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "log" "sort" "strconv" "sync" "time" Loading Loading @@ -109,6 +110,34 @@ func (l *Leaderboard) GetCreateTime() int64 { return l.CreateTime } // Type alias for a list of tournaments that binds together sorting functions. // Not intended to be used for sorting lists of non-tournament leaderboards. type OrderedTournaments []*Leaderboard func (t OrderedTournaments) Len() int { return len(t) } func (t OrderedTournaments) Swap(i, j int) { t[i], t[j] = t[j], t[i] } func (t OrderedTournaments) Less(i, j int) bool { ti, tj := t[i], t[j] if ti.StartTime < tj.StartTime { return true } else if ti.StartTime == tj.StartTime { if ti.EndTime > tj.EndTime { return true } else if ti.EndTime == tj.EndTime { if ti.Category < tj.Category { return true } else if ti.Category == tj.Category { return ti.Id < tj.Id } } } return false } type LeaderboardCache interface { Get(id string) *Leaderboard GetAllLeaderboards() []*Leaderboard Loading @@ -117,6 +146,7 @@ type LeaderboardCache interface { Insert(id string, authoritative bool, sortOrder, operator int, resetSchedule, metadata string, createTime int64) CreateTournament(ctx context.Context, id string, sortOrder, operator int, resetSchedule, metadata, title, description string, category, startTime, endTime, duration, maxSize, maxNumScore int, joinRequired bool) (*Leaderboard, error) InsertTournament(id string, sortOrder, operator int, resetSchedule, metadata, title, description string, category, duration, maxSize, maxNumScore int, joinRequired bool, createTime, startTime, endTime int64) ListTournaments(now int64, categoryStart, categoryEnd int, startTime, endTime int64, limit int, cursor *TournamentListCursor) ([]*Leaderboard, *TournamentListCursor, error) Delete(ctx context.Context, id string) error Remove(id string) } Loading @@ -126,6 +156,8 @@ type LocalLeaderboardCache struct { logger *zap.Logger db *sql.DB leaderboards map[string]*Leaderboard tournamentList []*Leaderboard } func NewLocalLeaderboardCache(logger, startupLogger *zap.Logger, db *sql.DB) LeaderboardCache { Loading @@ -133,6 +165,8 @@ func NewLocalLeaderboardCache(logger, startupLogger *zap.Logger, db *sql.DB) Lea logger: logger, db: db, leaderboards: make(map[string]*Leaderboard), tournamentList: make([]*Leaderboard, 0), } err := l.RefreshAllLeaderboards(context.Background()) Loading @@ -157,6 +191,7 @@ FROM leaderboard` } leaderboards := make(map[string]*Leaderboard) tournamentList := make([]*Leaderboard, 0) for rows.Next() { var id string Loading Loading @@ -217,11 +252,18 @@ FROM leaderboard` } leaderboards[id] = leaderboard if leaderboard.IsTournament() { tournamentList = append(tournamentList, leaderboard) } } _ = rows.Close() sort.Sort(OrderedTournaments(tournamentList)) l.Lock() l.leaderboards = leaderboards l.tournamentList = tournamentList l.Unlock() return nil Loading Loading @@ -252,6 +294,7 @@ func (l *LocalLeaderboardCache) Create(ctx context.Context, id string, authorita l.Unlock() return leaderboard, nil } l.Unlock() var expr *cronexpr.Expression var err error Loading Loading @@ -280,7 +323,6 @@ func (l *LocalLeaderboardCache) Create(ctx context.Context, id string, authorita var createTime pgtype.Timestamptz err = l.db.QueryRowContext(ctx, query, params...).Scan(&createTime) if err != nil { l.Unlock() l.logger.Error("Error creating leaderboard", zap.Error(err)) return nil, err } Loading @@ -296,8 +338,14 @@ func (l *LocalLeaderboardCache) Create(ctx context.Context, id string, authorita Metadata: metadata, CreateTime: createTime.Time.Unix(), } l.leaderboards[id] = leaderboard l.Lock() if leaderboard, ok := l.leaderboards[id]; ok { // Maybe multiple concurrent creations for this ID. l.Unlock() return leaderboard, nil } l.leaderboards[id] = leaderboard l.Unlock() return leaderboard, nil Loading @@ -315,8 +363,7 @@ func (l *LocalLeaderboardCache) Insert(id string, authoritative bool, sortOrder, } } l.Lock() l.leaderboards[id] = &Leaderboard{ leaderboard := &Leaderboard{ Id: id, Authoritative: authoritative, SortOrder: sortOrder, Loading @@ -326,11 +373,15 @@ func (l *LocalLeaderboardCache) Insert(id string, authoritative bool, sortOrder, Metadata: metadata, CreateTime: createTime, } l.Lock() l.leaderboards[id] = leaderboard l.Unlock() } func (l *LocalLeaderboardCache) CreateTournament(ctx context.Context, id string, sortOrder, operator int, resetSchedule, metadata, title, description string, category, startTime, endTime, duration, maxSize, maxNumScore int, joinRequired bool) (*Leaderboard, error) { if err := checkTournamentConfig(resetSchedule, startTime, endTime, duration, maxSize, maxNumScore); err != nil { resetCron, err := checkTournamentConfig(resetSchedule, startTime, endTime, duration, maxSize, maxNumScore) if err != nil { l.logger.Error("Error while creating tournament", zap.Error(err)) return nil, err } Loading Loading @@ -433,20 +484,19 @@ func (l *LocalLeaderboardCache) CreateTournament(ctx context.Context, id string, var createTime pgtype.Timestamptz var dbStartTime pgtype.Timestamptz var dbEndTime pgtype.Timestamptz err := l.db.QueryRowContext(ctx, query, params...).Scan(&createTime, &dbStartTime, &dbEndTime) err = l.db.QueryRowContext(ctx, query, params...).Scan(&createTime, &dbStartTime, &dbEndTime) if err != nil { l.logger.Error("Error creating tournament", zap.Error(err)) return nil, err } cron, _ := cronexpr.Parse(resetSchedule) leaderboard = &Leaderboard{ Id: id, Authoritative: true, SortOrder: sortOrder, Operator: operator, ResetScheduleStr: resetSchedule, ResetSchedule: cron, ResetSchedule: resetCron, Metadata: metadata, CreateTime: createTime.Time.Unix(), Category: category, Loading @@ -465,6 +515,8 @@ func (l *LocalLeaderboardCache) CreateTournament(ctx context.Context, id string, l.Lock() l.leaderboards[id] = leaderboard l.tournamentList = append(l.tournamentList, leaderboard) sort.Sort(OrderedTournaments(l.tournamentList)) l.Unlock() return leaderboard, nil Loading @@ -482,8 +534,7 @@ func (l *LocalLeaderboardCache) InsertTournament(id string, sortOrder, operator } } l.Lock() l.leaderboards[id] = &Leaderboard{ leaderboard := &Leaderboard{ Id: id, Authoritative: true, SortOrder: sortOrder, Loading @@ -502,12 +553,65 @@ func (l *LocalLeaderboardCache) InsertTournament(id string, sortOrder, operator StartTime: startTime, EndTime: endTime, } l.Lock() l.leaderboards[id] = leaderboard l.tournamentList = append(l.tournamentList, leaderboard) sort.Sort(OrderedTournaments(l.tournamentList)) l.Unlock() } func (l *LocalLeaderboardCache) ListTournaments(now int64, categoryStart, categoryEnd int, startTime, endTime int64, limit int, cursor *TournamentListCursor) ([]*Leaderboard, *TournamentListCursor, error) { list := make([]*Leaderboard, 0, limit) var newCursor *TournamentListCursor skip := cursor != nil l.RLock() for _, leaderboard := range l.tournamentList { if skip { if leaderboard.Id == cursor.Id { skip = false } continue } if leaderboard.Category < categoryStart { // Skip tournaments with category before start boundary. continue } if leaderboard.Category > categoryEnd { // Skip tournaments with category after end boundary. continue } if leaderboard.StartTime < startTime { // Skip tournaments with start time before filter. continue } if endTime == 0 && leaderboard.EndTime != 0 || (endTime < now && (leaderboard.EndTime > endTime || leaderboard.EndTime == 0)) || leaderboard.EndTime > endTime { // SKIP tournaments where: // - If end time filter is == 0, tournament end time is non-0. // - If end time filter is in the past, tournament end time is after the filter or 0 (never end). // - If end time is in the future, tournament end time is after the filter. continue } if ln := len(list); ln >= limit { newCursor = &TournamentListCursor{ Id: list[ln-1].Id, } break } list = append(list, leaderboard) } l.RUnlock() return list, newCursor, nil } func (l *LocalLeaderboardCache) Delete(ctx context.Context, id string) error { l.Lock() _, leaderboardFound := l.leaderboards[id] leaderboard, leaderboardFound := l.leaderboards[id] l.Unlock() if !leaderboardFound { Loading @@ -526,42 +630,60 @@ func (l *LocalLeaderboardCache) Delete(ctx context.Context, id string) error { l.Lock() // Then delete from cache. delete(l.leaderboards, id) if leaderboard.IsTournament() { for i, currentLeaderboard := range l.tournamentList { if currentLeaderboard.Id == id { l.tournamentList = append(l.tournamentList[:i], l.tournamentList[i+1:]...) break } } } l.Unlock() return nil } func (l *LocalLeaderboardCache) Remove(id string) { l.Lock() if leaderboard, ok := l.leaderboards[id]; ok { delete(l.leaderboards, id) if leaderboard.IsTournament() { for i, currentLeaderboard := range l.tournamentList { if currentLeaderboard.Id == id { l.tournamentList = append(l.tournamentList[:i], l.tournamentList[i+1:]...) break } } } } l.Unlock() } func checkTournamentConfig(resetSchedule string, startTime, endTime, duration, maxSize, maxNumScore int) error { func checkTournamentConfig(resetSchedule string, startTime, endTime, duration, maxSize, maxNumScore int) (*cronexpr.Expression, error) { if startTime < 0 { return fmt.Errorf("tournament start time must be a unix UTC time in the future") return nil, fmt.Errorf("tournament start time must be a unix UTC time in the future") } if duration <= 0 { return fmt.Errorf("tournament duration must be greater than zero") return nil, fmt.Errorf("tournament duration must be greater than zero") } if maxSize < 0 { return fmt.Errorf("tournament max must be greater than zero") return nil, fmt.Errorf("tournament max must be greater than zero") } if maxNumScore < 0 { return fmt.Errorf("tournament m num score must be greater than zero") return nil, fmt.Errorf("tournament m num score must be greater than zero") } if (endTime > 0) && (endTime < startTime) { return fmt.Errorf("tournament end time cannot be before start time") return nil, fmt.Errorf("tournament end time cannot be before start time") } var cron *cronexpr.Expression if resetSchedule != "" { expr, err := cronexpr.Parse(resetSchedule) if err != nil { return fmt.Errorf("could not parse reset schedule: %s", err.Error()) return nil, fmt.Errorf("could not parse reset schedule: %s", err.Error()) } cron = expr } Loading @@ -572,9 +694,9 @@ func checkTournamentConfig(resetSchedule string, startTime, endTime, duration, m // Check that the end time (if specified) is at least strictly after the first active period start time. if (endTime > 0) && (int64(endTime) <= firstResetUnix) { return fmt.Errorf("tournament end time cannot be before first reset schedule - either increase end time or change/disable reset schedule") return nil, fmt.Errorf("tournament end time cannot be before first reset schedule - either increase end time or change/disable reset schedule") } } return nil return cron, nil }
server/runtime_go_nakama.go +3 −3 Original line number Diff line number Diff line Loading @@ -1473,19 +1473,19 @@ func (n *RuntimeGoNakamaModule) TournamentList(ctx context.Context, categoryStar return nil, errors.New("limit must be 1-100") } var cursorPtr *tournamentListCursor var cursorPtr *TournamentListCursor if cursor != "" { cb, err := base64.StdEncoding.DecodeString(cursor) if err != nil { return nil, errors.New("expects cursor to be valid when provided") } cursorPtr = &tournamentListCursor{} cursorPtr = &TournamentListCursor{} if err := gob.NewDecoder(bytes.NewReader(cb)).Decode(cursorPtr); err != nil { return nil, errors.New("expects cursor to be valid when provided") } } return TournamentList(ctx, n.logger, n.db, categoryStart, categoryEnd, startTime, endTime, limit, cursorPtr) return TournamentList(ctx, n.logger, n.db, n.leaderboardCache, categoryStart, categoryEnd, startTime, endTime, limit, cursorPtr) } func (n *RuntimeGoNakamaModule) TournamentRecordWrite(ctx context.Context, id, ownerID, username string, score, subscore int64, metadata map[string]interface{}) (*api.LeaderboardRecord, error) { Loading