From 22ddc88d8254a5ecd9cf50a9652538c1c21d9e68 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Mon, 7 Jul 2025 15:01:15 +0300 Subject: [PATCH] Enha: timer package --- handlers/timer.go | 82 ++++++++++++------------------------- llmapi/timer.go | 102 ++++++++++++++++------------------------------ repos/main.go | 11 ++++- timer/timer.go | 83 +++++++++++++++++++++++++++++++++++++ todos.md | 1 + 5 files changed, 153 insertions(+), 126 deletions(-) create mode 100644 timer/timer.go diff --git a/handlers/timer.go b/handlers/timer.go index d0dd319..3f9d945 100644 --- a/handlers/timer.go +++ b/handlers/timer.go @@ -3,69 +3,37 @@ package handlers import ( "context" "gralias/models" + "gralias/timer" "log/slog" "strconv" - "sync" - "time" -) - -type roomTimer struct { - ticker *time.Ticker - done chan bool -} - -var ( - timers = make(map[string]*roomTimer) - mu sync.Mutex ) func StartTurnTimer(roomID string, timeLeft uint32) { - mu.Lock() - defer mu.Unlock() - if _, exists := timers[roomID]; exists { - slog.Debug("trying to launch already running timer", "room_id", roomID) - return // Timer already running - } - ticker := time.NewTicker(1 * time.Second) - done := make(chan bool) - timers[roomID] = &roomTimer{ticker: ticker, done: done} - go func() { - for { - select { - case <-done: - return - case <-ticker.C: - if timeLeft <= 0 { - room, err := repo.RoomGetByID(context.Background(), roomID) - if err != nil { - log.Error("failed to get room by id", "error", err) - StopTurnTimer(roomID) - return - } - log.Info("turn time is over", "room_id", roomID) - room.ChangeTurn() - room.MimeDone = false - if err := repo.RoomUpdate(context.Background(), room); err != nil { - log.Error("failed to save room", "error", err) - } - notify(models.NotifyTurnTimerPrefix+room.ID, strconv.FormatUint(uint64(room.Settings.RoundTime), 10)) - notifyBotIfNeeded(room) - StopTurnTimer(roomID) - return - } - timeLeft-- - notify(models.NotifyTurnTimerPrefix+roomID, strconv.FormatUint(uint64(timeLeft), 10)) - } + logger := slog.Default().With("room_id", roomID) + + onTurnEnd := func(ctx context.Context, roomID string) { + room, err := repo.RoomGetByID(context.Background(), roomID) + if err != nil { + logger.Error("failed to get room by id", "error", err) + return } - }() + logger.Info("turn time is over") + room.ChangeTurn() + room.MimeDone = false + if err := repo.RoomUpdate(context.Background(), room); err != nil { + logger.Error("failed to save room", "error", err) + } + notify(models.NotifyTurnTimerPrefix+room.ID, strconv.FormatUint(uint64(room.Settings.RoundTime), 10)) + notifyBotIfNeeded(room) + } + + onTick := func(ctx context.Context, roomID string, currentLeft uint32) { + notify(models.NotifyTurnTimerPrefix+roomID, strconv.FormatUint(uint64(currentLeft), 10)) + } + + timer.StartTurnTimer(context.Background(), roomID, timeLeft, onTurnEnd, onTick, logger) } func StopTurnTimer(roomID string) { - mu.Lock() - defer mu.Unlock() - if timer, exists := timers[roomID]; exists { - timer.ticker.Stop() - close(timer.done) - delete(timers, roomID) - } -} + timer.StopTurnTimer(roomID) +} \ No newline at end of file diff --git a/llmapi/timer.go b/llmapi/timer.go index 4ea0475..21bf0fa 100644 --- a/llmapi/timer.go +++ b/llmapi/timer.go @@ -4,78 +4,46 @@ import ( "context" "gralias/broker" "gralias/models" + "gralias/repos" + "gralias/timer" "strconv" - "sync" - "time" -) - -type roomTimer struct { - ticker *time.Ticker - done chan bool -} - -var ( - timers = make(map[string]*roomTimer) - mu sync.Mutex ) func (b *Bot) StartTurnTimer(timeLeft uint32) { - mu.Lock() - defer mu.Unlock() - if _, exists := timers[b.RoomID]; exists { - b.log.Debug("trying to launch already running timer", "room_id", b.RoomID) - return // Timer already running - } - ticker := time.NewTicker(1 * time.Second) - done := make(chan bool) - timers[b.RoomID] = &roomTimer{ticker: ticker, done: done} - go func() { - for { - select { - case <-done: - return - case <-ticker.C: - if timeLeft <= 0 { - room, err := repo.RoomGetByID(context.Background(), b.RoomID) - if err != nil { - b.log.Error("failed to get room by id", "error", err) - b.StopTurnTimer() - return - } - b.log.Info("turn time is over", "room_id", b.RoomID) - room.ChangeTurn() - room.MimeDone = false - if err := repo.RoomUpdate(context.Background(), room); err != nil { - b.log.Error("failed to save room", "error", err) - } - broker.Notifier.Notifier <- broker.NotificationEvent{ - EventName: models.NotifyTurnTimerPrefix + room.ID, - Payload: strconv.FormatUint(uint64(room.Settings.RoundTime), 10), - } - // notifyBotIfNeeded(room) - if botName := room.WhichBotToMove(); botName != "" { - SignalChanMap[botName] <- true - } - b.StopTurnTimer() - return - } - timeLeft-- - // notify(models.NotifyTurnTimerPrefix+roomID, strconv.FormatUint(uint64(timeLeft), 10)) - broker.Notifier.Notifier <- broker.NotificationEvent{ - EventName: models.NotifyTurnTimerPrefix + b.RoomID, - Payload: strconv.FormatUint(uint64(timeLeft), 10), - } - } + logger := b.log.With("room_id", b.RoomID) + + onTurnEnd := func(ctx context.Context, roomID string) { + room, err := repos.RP.RoomGetByID(context.Background(), roomID) + if err != nil { + logger.Error("failed to get room by id", "error", err) + return } - }() + logger.Info("turn time is over") + room.ChangeTurn() + room.MimeDone = false + if err := repos.RP.RoomUpdate(context.Background(), room); err != nil { + logger.Error("failed to save room", "error", err) + } + broker.Notifier.Notifier <- broker.NotificationEvent{ + EventName: models.NotifyTurnTimerPrefix + room.ID, + Payload: strconv.FormatUint(uint64(room.Settings.RoundTime), 10), + } + // notifyBotIfNeeded(room) + if botName := room.WhichBotToMove(); botName != "" { + SignalChanMap[botName] <- true + } + } + + onTick := func(ctx context.Context, roomID string, currentLeft uint32) { + broker.Notifier.Notifier <- broker.NotificationEvent{ + EventName: models.NotifyTurnTimerPrefix + roomID, + Payload: strconv.FormatUint(uint64(currentLeft), 10), + } + } + + timer.StartTurnTimer(context.Background(), b.RoomID, timeLeft, onTurnEnd, onTick, logger) } func (b *Bot) StopTurnTimer() { - mu.Lock() - defer mu.Unlock() - if timer, exists := timers[b.RoomID]; exists { - timer.ticker.Stop() - close(timer.done) - delete(timers, b.RoomID) - } -} + timer.StopTurnTimer(b.RoomID) +} \ No newline at end of file diff --git a/repos/main.go b/repos/main.go index 13d2b0d..fbbbf30 100644 --- a/repos/main.go +++ b/repos/main.go @@ -2,6 +2,7 @@ package repos import ( "context" + "gralias/config" "log/slog" "os" "sync" @@ -20,6 +21,7 @@ type AllRepos interface { SettingsRepo CardMarksRepo PlayerStatsRepo + JournalRepo InitTx(ctx context.Context) (context.Context, *sqlx.Tx, error) Close() } @@ -32,9 +34,14 @@ type RepoProvider struct { var RP AllRepos +func init() { + cfg := config.LoadConfigOrDefault("") + // sqlite3 has lock on write, so we need to have only one connection per whole app + // https://github.com/mattn/go-sqlite3/issues/274#issuecomment-232942571 + RP = NewRepoProvider(cfg.DBPath) +} - -func NewRepoProvider(pathToDB string) *RepoProvider { +func NewRepoProvider(pathToDB string) AllRepos { db, err := sqlx.Connect("sqlite3", pathToDB) if err != nil { slog.Error("Unable to connect to database", "error", err) diff --git a/timer/timer.go b/timer/timer.go new file mode 100644 index 0000000..c44312a --- /dev/null +++ b/timer/timer.go @@ -0,0 +1,83 @@ +package timer + +import ( + "context" + "log/slog" + "sync" + "time" +) + +// TurnEndCallback defines the function signature for actions to be performed when a turn ends. +type TurnEndCallback func(ctx context.Context, roomID string) + +// TickCallback defines the function signature for actions to be performed on each timer tick. +type TickCallback func(ctx context.Context, roomID string, timeLeft uint32) + +type RoomTimer struct { + ticker *time.Ticker + done chan bool + roomID string + onTurnEnd TurnEndCallback + onTick TickCallback + log *slog.Logger +} + +var ( + timers = make(map[string]*RoomTimer) + mu sync.Mutex +) + +// StartTurnTimer initializes and starts a new turn timer for a given room. +func StartTurnTimer(ctx context.Context, roomID string, timeLeft uint32, onTurnEnd TurnEndCallback, onTick TickCallback, logger *slog.Logger) { + mu.Lock() + defer mu.Unlock() + + if _, exists := timers[roomID]; exists { + logger.Debug("trying to launch already running timer", "room_id", roomID) + return // Timer already running + } + + ticker := time.NewTicker(1 * time.Second) + done := make(chan bool) + + rt := &RoomTimer{ + ticker: ticker, + done: done, + roomID: roomID, + onTurnEnd: onTurnEnd, + onTick: onTick, + log: logger, + } + timers[roomID] = rt + + go func() { + currentLeft := timeLeft + for { + select { + case <-done: + return + case <-ticker.C: + if currentLeft <= 0 { + rt.onTurnEnd(ctx, roomID) + StopTurnTimer(roomID) + return + } + rt.onTick(ctx, roomID, currentLeft) + currentLeft-- + } + } + }() +} + +// StopTurnTimer stops the timer for a given room. +func StopTurnTimer(roomID string) { + mu.Lock() + defer mu.Unlock() + + if rt, exists := timers[roomID]; exists { + rt.ticker.Stop() + close(rt.done) + delete(timers, roomID) + rt.log.Info("timer stopped", "room_id", roomID) + } +} diff --git a/todos.md b/todos.md index cf5c7c7..734ea95 100644 --- a/todos.md +++ b/todos.md @@ -87,3 +87,4 @@ - timer ended and went to 300; - mime sees the clue input out of turn; - there is a problem of two timers, they both could switch turn, but it is not easy to stop them from llmapi or handlers. +- journal still does not work;