Hello!,
I have some custom behaviour for leaderboard with a reset schedule. They reset weekly and I have some custom behaviour to reward the top users. I also need to notify every other user about the week being reset.
I was wondering on using the NakamaModule’s NotificationSendAll , but I don’t know details of its implementation, the game is likely to have about a million users registered on the database. Not being a server programmer myself, I was wondering if this acceptable or if that would be a problem on production with that amount of user.
The last resource would be handling it locally using the leaderboard reset time. But this is not the ideal solution since I’ll have to handle quite a few edge cases that wouldn’t be a problem if the server fires a message when the week ends.
Another related issue it’s deleting existing notifications with that ID every week (to avoid users stacking the same reset notification). I implemented a reset function but I also wonder if it would be too much stress on the server.
- Versions: Nakama 3.30
- Server Framework Runtime language Go
Reseting and broadcasting the notification:
content := map[string]interface{}{"leaderboardId": targetID}
s.ClearUserNotificationsByCode(ctx, db, logger, CodeLeaderboardReset)
nk.NotificationSendAll(ctx, "Weekly Reset", content, CodeLeaderboardReset, true)
Implemented this function to clear the notifications
func (s *CustomLeaderboard) ClearUserNotificationsByCode(ctx context.Context, db *sql.DB, logger runtime.Logger, code int) error {
query := `
DELETE FROM notification
WHERE id IN (
SELECT id FROM notification
WHERE code = $1
LIMIT 5000
)
`
for {
result, err := db.ExecContext(ctx, query, code)
if err != nil {
logger.Error("Failed to batch delete notifications: %v", err)
return err
}
rows, _ := result.RowsAffected()
if rows == 0 {
break
}
}
return nil
}
And this is the alternative broadcast function with Batching I thought on using instead of nk.NotificationSendAll, doing batches of 5000 users
// Broacasting
func (s *CustomLeaderboard) BroadcastResetNotification(ctx context.Context, db *sql.DB, logger runtime.Logger, sourceID string) error {
// 1. Clear old reset notifications
if err := s.ClearUserNotificationsByCode(ctx, db, logger, CodeLeaderboardReset); err != nil {
return err // Stop if we can't clear old data to avoid massive stacking
}
content, err := json.Marshal(map[string]interface{}{"id": sourceID})
if err != nil {
return err
}
query := `
INSERT INTO notification (id, user_id, subject, content, code, sender_id, create_time, persistent)
SELECT gen_random_uuid(), u.id, 'Season Reset', $1, $2, '', (now() AT TIME ZONE 'UTC'), true
FROM users u
WHERE u.disable_time = '1970-01-01 00:00:00+00'
AND NOT EXISTS (
SELECT 1 FROM notification n
WHERE n.user_id = u.id AND n.code = $2
)
LIMIT 5000
`
totalBroadcasted := int64(0)
for {
batchCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
result, err := db.ExecContext(batchCtx, query, string(content), CodeQueensLeaderboardReset)
cancel()
if err != nil {
logger.Error("SQL Broadcast batch failed after %d users: %v", totalBroadcasted, err)
return err
}
rows, _ := result.RowsAffected()
if rows == 0 {
break
}
totalBroadcasted += rows
time.Sleep(20 * time.Millisecond)
}
logger.Info("Global broadcast complete. Sent %d notifications.", totalBroadcasted)
return nil
}