Player cache and project structure refactor.

This commit is contained in:
MaxJa4
2024-01-21 18:02:27 +01:00
parent 4aae0896aa
commit f2573f2273
10 changed files with 73 additions and 60 deletions

View File

@@ -2,13 +2,9 @@ FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum internal/**/go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-w -s" -o isc .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-w -s" -o isc ./cmd
FROM alpine:latest

View File

@@ -79,7 +79,7 @@ func main() {
admin.Use(AdminAuthRequired())
models.ConnectDatabase()
models.ConnectCache()
models.ConnectCache(utils.PlayerCacheLifetime)
var code models.ActivationCode
if err := models.DB.First(&code).Error; err != nil {

View File

@@ -5,7 +5,6 @@ import (
"InfantrySkillCalculator/utils"
"fmt"
"github.com/gin-gonic/gin"
"internal/cache"
"log"
"net/http"
)
@@ -30,7 +29,7 @@ func GetCacheByPlayerID(c *gin.Context) {
return
}
val, err := cache.GetScore(playerId, game.Tag)
val, err := models.PlayerCache.GetScore(playerId, game.Tag)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
} else {
@@ -52,7 +51,7 @@ func AddCache(c *gin.Context) {
return
}
err := cache.SetScore(input.PlayerID, input.GameTag, input.Score)
err := models.PlayerCache.SetScore(input.PlayerID, input.GameTag, input.Score)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cache update failed! Error: " + err.Error()})
return
@@ -67,7 +66,7 @@ func UpdateCacheByPlayerID(c *gin.Context) {
gameTag := c.Request.URL.Query().Get("game")
score := utils.StringToFloat(c.PostForm("score"))
err := cache.SetScore(playerID, gameTag, score)
err := models.PlayerCache.SetScore(playerID, gameTag, score)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cache update failed! Error: " + err.Error()})
} else {
@@ -80,7 +79,7 @@ func DeleteCacheByPlayerID(c *gin.Context) {
playerID := utils.StringToUint(c.Param("id"))
gameTag := c.Request.URL.Query().Get("game")
if err := cache.DeleteScore(playerID, gameTag); err != nil {
if err := models.PlayerCache.DeleteScore(playerID, gameTag); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
} else {
c.JSON(http.StatusOK, nil)
@@ -89,7 +88,7 @@ func DeleteCacheByPlayerID(c *gin.Context) {
// DeleteAllCaches DELETE /cache
func DeleteAllCaches(c *gin.Context) {
if err := cache.PurgeCache(); err != nil {
if err := models.PlayerCache.PurgeCache(); err != nil {
c.String(http.StatusBadRequest, err.Error())
} else {
c.String(http.StatusOK, "Purged all caches!")
@@ -116,11 +115,11 @@ func GetScoreByPlayerID(c *gin.Context) {
return
}
score, err := cache.GetScore(player.ID, game.Tag)
score, err := models.PlayerCache.GetScore(player.ID, game.Tag)
if err != nil || score == -1 {
score = utils.CalcPlayerScore(player.Name, game.Tag)
if score == score && score != -1 { // not NaN
if err := cache.SetScore(player.ID, game.Tag, score); err != nil {
if err := models.PlayerCache.SetScore(player.ID, game.Tag, score); err != nil {
log.Fatal(err)
}
}

View File

@@ -8,7 +8,6 @@ import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
"html/template"
"internal/cache"
"log"
"net/http"
)
@@ -51,7 +50,12 @@ func GetPlayersByClanHTML(c *gin.Context) {
return
}
scores, err := cache.GetScores(players, game.Tag)
var playerIDs []uint
for _, player := range players {
playerIDs = append(playerIDs, player.ID)
}
scores, err := models.PlayerCache.GetScores(playerIDs, game.Tag)
if err != nil {
c.String(http.StatusBadRequest, "")
log.Fatal(err)

View File

@@ -7,7 +7,7 @@ services:
- "127.0.0.1:8000:8000"
environment:
- GO_ENV=production
- REDIS_ADDRESS=redis
- REDIS_ADDRESS=redis:6379
depends_on:
- redis
volumes:

View File

@@ -1,18 +1,40 @@
package cache
import (
"InfantrySkillCalculator/models"
"InfantrySkillCalculator/utils"
"context"
"errors"
"fmt"
"github.com/redis/go-redis/v9"
"strconv"
"time"
)
var ctx = context.Background()
func GetValue(key string) (string, error) {
val, err := models.Cache.Get(ctx, key).Result()
type PlayerCache struct {
cache *redis.Client
lifetime time.Duration
}
func NewPlayerCache(address string, lifetime time.Duration) *PlayerCache {
return &PlayerCache{
redis.NewClient(
&redis.Options{
Addr: address,
Password: "",
DB: 0,
}),
lifetime,
}
}
func (pc *PlayerCache) Connect() error {
_, err := pc.cache.Ping(ctx).Result()
return err
}
func (pc *PlayerCache) GetValue(key string) (string, error) {
val, err := pc.cache.Get(ctx, key).Result()
if err != nil {
return "", err // cache miss or error
} else {
@@ -20,31 +42,32 @@ func GetValue(key string) (string, error) {
}
}
func SetValue(key string, value interface{}) error {
return models.Cache.Set(ctx, key, value, utils.PlayerCacheLifetime).Err()
func (pc *PlayerCache) SetValue(key string, value interface{}) error {
return pc.cache.Set(ctx, key, value, pc.lifetime).Err()
}
func SetScore(playerId uint, gameTag string, score float32) error {
key := GetPlayerCacheKey(playerId, gameTag)
return SetValue(key, score)
func (pc *PlayerCache) SetScore(playerId uint, gameTag string, score float32) error {
key := getPlayerCacheKey(playerId, gameTag)
return pc.SetValue(key, score)
}
func GetScore(playerId uint, gameTag string) (float32, error) {
key := GetPlayerCacheKey(playerId, gameTag)
val, err := GetValue(key)
func (pc *PlayerCache) GetScore(playerId uint, gameTag string) (float32, error) {
key := getPlayerCacheKey(playerId, gameTag)
val, err := pc.GetValue(key)
if errors.Is(err, redis.Nil) {
return -1.0, nil // cache miss
} else if err != nil {
return -1.0, err // cache error
} else {
return utils.StringToFloat(val), nil // cache hit
valFloat, _ := strconv.ParseFloat(val, 32)
return float32(valFloat), nil // cache hit
}
}
func GetScores(players []models.Player, gameTag string) ([]float32, error) {
vals, err := models.Cache.Pipelined(ctx, func(pipe redis.Pipeliner) error {
for _, p := range players {
key := GetPlayerCacheKey(p.ID, gameTag)
func (pc *PlayerCache) GetScores(playerIds []uint, gameTag string) ([]float32, error) {
vals, err := pc.cache.Pipelined(ctx, func(pipe redis.Pipeliner) error {
for _, id := range playerIds {
key := getPlayerCacheKey(id, gameTag)
pipe.Get(ctx, key)
}
return nil
@@ -69,15 +92,15 @@ func GetScores(players []models.Player, gameTag string) ([]float32, error) {
return scores, nil
}
func DeleteScore(playerId uint, gameTag string) error {
key := GetPlayerCacheKey(playerId, gameTag)
return models.Cache.Del(ctx, key).Err()
func (pc *PlayerCache) DeleteScore(playerId uint, gameTag string) error {
key := getPlayerCacheKey(playerId, gameTag)
return pc.cache.Del(ctx, key).Err()
}
func PurgeCache() error {
return models.Cache.FlushAll(ctx).Err()
func (pc *PlayerCache) PurgeCache() error {
return pc.cache.FlushAll(ctx).Err()
}
func GetPlayerCacheKey(playerId uint, gameTag string) string {
func getPlayerCacheKey(playerId uint, gameTag string) string {
return fmt.Sprintf("player:%d:game:%s", playerId, gameTag)
}

View File

@@ -1,18 +1,17 @@
package models
import (
"context"
"cache"
"github.com/glebarez/sqlite"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"log"
"os"
"time"
)
var DB *gorm.DB
var ctx = context.Background()
var Cache *redis.Client
var PlayerCache *cache.PlayerCache
func ConnectDatabase() {
database, err := gorm.Open(sqlite.Open("isc_data.db"), &gorm.Config{
@@ -61,25 +60,15 @@ func ConnectDatabase() {
DB = database
}
func ConnectCache() {
func ConnectCache(playerCacheLifetime time.Duration) {
address := os.Getenv("REDIS_ADDRESS")
if address == "" {
address = "127.0.0.1"
}
port := os.Getenv("REDIS_PORT")
if port == "" {
port = "6379"
address = "127.0.0.1:6379"
}
Cache = redis.NewClient(&redis.Options{
Addr: address + ":" + port,
Password: "",
DB: 0,
})
_, err := Cache.Ping(ctx).Result()
if err != nil {
Cache = nil
PlayerCache = cache.NewPlayerCache(address, playerCacheLifetime)
if err := PlayerCache.Connect(); err != nil {
PlayerCache = nil
log.Fatal("Failed to connect to Redis! " + err.Error())
}
}

View File

@@ -1,7 +1,9 @@
{{ range . }}
<div class="input-group input-group-lg mb-1">
<div class="input-group-text py-1 px-2">
<input class="form-check-input fs-4 border-secondary mt-0" type="checkbox" value="" onchange="updateSelectedPlayers(this)">
<label>
<input class="form-check-input fs-4 border-secondary mt-0" type="checkbox" value="" onchange="updateSelectedPlayers(this)">
</label>
</div>
<span class="form-control py-2 px-3" style="width: 10em">{{ .PlayerName }}</span>
<div class="form-control text-center px-2 text-secondary">