Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add leaderboard and tournament create param to enable or disable ranks #1248

Merged
merged 12 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ 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 get and delete notifications by id.
- Add runtime function to disable ranks for an active leaderboard.

### Changed
- Add leaderboard and tournament create param to enable or disable ranks.

### Fixed
- Correctly wire Go runtime shutdown function context.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ require (
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0
github.com/heroiclabs/nakama-common v1.32.1-0.20240717194118-2d134b52a98b
github.com/heroiclabs/nakama-common v1.32.1-0.20240723124100-b530f4e89fd6
github.com/heroiclabs/sql-migrate v0.0.0-20240528102547-233afc8cf05a
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438
github.com/jackc/pgx/v5 v5.6.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZH
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/heroiclabs/nakama-common v1.32.1-0.20240717194118-2d134b52a98b h1:JCNsaUTDoIgOY7MYsCnRNFF+PYrSMuguu1H2oEghcLw=
github.com/heroiclabs/nakama-common v1.32.1-0.20240717194118-2d134b52a98b/go.mod h1:lPG64MVCs0/tEkh311Cd6oHX9NLx2vAPx7WW7QCJHQ0=
github.com/heroiclabs/nakama-common v1.32.1-0.20240723124100-b530f4e89fd6 h1:K543q0jEBcAvfiA0wJ/mEGD+jaGCNNARu/V0aG9kKKY=
github.com/heroiclabs/nakama-common v1.32.1-0.20240723124100-b530f4e89fd6/go.mod h1:lPG64MVCs0/tEkh311Cd6oHX9NLx2vAPx7WW7QCJHQ0=
github.com/heroiclabs/sql-migrate v0.0.0-20240528102547-233afc8cf05a h1:tuL2ZPaeCbNw8rXmV9ywd00nXRv95V4/FmbIGKLQJAE=
github.com/heroiclabs/sql-migrate v0.0.0-20240528102547-233afc8cf05a/go.mod h1:hzCTPoEi/oml2BllVydJcNP63S7b56e5DzeQeLGvw1U=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
Expand Down
21 changes: 21 additions & 0 deletions migrate/sql/20240715155039-leaderboard-rank-enable.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2024 The Nakama Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

-- +migrate Up
ALTER TABLE leaderboard ADD COLUMN IF NOT EXISTS enable_ranks boolean DEFAULT true;

-- +migrate Down
ALTER TABLE leaderboard DROP COLUMN IF EXISTS enable_ranks;
10 changes: 8 additions & 2 deletions server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"net"
"net/http"
"strings"
"sync"
"time"

"github.com/gofrs/uuid/v5"
Expand All @@ -52,6 +53,8 @@ import (
"google.golang.org/protobuf/types/known/emptypb"
)

var once sync.Once

// Used as part of JSON input validation.
const byteBracket byte = '{'

Expand Down Expand Up @@ -114,8 +117,11 @@ func StartApiServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.DB, p
grpcServer := grpc.NewServer(serverOpts...)

// Set grpc logger
grpcLogger := NewGrpcCustomLogger(logger)
grpclog.SetLoggerV2(grpcLogger)
grpcLogger, err := NewGrpcCustomLogger(logger)
if err != nil {
startupLogger.Fatal("failed to set up grpc logger", zap.Error(err))
}
once.Do(func() { grpclog.SetLoggerV2(grpcLogger) })

s := &ApiServer{
logger: logger,
Expand Down
53 changes: 48 additions & 5 deletions server/api_leaderboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ func TestApiLeaderboard(t *testing.T) {
local nk = require("nakama")
local reset = ""
local metadata = {}
nk.leaderboard_create(%q, %v, %q, %q, reset, metadata)
`, lb.Id, lb.Authoritative, lb.GetSortOrder(), lb.GetOperator()),
nk.leaderboard_create(%q, %t, %q, %q, reset, metadata, %t)
`, lb.Id, lb.Authoritative, lb.GetSortOrder(), lb.GetOperator(), lb.EnableRanks),
}

runtime, _, rtData, err := runtimeWithModulesWithData(t, modules)
Expand Down Expand Up @@ -233,9 +233,10 @@ nk.leaderboard_create(%q, %v, %q, %q, reset, metadata)
lbId := newId().String()
db := NewDB(t)
conn, cl, srv, ctx := newAPI(&Leaderboard{
Id: lbId,
SortOrder: LeaderboardSortOrderDescending,
Operator: LeaderboardOperatorSet,
Id: lbId,
SortOrder: LeaderboardSortOrderDescending,
Operator: LeaderboardOperatorSet,
EnableRanks: true,
})

users := newUsers()
Expand Down Expand Up @@ -301,4 +302,46 @@ nk.leaderboard_create(%q, %v, %q, %q, reset, metadata)
require.Equal(t, users[0].id.String(), resp.Records[2].OwnerId)
require.Equal(t, int64(5), resp.Records[2].Rank)
})

t.Run("disable ranks", func(t *testing.T) {
lbId := newId().String()
db := NewDB(t)
conn, cl, srv, ctx := newAPI(&Leaderboard{
Id: lbId,
SortOrder: LeaderboardSortOrderDescending,
Operator: LeaderboardOperatorSet,
EnableRanks: true,
})

users := newUsers()
defer cleanup(db, srv, conn, users)

populateLb(users, lbId)

verifyList(ctx, cl, lbId, []*testUser{
users[4], users[3], users[2], users[1], users[0],
})

if err := disableLeaderboardRanks(ctx, logger, db, srv.leaderboardCache, srv.leaderboardRankCache, lbId); err != nil {
t.Fatal("should disable leaderboard ranks")
}

// Fetch from the middle
resp, err := cl.ListLeaderboardRecordsAroundOwner(ctx, &api.ListLeaderboardRecordsAroundOwnerRequest{
LeaderboardId: lbId,
Limit: wrapperspb.UInt32(3),
OwnerId: users[2].id.String(),
})
require.NoError(t, err, "should list user leaderboard records around owner")

require.Len(t, resp.Records, 3)
require.Equal(t, users[3].id.String(), resp.Records[0].OwnerId)
require.Equal(t, int64(0), resp.Records[0].Rank)

require.Equal(t, users[2].id.String(), resp.Records[1].OwnerId)
require.Equal(t, int64(0), resp.Records[1].Rank)

require.Equal(t, users[1].id.String(), resp.Records[2].OwnerId)
require.Equal(t, int64(0), resp.Records[2].Rank)
})
}
6 changes: 6 additions & 0 deletions server/core_friend.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ ORDER BY destination_id`
_ = friendsRows.Close()

if len(friends) == 0 {
// return early if user has no friends
return &api.FriendsOfFriendsList{FriendsOfFriends: []*api.FriendsOfFriendsList_FriendOfFriend{}}, nil
}

Expand Down Expand Up @@ -334,6 +335,11 @@ AND state = 0
rows.Close()
}

if len(userIds) == 0 {
// return early if friends have no other friends
return &api.FriendsOfFriendsList{FriendsOfFriends: []*api.FriendsOfFriendsList_FriendOfFriend{}}, nil
}

users, err := GetUsers(ctx, logger, db, statusRegistry, userIds, nil, nil)
if err != nil {
return nil, err
Expand Down
35 changes: 32 additions & 3 deletions server/core_leaderboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"encoding/base64"
"encoding/gob"
"errors"
"github.com/heroiclabs/nakama-common/runtime"
"sort"
"strings"
"time"
Expand Down Expand Up @@ -367,7 +368,7 @@ WHERE leaderboard_id = $1 AND expiry_time = $2 AND owner_id = ANY($3)`
sort.Slice(ownerRecords, sortFn)

// Bulk fill in the ranks of any owner records requested.
rankCount := rankCache.Fill(leaderboardId, expiryTime, ownerRecords)
rankCount := rankCache.Fill(leaderboardId, expiryTime, ownerRecords, leaderboard.EnableRanks)

return &api.LeaderboardRecordList{
Records: records,
Expand Down Expand Up @@ -552,7 +553,7 @@ func LeaderboardRecordWrite(ctx context.Context, logger *zap.Logger, db *sql.DB,
rank = rankCache.Get(leaderboardId, expiryTime, uuid.Must(uuid.FromString(ownerID)))
} else {
// Ensure we have the latest dbscore, dbsubscore if there was an update.
rank = rankCache.Insert(leaderboardId, leaderboard.SortOrder, dbScore, dbSubscore, dbNumScore, expiryTime, uuid.Must(uuid.FromString(ownerID)))
rank = rankCache.Insert(leaderboardId, leaderboard.SortOrder, dbScore, dbSubscore, dbNumScore, expiryTime, uuid.Must(uuid.FromString(ownerID)), leaderboard.EnableRanks)
}

record := &api.LeaderboardRecord{
Expand Down Expand Up @@ -856,7 +857,12 @@ func getLeaderboardRecordsHaystack(ctx context.Context, logger *zap.Logger, db *
}

records = records[start:end]
rankCount := rankCache.Fill(leaderboardId, expiryTime.Unix(), records)
l := leaderboardCache.Get(leaderboardId)
if l == nil {
// Should never happen unless leaderboard is concurrently deleted as records are being requested.
return nil, ErrLeaderboardNotFound
}
rankCount := rankCache.Fill(leaderboardId, expiryTime.Unix(), records, l.EnableRanks)

var prevCursorStr string
if setPrevCursor {
Expand Down Expand Up @@ -968,3 +974,26 @@ func calculateExpiryOverride(overrideExpiry int64, leaderboard *Leaderboard) (in
}
return overrideExpiry, true
}

func disableLeaderboardRanks(ctx context.Context, logger *zap.Logger, db *sql.DB, leaderboardCache LeaderboardCache, rankCache LeaderboardRankCache, id string) error {
l := leaderboardCache.Get(id)
if l == nil || l.IsTournament() {
return runtime.ErrLeaderboardNotFound
}

if _, err := db.QueryContext(ctx, "UPDATE leaderboard SET enable_ranks = false WHERE id = $1", id); err != nil {
logger.Error("failed to set leaderboard enable_ranks value", zap.Error(err))
return errors.New("failed to disable tournament ranks")
}

leaderboardCache.Insert(l.Id, l.Authoritative, l.SortOrder, l.Operator, l.ResetScheduleStr, l.Metadata, l.CreateTime, false)

expiryTime := int64(0)
if l.ResetSchedule != nil {
expiryTime = l.ResetSchedule.Next(time.Now().UTC()).UTC().Unix()
}

rankCache.DeleteLeaderboard(l.Id, expiryTime)

return nil
}
27 changes: 23 additions & 4 deletions server/core_tournament.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ type TournamentListCursor struct {
}

func TournamentCreate(ctx context.Context, logger *zap.Logger, cache LeaderboardCache, scheduler LeaderboardScheduler, leaderboardId string, authoritative bool, sortOrder, operator int, resetSchedule, metadata,
title, description string, category, startTime, endTime, duration, maxSize, maxNumScore int, joinRequired bool) error {
title, description string, category, startTime, endTime, duration, maxSize, maxNumScore int, joinRequired, enableRanks bool) error {

_, created, err := cache.CreateTournament(ctx, leaderboardId, authoritative, sortOrder, operator, resetSchedule, metadata, title, description, category, startTime, endTime, duration, maxSize, maxNumScore, joinRequired)
_, created, err := cache.CreateTournament(ctx, leaderboardId, authoritative, sortOrder, operator, resetSchedule, metadata, title, description, category, startTime, endTime, duration, maxSize, maxNumScore, joinRequired, enableRanks)
if err != nil {
return err
}
Expand Down Expand Up @@ -179,7 +179,7 @@ ON CONFLICT(owner_id, leaderboard_id, expiry_time) DO NOTHING`

// Ensure new tournament joiner is included in the rank cache.
if isNewJoin {
_ = rankCache.Insert(leaderboard.Id, leaderboard.SortOrder, 0, 0, 0, expiryTime, ownerID)
_ = rankCache.Insert(leaderboard.Id, leaderboard.SortOrder, 0, 0, 0, expiryTime, ownerID, leaderboard.EnableRanks)
}

logger.Info("Joined tournament.", zap.String("tournament_id", tournamentId), zap.String("owner", ownerID.String()), zap.String("username", username))
Expand Down Expand Up @@ -628,7 +628,7 @@ func TournamentRecordWrite(ctx context.Context, logger *zap.Logger, db *sql.DB,
}

// Enrich the return record with rank data.
record.Rank = rankCache.Insert(leaderboard.Id, leaderboard.SortOrder, record.Score, record.Subscore, dbNumScore, expiryUnix, ownerId)
record.Rank = rankCache.Insert(leaderboard.Id, leaderboard.SortOrder, record.Score, record.Subscore, dbNumScore, expiryUnix, ownerId, leaderboard.EnableRanks)

return record, nil
}
Expand Down Expand Up @@ -829,3 +829,22 @@ func parseTournament(scannable Scannable, now time.Time) (*api.Tournament, error

return tournament, nil
}

func DisableTournamentRanks(ctx context.Context, logger *zap.Logger, db *sql.DB, leaderboardCache LeaderboardCache, rankCache LeaderboardRankCache, id string) error {
l := leaderboardCache.Get(id)
if l == nil || !l.IsTournament() {
return runtime.ErrTournamentNotFound
}

if _, err := db.QueryContext(ctx, "UPDATE leaderboard SET enable_ranks = false WHERE id = $1", id); err != nil {
logger.Error("failed to set leaderboard enable_ranks value", zap.Error(err))
return errors.New("failed to disable leaderboard ranks")
}

leaderboardCache.Insert(l.Id, l.Authoritative, l.SortOrder, l.Operator, l.ResetScheduleStr, l.Metadata, l.CreateTime, false)

_, _, expiryUnix := calculateTournamentDeadlines(l.StartTime, l.EndTime, int64(l.Duration), l.ResetSchedule, time.Now())
rankCache.DeleteLeaderboard(l.Id, expiryUnix)

return nil
}
Loading
Loading