add redis cache to results
This commit is contained in:
@@ -138,13 +138,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
<<<<<<< HEAD
|
|
||||||
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
|
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
|
||||||
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||||
=======
|
|
||||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
|
||||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
|
||||||
>>>>>>> origin/feature/task
|
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
|||||||
@@ -11,18 +11,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
GRPCPort int
|
GRPCPort int
|
||||||
GRPCEnableReflection bool
|
GRPCEnableReflection bool
|
||||||
HTTPPort int
|
HTTPPort int
|
||||||
LogLevel string
|
LogLevel string
|
||||||
DBHost string
|
DBHost string
|
||||||
DBPort int
|
DBPort int
|
||||||
DBUser string
|
DBUser string
|
||||||
DBPassword string
|
DBPassword string
|
||||||
DBName string
|
DBName string
|
||||||
|
RedisAddr string
|
||||||
UserServiceAddr string
|
UserServiceAddr string
|
||||||
SubmissionServiceAddr string
|
SubmissionServiceAddr string
|
||||||
TaskServiceAddr string
|
TaskServiceAddr string
|
||||||
|
CacheEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load() (*Config, error) {
|
func Load() (*Config, error) {
|
||||||
@@ -38,9 +40,11 @@ func Load() (*Config, error) {
|
|||||||
DBUser: getEnv("POSTGRES_USERNAME", "postgres"),
|
DBUser: getEnv("POSTGRES_USERNAME", "postgres"),
|
||||||
DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"),
|
DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"),
|
||||||
DBName: getEnv("POSTGRES_DATABASE", "postgres"),
|
DBName: getEnv("POSTGRES_DATABASE", "postgres"),
|
||||||
|
RedisAddr: getEnv("REDIS_ADDR", "localhost:6379"),
|
||||||
UserServiceAddr: getEnv("USER_GRPC_ADDR", "localhost:50051"),
|
UserServiceAddr: getEnv("USER_GRPC_ADDR", "localhost:50051"),
|
||||||
SubmissionServiceAddr: getEnv("SUBMISSION_GRPC_ADDR", "localhost:50055"),
|
SubmissionServiceAddr: getEnv("SUBMISSION_GRPC_ADDR", "localhost:50055"),
|
||||||
TaskServiceAddr: getEnv("TASK_GRPC_ADDR", "localhost:50056"),
|
TaskServiceAddr: getEnv("TASK_GRPC_ADDR", "localhost:50056"),
|
||||||
|
CacheEnabled: mustGetBool("CACHE_ENABLED", false),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
resultCachePrefix = "result:"
|
resultCachePrefix = "result:"
|
||||||
|
leaderboardCachePrefix = "leaderboard:"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type leaderboardCache struct {
|
||||||
|
Results []domain.Result `json:"results"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
}
|
||||||
|
|
||||||
type ResultRepository struct {
|
type ResultRepository struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
redisClient *redis.Client
|
redisClient *redis.Client
|
||||||
@@ -37,6 +43,21 @@ func (r *ResultRepository) cacheKey(id string) string {
|
|||||||
return resultCachePrefix + id
|
return resultCachePrefix + id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ResultRepository) leaderboardCacheKey(competitionID uuid.UUID, limit, offset int) string {
|
||||||
|
return fmt.Sprintf("%s%s:limit:%d:offset:%d", leaderboardCachePrefix, competitionID.String(), limit, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ResultRepository) invalidateLeaderboardCache(ctx context.Context, competitionID uuid.UUID) {
|
||||||
|
if !r.cacheEnabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pattern := fmt.Sprintf("%s%s:*", leaderboardCachePrefix, competitionID.String())
|
||||||
|
keys, err := r.redisClient.Keys(ctx, pattern).Result()
|
||||||
|
if err == nil && len(keys) > 0 {
|
||||||
|
r.redisClient.Del(ctx, keys...).Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ResultRepository) Create(ctx context.Context, result *domain.Result) error {
|
func (r *ResultRepository) Create(ctx context.Context, result *domain.Result) error {
|
||||||
metadataJSON, err := json.Marshal(result.Metadata)
|
metadataJSON, err := json.Marshal(result.Metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -65,6 +86,7 @@ func (r *ResultRepository) Create(ctx context.Context, result *domain.Result) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
if r.cacheEnabled {
|
if r.cacheEnabled {
|
||||||
|
r.invalidateLeaderboardCache(ctx, result.CompetitionID)
|
||||||
data, err := json.Marshal(result)
|
data, err := json.Marshal(result)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
r.redisClient.Set(ctx, r.cacheKey(result.ID.String()), data, 10*time.Minute).Err()
|
r.redisClient.Set(ctx, r.cacheKey(result.ID.String()), data, 10*time.Minute).Err()
|
||||||
@@ -160,6 +182,7 @@ func (r *ResultRepository) Update(ctx context.Context, result *domain.Result) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
if r.cacheEnabled {
|
if r.cacheEnabled {
|
||||||
|
r.invalidateLeaderboardCache(ctx, result.CompetitionID)
|
||||||
r.redisClient.Del(ctx, r.cacheKey(result.ID.String())).Err()
|
r.redisClient.Del(ctx, r.cacheKey(result.ID.String())).Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +335,16 @@ func (r *ResultRepository) GetByCompetitionAndUser(ctx context.Context, competit
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *ResultRepository) GetLeaderboard(ctx context.Context, competitionID uuid.UUID, limit int, offset int) ([]domain.Result, int, error) {
|
func (r *ResultRepository) GetLeaderboard(ctx context.Context, competitionID uuid.UUID, limit int, offset int) ([]domain.Result, int, error) {
|
||||||
|
if r.cacheEnabled {
|
||||||
|
key := r.leaderboardCacheKey(competitionID, limit, offset)
|
||||||
|
val, err := r.redisClient.Get(ctx, key).Result()
|
||||||
|
if err == nil {
|
||||||
|
var cached leaderboardCache
|
||||||
|
if json.Unmarshal([]byte(val), &cached) == nil {
|
||||||
|
return cached.Results, cached.TotalCount, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
countQuery := `SELECT COUNT(*) FROM results WHERE competition_id = $1 AND status = $2`
|
countQuery := `SELECT COUNT(*) FROM results WHERE competition_id = $1 AND status = $2`
|
||||||
var total int
|
var total int
|
||||||
if err := r.db.QueryRowContext(ctx, countQuery, competitionID, domain.ResultStatusCompleted).Scan(&total); err != nil {
|
if err := r.db.QueryRowContext(ctx, countQuery, competitionID, domain.ResultStatusCompleted).Scan(&total); err != nil {
|
||||||
@@ -359,6 +392,15 @@ func (r *ResultRepository) GetLeaderboard(ctx context.Context, competitionID uui
|
|||||||
results = append(results, result)
|
results = append(results, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.cacheEnabled {
|
||||||
|
key := r.leaderboardCacheKey(competitionID, limit, offset)
|
||||||
|
cacheData := leaderboardCache{Results: results, TotalCount: total}
|
||||||
|
data, err := json.Marshal(cacheData)
|
||||||
|
if err == nil {
|
||||||
|
r.redisClient.Set(ctx, key, data, 1*time.Minute).Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return results, total, nil
|
return results, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,7 +424,16 @@ func (r *ResultRepository) UpdateStatus(ctx context.Context, id uuid.UUID, statu
|
|||||||
r.redisClient.Del(ctx, r.cacheKey(id.String())).Err()
|
r.redisClient.Del(ctx, r.cacheKey(id.String())).Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.Get(ctx, id)
|
result, err := r.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.cacheEnabled {
|
||||||
|
r.invalidateLeaderboardCache(ctx, result.CompetitionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ResultRepository) RecalculateRanks(ctx context.Context, competitionID uuid.UUID) error {
|
func (r *ResultRepository) RecalculateRanks(ctx context.Context, competitionID uuid.UUID) error {
|
||||||
@@ -425,5 +476,14 @@ func (r *ResultRepository) RecalculateRanks(ctx context.Context, competitionID u
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tx.Commit()
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.cacheEnabled {
|
||||||
|
r.invalidateLeaderboardCache(ctx, competitionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/reflection"
|
"google.golang.org/grpc/reflection"
|
||||||
)
|
)
|
||||||
@@ -30,6 +31,7 @@ type Server struct {
|
|||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
config *config.Config
|
config *config.Config
|
||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
|
redis *redis.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg *config.Config) *Server {
|
func New(cfg *config.Config) *Server {
|
||||||
@@ -45,6 +47,11 @@ func (s *Server) Start() error {
|
|||||||
}
|
}
|
||||||
s.db = db
|
s.db = db
|
||||||
|
|
||||||
|
redisClient := redis.NewClient(&redis.Options{
|
||||||
|
Addr: s.config.RedisAddr,
|
||||||
|
})
|
||||||
|
s.redis = redisClient
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := s.startGRPCServer(); err != nil {
|
if err := s.startGRPCServer(); err != nil {
|
||||||
log.Fatalf("failed to start gRPC server: %v", err)
|
log.Fatalf("failed to start gRPC server: %v", err)
|
||||||
@@ -69,7 +76,7 @@ func (s *Server) startGRPCServer() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) registerGRPCServices() {
|
func (s *Server) registerGRPCServices() {
|
||||||
resultRepo := postgres.NewResultRepository(s.db.DB, nil, false)
|
resultRepo := postgres.NewResultRepository(s.db.DB, s.redis, s.config.CacheEnabled)
|
||||||
resultService := service.NewService(resultRepo)
|
resultService := service.NewService(resultRepo)
|
||||||
resultHandler := grpcHandlers.NewResultsHandler(
|
resultHandler := grpcHandlers.NewResultsHandler(
|
||||||
resultService,
|
resultService,
|
||||||
@@ -98,5 +105,11 @@ func (s *Server) Stop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.redis != nil {
|
||||||
|
if err := s.redis.Close(); err != nil {
|
||||||
|
log.Printf("failed to close redis connection: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.Println("results server stopped")
|
log.Println("results server stopped")
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user