From 71a2d9d747fc5b37b8a529accda1c48e2da45335 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Fri, 4 Jul 2025 10:34:08 +0300 Subject: [PATCH] Feat: add cleaner cron --- config.example.toml | 1 + config/config.go | 2 ++ crons/main.go | 76 ++++++++++++++++++++++++++++++++++++++++++ handlers/game.go | 16 ++++++--- handlers/middleware.go | 4 ++- main.go | 8 +++++ repos/main.go | 4 +++ repos/players.go | 10 ++++++ 8 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 crons/main.go diff --git a/config.example.toml b/config.example.toml index 9b84f99..99cb8e7 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,6 +1,7 @@ BASE_URL = "https://localhost:3000" SESSION_LIFETIME_SECONDS = 30000 COOKIE_SECRET = "test" +DB_PATH = "sqlite3://gralias.db" [SERVICE] HOST = "localhost" diff --git a/config/config.go b/config/config.go index 78b4454..3f9a279 100644 --- a/config/config.go +++ b/config/config.go @@ -13,6 +13,7 @@ type Config struct { SessionLifetime int64 `toml:"SESSION_LIFETIME_SECONDS"` CookieSecret string `toml:"COOKIE_SECRET"` LLMConfig LLMConfig `toml:"LLM"` + DBPath string `toml:"DB_PATH"` } type ServerConfig struct { @@ -38,6 +39,7 @@ func LoadConfigOrDefault(fn string) *Config { config.CookieSecret = "test" config.ServerConfig.Host = "localhost" config.ServerConfig.Port = "3000" + config.DBPath = "sqlite3://gralias.db" } fmt.Printf("config debug; config.LLMConfig.URL: %s\n", config.LLMConfig.URL) return config diff --git a/crons/main.go b/crons/main.go new file mode 100644 index 0000000..9e152de --- /dev/null +++ b/crons/main.go @@ -0,0 +1,76 @@ +package crons + +import ( + "context" + "gralias/repos" + "log/slog" + "time" +) + +type CronManager struct { + repo repos.AllRepos + log *slog.Logger +} + +func NewCronManager(repo repos.AllRepos, log *slog.Logger) *CronManager { + return &CronManager{ + repo: repo, + log: log, + } +} + +func (cm *CronManager) Start() { + ticker := time.NewTicker(30 * time.Second) + go func() { + for range ticker.C { + cm.CleanupRooms() + } + }() +} + +func (cm *CronManager) CleanupRooms() { + ctx := context.Background() + rooms, err := cm.repo.RoomList(ctx) + if err != nil { + cm.log.Error("failed to get rooms list", "err", err) + return + } + for _, room := range rooms { + players, err := cm.repo.PlayerListByRoom(ctx, room.ID) + if err != nil { + cm.log.Error("failed to get players for room", "room_id", room.ID, "err", err) + continue + } + if len(players) == 0 { + cm.log.Info("deleting empty room", "room_id", room.ID) + if err := cm.repo.RoomDeleteByID(ctx, room.ID); err != nil { + cm.log.Error("failed to delete empty room", "room_id", room.ID, "err", err) + } + continue + } + creatorInRoom := false + for _, player := range players { + if player.Username == room.CreatorName { + creatorInRoom = true + break + } + } + if !creatorInRoom { + cm.log.Info("deleting room because creator left", "room_id", room.ID) + for _, player := range players { + if player.IsBot { + if err := cm.repo.PlayerDelete(ctx, room.ID, player.Username); err != nil { + cm.log.Error("failed to delete bot player", "room_id", room.ID, "username", player.Username, "err", err) + } + } else { + if err := cm.repo.PlayerExitRoom(ctx, player.Username); err != nil { + cm.log.Error("failed to update player room", "room_id", room.ID, "username", player.Username, "err", err) + } + } + } + if err := cm.repo.RoomDeleteByID(ctx, room.ID); err != nil { + cm.log.Error("failed to delete room after creator left", "room_id", room.ID, "err", err) + } + } + } +} diff --git a/handlers/game.go b/handlers/game.go index b5715fe..a4e2ed5 100644 --- a/handlers/game.go +++ b/handlers/game.go @@ -168,7 +168,9 @@ func HandleStartGame(w http.ResponseWriter, r *http.Request) { } defer func() { if r := recover(); r != nil { - tx.Rollback() + if err := tx.Rollback(); err != nil { + log.Error("failed to rollback transaction", "error", err) + } panic(r) } }() @@ -189,7 +191,9 @@ func HandleStartGame(w http.ResponseWriter, r *http.Request) { fi.Room.ActionHistory = append(fi.Room.ActionHistory, action) // Use the new context with transaction if err := saveFullInfo(ctx, fi); err != nil { - tx.Rollback() + if err := tx.Rollback(); err != nil { + log.Error("failed to rollback transaction", "error", err) + } abortWithError(w, err.Error()) return } @@ -197,7 +201,9 @@ func HandleStartGame(w http.ResponseWriter, r *http.Request) { action.RoomID = fi.Room.ID action.CreatedAt = time.Now() if err := repo.CreateAction(ctx, fi.Room.ID, &action); err != nil { - tx.Rollback() + if err := tx.Rollback(); err != nil { + log.Error("failed to rollback transaction", "error", err) + } log.Error("failed to save action", "error", err) abortWithError(w, err.Error()) return @@ -206,7 +212,9 @@ func HandleStartGame(w http.ResponseWriter, r *http.Request) { for _, card := range fi.Room.Cards { card.RoomID = fi.Room.ID // Ensure RoomID is set for each card if err := repo.WordCardsCreate(ctx, &card); err != nil { - tx.Rollback() + if err := tx.Rollback(); err != nil { + log.Error("failed to rollback transaction", "error", err) + } log.Error("failed to save word card", "error", err) abortWithError(w, err.Error()) return diff --git a/handlers/middleware.go b/handlers/middleware.go index 5315f74..3850b3c 100644 --- a/handlers/middleware.go +++ b/handlers/middleware.go @@ -70,7 +70,9 @@ func GetSession(next http.Handler) http.Handler { return } if userSession.IsExpired() { - repo.SessionDelete(r.Context(), sessionToken) + if err := repo.SessionDelete(r.Context(), sessionToken); err != nil { + log.Error("failed to delete session", "error", err) + } // cache.MemCache.RemoveKey(sessionToken) msg := "session is expired" log.Debug(msg, "error", err, "token", sessionToken) diff --git a/main.go b/main.go index 6330583..fe79796 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,9 @@ package main import ( "context" "gralias/config" + "gralias/crons" "gralias/handlers" + "gralias/repos" "log/slog" "net/http" "os" @@ -61,6 +63,12 @@ func main() { stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) + repo := repos.NewRepoProvider(cfg.DBPath) + defer repo.Close() + + cm := crons.NewCronManager(repo, slog.Default()) + cm.Start() + server := ListenToRequests(cfg.ServerConfig.Port) go func() { if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { diff --git a/repos/main.go b/repos/main.go index 1c1cd22..6cd19bf 100644 --- a/repos/main.go +++ b/repos/main.go @@ -104,3 +104,7 @@ func (p *RepoProvider) InitTx(ctx context.Context) (context.Context, *sqlx.Tx, e } return context.WithValue(ctx, "tx", tx), tx, nil } + +func (p *RepoProvider) Close() { + p.DB.Close() +} diff --git a/repos/players.go b/repos/players.go index 2d4fb8b..188225e 100644 --- a/repos/players.go +++ b/repos/players.go @@ -16,6 +16,7 @@ type PlayersRepo interface { PlayerExitRoom(ctx context.Context, username string) error PlayerListNames(ctx context.Context) ([]string, error) PlayerList(ctx context.Context, isBot bool) ([]models.Player, error) + PlayerListByRoom(ctx context.Context, roomID string) ([]models.Player, error) } func (p *RepoProvider) PlayerListNames(ctx context.Context) ([]string, error) { @@ -85,3 +86,12 @@ func (p *RepoProvider) PlayerList(ctx context.Context, isBot bool) ([]models.Pla } return players, nil } + +func (p *RepoProvider) PlayerListByRoom(ctx context.Context, roomID string) ([]models.Player, error) { + var players []models.Player + err := sqlx.SelectContext(ctx, p.DB, &players, "SELECT id, room_id, username, team, role, is_bot FROM players WHERE room_id = ?", roomID) + if err != nil { + return nil, err + } + return players, nil +}