Enha: timer package

This commit is contained in:
Grail Finder
2025-07-07 15:01:15 +03:00
parent 75651d7f76
commit 22ddc88d82
5 changed files with 153 additions and 126 deletions

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)

83
timer/timer.go Normal file
View File

@ -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)
}
}

View File

@ -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;