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 WORKDIR /app
COPY go.mod go.sum internal/**/go.mod ./
RUN go mod download
COPY . . 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 FROM alpine:latest

View File

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

View File

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

View File

@@ -8,7 +8,6 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
"html/template" "html/template"
"internal/cache"
"log" "log"
"net/http" "net/http"
) )
@@ -51,7 +50,12 @@ func GetPlayersByClanHTML(c *gin.Context) {
return 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 { if err != nil {
c.String(http.StatusBadRequest, "") c.String(http.StatusBadRequest, "")
log.Fatal(err) log.Fatal(err)

View File

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

View File

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

View File

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

View File

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