Loading CHANGELOG.md +4 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,10 @@ All notable changes to this project are documented below. The format is based on [keep a changelog](http://keepachangelog.com) and this project uses [semantic versioning](http://semver.org). ## [Unreleased] ### Added - New runtime functions to retrieve tournaments by ID. - Allow tournament duration to exceed reset window, and cap the duration if it does. ### Changed - Do not use absolute path for `tini` executable in default container entrypoint. - Faster validation of JSON object input payloads. Loading server/core_tournament.go +59 −6 Original line number Diff line number Diff line Loading @@ -21,6 +21,8 @@ import ( "encoding/base64" "encoding/gob" "errors" "fmt" "strings" "time" "github.com/gofrs/uuid" Loading Loading @@ -195,12 +197,47 @@ ON CONFLICT(owner_id, leaderboard_id, expiry_time) DO NOTHING` return nil } func TournamentsGet(ctx context.Context, logger *zap.Logger, db *sql.DB, tournamentIDs []string) ([]*api.Tournament, error) { now := time.Now().UTC() params := make([]interface{}, 0, len(tournamentIDs)) statements := make([]string, 0, len(tournamentIDs)) for i, tournamentID := range tournamentIDs { params = append(params, tournamentID) statements = append(statements, fmt.Sprintf("$%v", i+1)) } 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 id IN (` + strings.Join(statements, ",") + `) AND duration > 0` // Retrieved directly from database to have the latest configuration and 'size' etc field values. // Ensures consistency between return data from this call and TournamentList. rows, err := db.QueryContext(ctx, query, params...) if err != nil { logger.Error("Could not retrieve tournaments", zap.Error(err)) return nil, err } records := make([]*api.Tournament, 0) for rows.Next() { tournament, err := parseTournament(rows, now) if err != nil { _ = rows.Close() logger.Error("Error parsing retrieved tournament records", zap.Error(err)) return nil, err } records = append(records, tournament) } _ = rows.Close() 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) { now := time.Now().UTC() query := ` SELECT id, sort_order, reset_schedule, metadata, create_time, category, description, duration, end_time, max_size, max_num_score, title, size, start_time 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) Loading Loading @@ -497,6 +534,10 @@ func calculateTournamentDeadlines(startTime, endTime, duration int64, resetSched startActiveUnix := schedule0Unix - (schedule1Unix - schedule0Unix) endActiveUnix := startActiveUnix + duration expiryUnix := schedule0Unix if endActiveUnix > expiryUnix { // Cap the end active to the same time as the expiry. endActiveUnix = expiryUnix } if startTime > endActiveUnix { // The start time after the end of the current active period but before the next reset. Loading @@ -504,12 +545,20 @@ func calculateTournamentDeadlines(startTime, endTime, duration int64, resetSched startActiveUnix = resetSchedule.Next(time.Unix(startTime, 0).UTC()).UTC().Unix() endActiveUnix = startActiveUnix + duration expiryUnix = startActiveUnix + (schedule1Unix - schedule0Unix) if endActiveUnix > expiryUnix { // Cap the end active to the same time as the expiry. endActiveUnix = expiryUnix } } else if startTime > startActiveUnix { startActiveUnix = startTime } if endTime > 0 && expiryUnix > endTime { expiryUnix = endTime if endActiveUnix > expiryUnix { // Cap the end active to the same time as the expiry. endActiveUnix = expiryUnix } } return startActiveUnix, endActiveUnix, expiryUnix Loading @@ -519,6 +568,10 @@ func calculateTournamentDeadlines(startTime, endTime, duration int64, resetSched endActiveUnix = startTime + duration } expiryUnix := endTime if endActiveUnix > expiryUnix { // Cap the end active to the same time as the expiry. endActiveUnix = expiryUnix } return startTime, endActiveUnix, expiryUnix } Loading server/leaderboard_cache.go +3 −13 Original line number Diff line number Diff line Loading @@ -557,10 +557,6 @@ func checkTournamentConfig(resetSchedule string, startTime, endTime, duration, m return fmt.Errorf("tournament end time cannot be before start time") } if (endTime > 0) && (endTime < (startTime + duration)) { return fmt.Errorf("tournament end time cannot be before end of first session or in the past") } var cron *cronexpr.Expression if resetSchedule != "" { expr, err := cronexpr.Parse(resetSchedule) Loading @@ -573,17 +569,11 @@ func checkTournamentConfig(resetSchedule string, startTime, endTime, duration, m if cron != nil { schedules := cron.NextN(time.Unix(int64(startTime), 0).UTC(), 2) firstResetUnix := schedules[0].UTC().Unix() secondResetUnix := schedules[1].UTC().Unix() // 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") } // Check that the gap between resets is >= the duration of each tournament round. if secondResetUnix-firstResetUnix < int64(duration) { return fmt.Errorf("tournament cannot be scheduled to be reset while it is ongoing - either decrease duration or change/disable reset schedule") } } return nil Loading server/runtime_go_nakama.go +8 −0 Original line number Diff line number Diff line Loading @@ -1439,6 +1439,14 @@ func (n *RuntimeGoNakamaModule) TournamentJoin(ctx context.Context, id, ownerID, return TournamentJoin(ctx, n.logger, n.db, n.leaderboardCache, ownerID, username, id) } func (n *RuntimeGoNakamaModule) TournamentsGetId(ctx context.Context, tournamentIDs []string) ([]*api.Tournament, error) { if len(tournamentIDs) == 0 { return []*api.Tournament{}, nil } return TournamentsGet(ctx, n.logger, n.db, tournamentIDs) } func (n *RuntimeGoNakamaModule) TournamentList(ctx context.Context, categoryStart, categoryEnd, startTime, endTime, limit int, cursor string) (*api.TournamentList, error) { if categoryStart < 0 || categoryStart >= 128 { Loading server/runtime_lua_nakama.go +82 −0 Original line number Diff line number Diff line Loading @@ -4591,6 +4591,88 @@ func (n *RuntimeLuaNakamaModule) tournamentJoin(l *lua.LState) int { return 0 } func (n *RuntimeLuaNakamaModule) tournamentsGetId(l *lua.LState) int { // Input table validation. input := l.OptTable(1, nil) if input == nil { l.ArgError(1, "invalid tournament id list") return 0 } if input.Len() == 0 { l.Push(l.CreateTable(0, 0)) return 1 } tournamentIDs, ok := RuntimeLuaConvertLuaValue(input).([]interface{}) if !ok { l.ArgError(1, "invalid tournament id data") return 0 } if len(tournamentIDs) == 0 { l.Push(l.CreateTable(0, 0)) return 1 } // Input individual ID validation. tournamentIDStrings := make([]string, 0, len(tournamentIDs)) for _, id := range tournamentIDs { if ids, ok := id.(string); !ok || ids == "" { l.ArgError(1, "each tournament id must be a string") return 0 } else { tournamentIDStrings = append(tournamentIDStrings, ids) } } // Get the tournaments. list, err := TournamentsGet(l.Context(), n.logger, n.db, tournamentIDStrings) if err != nil { l.RaiseError(fmt.Sprintf("failed to get tournaments: %s", err.Error())) return 0 } tournaments := l.CreateTable(len(list), 0) for i, t := range list { tt := l.CreateTable(0, 16) tt.RawSetString("id", lua.LString(t.Id)) tt.RawSetString("title", lua.LString(t.Title)) tt.RawSetString("description", lua.LString(t.Description)) tt.RawSetString("category", lua.LNumber(t.Category)) if t.SortOrder == LeaderboardSortOrderAscending { tt.RawSetString("sort_order", lua.LString("asc")) } else { tt.RawSetString("sort_order", lua.LString("desc")) } tt.RawSetString("size", lua.LNumber(t.Size)) tt.RawSetString("max_size", lua.LNumber(t.MaxSize)) tt.RawSetString("max_num_score", lua.LNumber(t.MaxNumScore)) tt.RawSetString("duration", lua.LNumber(t.Duration)) tt.RawSetString("end_active", lua.LNumber(t.EndActive)) tt.RawSetString("can_enter", lua.LBool(t.CanEnter)) tt.RawSetString("next_reset", lua.LNumber(t.NextReset)) metadataMap := make(map[string]interface{}) err = json.Unmarshal([]byte(t.Metadata), &metadataMap) if err != nil { l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error())) return 0 } metadataTable := RuntimeLuaConvertMap(l, metadataMap) tt.RawSetString("metadata", metadataTable) tt.RawSetString("create_time", lua.LNumber(t.CreateTime.Seconds)) tt.RawSetString("start_time", lua.LNumber(t.StartTime.Seconds)) if t.EndTime == nil { tt.RawSetString("end_time", lua.LNil) } else { tt.RawSetString("end_time", lua.LNumber(t.EndTime.Seconds)) } tournaments.RawSetInt(i+1, tt) } l.Push(tournaments) return 1 } func (n *RuntimeLuaNakamaModule) tournamentList(l *lua.LState) int { categoryStart := l.OptInt(1, 0) if categoryStart < 0 || categoryStart >= 128 { Loading server/core_leaderboard.go +1 −1 File changed.Contains only whitespace changes. Show changes Loading
CHANGELOG.md +4 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,10 @@ All notable changes to this project are documented below. The format is based on [keep a changelog](http://keepachangelog.com) and this project uses [semantic versioning](http://semver.org). ## [Unreleased] ### Added - New runtime functions to retrieve tournaments by ID. - Allow tournament duration to exceed reset window, and cap the duration if it does. ### Changed - Do not use absolute path for `tini` executable in default container entrypoint. - Faster validation of JSON object input payloads. Loading
server/core_tournament.go +59 −6 Original line number Diff line number Diff line Loading @@ -21,6 +21,8 @@ import ( "encoding/base64" "encoding/gob" "errors" "fmt" "strings" "time" "github.com/gofrs/uuid" Loading Loading @@ -195,12 +197,47 @@ ON CONFLICT(owner_id, leaderboard_id, expiry_time) DO NOTHING` return nil } func TournamentsGet(ctx context.Context, logger *zap.Logger, db *sql.DB, tournamentIDs []string) ([]*api.Tournament, error) { now := time.Now().UTC() params := make([]interface{}, 0, len(tournamentIDs)) statements := make([]string, 0, len(tournamentIDs)) for i, tournamentID := range tournamentIDs { params = append(params, tournamentID) statements = append(statements, fmt.Sprintf("$%v", i+1)) } 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 id IN (` + strings.Join(statements, ",") + `) AND duration > 0` // Retrieved directly from database to have the latest configuration and 'size' etc field values. // Ensures consistency between return data from this call and TournamentList. rows, err := db.QueryContext(ctx, query, params...) if err != nil { logger.Error("Could not retrieve tournaments", zap.Error(err)) return nil, err } records := make([]*api.Tournament, 0) for rows.Next() { tournament, err := parseTournament(rows, now) if err != nil { _ = rows.Close() logger.Error("Error parsing retrieved tournament records", zap.Error(err)) return nil, err } records = append(records, tournament) } _ = rows.Close() 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) { now := time.Now().UTC() query := ` SELECT id, sort_order, reset_schedule, metadata, create_time, category, description, duration, end_time, max_size, max_num_score, title, size, start_time 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) Loading Loading @@ -497,6 +534,10 @@ func calculateTournamentDeadlines(startTime, endTime, duration int64, resetSched startActiveUnix := schedule0Unix - (schedule1Unix - schedule0Unix) endActiveUnix := startActiveUnix + duration expiryUnix := schedule0Unix if endActiveUnix > expiryUnix { // Cap the end active to the same time as the expiry. endActiveUnix = expiryUnix } if startTime > endActiveUnix { // The start time after the end of the current active period but before the next reset. Loading @@ -504,12 +545,20 @@ func calculateTournamentDeadlines(startTime, endTime, duration int64, resetSched startActiveUnix = resetSchedule.Next(time.Unix(startTime, 0).UTC()).UTC().Unix() endActiveUnix = startActiveUnix + duration expiryUnix = startActiveUnix + (schedule1Unix - schedule0Unix) if endActiveUnix > expiryUnix { // Cap the end active to the same time as the expiry. endActiveUnix = expiryUnix } } else if startTime > startActiveUnix { startActiveUnix = startTime } if endTime > 0 && expiryUnix > endTime { expiryUnix = endTime if endActiveUnix > expiryUnix { // Cap the end active to the same time as the expiry. endActiveUnix = expiryUnix } } return startActiveUnix, endActiveUnix, expiryUnix Loading @@ -519,6 +568,10 @@ func calculateTournamentDeadlines(startTime, endTime, duration int64, resetSched endActiveUnix = startTime + duration } expiryUnix := endTime if endActiveUnix > expiryUnix { // Cap the end active to the same time as the expiry. endActiveUnix = expiryUnix } return startTime, endActiveUnix, expiryUnix } Loading
server/leaderboard_cache.go +3 −13 Original line number Diff line number Diff line Loading @@ -557,10 +557,6 @@ func checkTournamentConfig(resetSchedule string, startTime, endTime, duration, m return fmt.Errorf("tournament end time cannot be before start time") } if (endTime > 0) && (endTime < (startTime + duration)) { return fmt.Errorf("tournament end time cannot be before end of first session or in the past") } var cron *cronexpr.Expression if resetSchedule != "" { expr, err := cronexpr.Parse(resetSchedule) Loading @@ -573,17 +569,11 @@ func checkTournamentConfig(resetSchedule string, startTime, endTime, duration, m if cron != nil { schedules := cron.NextN(time.Unix(int64(startTime), 0).UTC(), 2) firstResetUnix := schedules[0].UTC().Unix() secondResetUnix := schedules[1].UTC().Unix() // 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") } // Check that the gap between resets is >= the duration of each tournament round. if secondResetUnix-firstResetUnix < int64(duration) { return fmt.Errorf("tournament cannot be scheduled to be reset while it is ongoing - either decrease duration or change/disable reset schedule") } } return nil Loading
server/runtime_go_nakama.go +8 −0 Original line number Diff line number Diff line Loading @@ -1439,6 +1439,14 @@ func (n *RuntimeGoNakamaModule) TournamentJoin(ctx context.Context, id, ownerID, return TournamentJoin(ctx, n.logger, n.db, n.leaderboardCache, ownerID, username, id) } func (n *RuntimeGoNakamaModule) TournamentsGetId(ctx context.Context, tournamentIDs []string) ([]*api.Tournament, error) { if len(tournamentIDs) == 0 { return []*api.Tournament{}, nil } return TournamentsGet(ctx, n.logger, n.db, tournamentIDs) } func (n *RuntimeGoNakamaModule) TournamentList(ctx context.Context, categoryStart, categoryEnd, startTime, endTime, limit int, cursor string) (*api.TournamentList, error) { if categoryStart < 0 || categoryStart >= 128 { Loading
server/runtime_lua_nakama.go +82 −0 Original line number Diff line number Diff line Loading @@ -4591,6 +4591,88 @@ func (n *RuntimeLuaNakamaModule) tournamentJoin(l *lua.LState) int { return 0 } func (n *RuntimeLuaNakamaModule) tournamentsGetId(l *lua.LState) int { // Input table validation. input := l.OptTable(1, nil) if input == nil { l.ArgError(1, "invalid tournament id list") return 0 } if input.Len() == 0 { l.Push(l.CreateTable(0, 0)) return 1 } tournamentIDs, ok := RuntimeLuaConvertLuaValue(input).([]interface{}) if !ok { l.ArgError(1, "invalid tournament id data") return 0 } if len(tournamentIDs) == 0 { l.Push(l.CreateTable(0, 0)) return 1 } // Input individual ID validation. tournamentIDStrings := make([]string, 0, len(tournamentIDs)) for _, id := range tournamentIDs { if ids, ok := id.(string); !ok || ids == "" { l.ArgError(1, "each tournament id must be a string") return 0 } else { tournamentIDStrings = append(tournamentIDStrings, ids) } } // Get the tournaments. list, err := TournamentsGet(l.Context(), n.logger, n.db, tournamentIDStrings) if err != nil { l.RaiseError(fmt.Sprintf("failed to get tournaments: %s", err.Error())) return 0 } tournaments := l.CreateTable(len(list), 0) for i, t := range list { tt := l.CreateTable(0, 16) tt.RawSetString("id", lua.LString(t.Id)) tt.RawSetString("title", lua.LString(t.Title)) tt.RawSetString("description", lua.LString(t.Description)) tt.RawSetString("category", lua.LNumber(t.Category)) if t.SortOrder == LeaderboardSortOrderAscending { tt.RawSetString("sort_order", lua.LString("asc")) } else { tt.RawSetString("sort_order", lua.LString("desc")) } tt.RawSetString("size", lua.LNumber(t.Size)) tt.RawSetString("max_size", lua.LNumber(t.MaxSize)) tt.RawSetString("max_num_score", lua.LNumber(t.MaxNumScore)) tt.RawSetString("duration", lua.LNumber(t.Duration)) tt.RawSetString("end_active", lua.LNumber(t.EndActive)) tt.RawSetString("can_enter", lua.LBool(t.CanEnter)) tt.RawSetString("next_reset", lua.LNumber(t.NextReset)) metadataMap := make(map[string]interface{}) err = json.Unmarshal([]byte(t.Metadata), &metadataMap) if err != nil { l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error())) return 0 } metadataTable := RuntimeLuaConvertMap(l, metadataMap) tt.RawSetString("metadata", metadataTable) tt.RawSetString("create_time", lua.LNumber(t.CreateTime.Seconds)) tt.RawSetString("start_time", lua.LNumber(t.StartTime.Seconds)) if t.EndTime == nil { tt.RawSetString("end_time", lua.LNil) } else { tt.RawSetString("end_time", lua.LNumber(t.EndTime.Seconds)) } tournaments.RawSetInt(i+1, tt) } l.Push(tournaments) return 1 } func (n *RuntimeLuaNakamaModule) tournamentList(l *lua.LState) int { categoryStart := l.OptInt(1, 0) if categoryStart < 0 || categoryStart >= 128 { Loading