From c9129c2e7edcf6e588cac674dfdb240f1714083d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Herg=C3=A8s?= Date: Sat, 17 Jan 2026 19:15:12 +0100 Subject: feat(member): save cache in db every 30 minutes --- user/level.go | 96 ++++++++++++++++++++++++++++------------------------------ user/member.go | 6 ++-- user/state.go | 91 +++++++++++++++++++++++++++++++++++-------------------- user/xp.go | 6 ++-- 4 files changed, 108 insertions(+), 91 deletions(-) (limited to 'user') diff --git a/user/level.go b/user/level.go index 654ecd1..07df16f 100644 --- a/user/level.go +++ b/user/level.go @@ -1,6 +1,7 @@ package user import ( + "context" "slices" "sync" "time" @@ -45,7 +46,7 @@ func onNewLevel(s bot.Session, m *user.Member, level uint) { } } -func (c *Copaing) OnNewLevel(s *discordgo.Session, level uint) { +func (c *CopaingCached) onNewLevel(s *discordgo.Session, level uint) { m, err := s.GuildAPI().Member(c.GuildID, c.DiscordID) if err != nil { s.Logger().Error("getting member for new level", "error", err, "user", c.DiscordID, "guild", c.GuildID) @@ -54,66 +55,61 @@ func (c *Copaing) OnNewLevel(s *discordgo.Session, level uint) { onNewLevel(s, m, level) } -func PeriodicReducer(s *discordgo.Session) { - wg := &sync.WaitGroup{} - var cs []*Copaing - if err := gokord.DB.Find(&cs).Error; err != nil { - s.Logger().Error("fetching all copaings", "error", err) - return - } - cxps := make([]*cXP, len(cs)) - for i, c := range cs { - if i%25 == 24 { - wg.Wait() // prevents spamming the DB - } - wg.Add(1) - go func() { - defer wg.Done() - xp, err := c.GetXP(s.Logger()) - if err != nil { - s.Logger().Error("getting xp", "error", err, "copaing", c.ID, "guild", c.GuildID) - xp = 0 - } - cxps[i] = &cXP{ - Cxp: xp, - copaing: c, - } - }() - } - wg.Wait() - i := 0 +func PeriodicReducer(ctx context.Context, s *discordgo.Session) { + PeriodicSaver(ctx, s) + + s.Logger().Debug("periodic reducer") + + state := GetState(ctx) + + n := 0 + var wg sync.WaitGroup for _, g := range s.GuildAPI().State.Guilds() { - i++ - wg.Add(1) - go func() { - defer wg.Done() - cfg := config.GetGuildConfig(g) - res := gokord.DB. - Model(&CopaingXP{}). - Where("guild_id = ? and created_at < ?", g, exp.TimeStampNDaysBefore(cfg.DaysXPRemains)). - Delete(&CopaingXP{}) - if res.Error != nil { - s.Logger().Error("removing old xp", "error", res.Error, "guild", g) - } - s.Logger().Debug("guild cleaned", "guild", g, "rows affected", res.RowsAffected) - }() + n++ + cfg := config.GetGuildConfig(g) + res := gokord.DB. + Model(&CopaingXP{}). + Where("guild_id = ? and created_at < ?", g, exp.TimeStampNDaysBefore(cfg.DaysXPRemains)). + Delete(&CopaingXP{}) + if res.Error != nil { + s.Logger().Error("removing old xp", "error", res.Error, "guild", g) + continue + } + s.Logger().Debug("guild cleaned", "guild", g, "rows affected", res.RowsAffected) + + wg.Go(func() { + syncCopaings(ctx, s, state.Copaings(g)) + }) } + wg.Wait() - for i, c := range cxps { + + s.Logger().Debug("periodic reduce finished", "guilds affected", n) +} + +func syncCopaings(ctx context.Context, s *discordgo.Session, ccs []CopaingCached) { + for i, cc := range ccs { if i%50 == 49 { s.Logger().Debug("sleeping...") time.Sleep(15 * time.Second) // prevents spamming the API } - oldXp := c.GetXP() - cp := c.Copaing() - xp, err := cp.GetXP(s.Logger()) + oldXp := cc.XPs + err := cc.Sync(ctx) if err != nil { - s.Logger().Error("getting xp of copaing", "error", err, "copaing", cp.ID, "guild", cp.GuildID) + s.Logger().Error("syncing copaing", "error", err, "copaing", cc.ID, "guild", cc.GuildID) continue } + xp := cc.XPs if exp.Level(oldXp) != exp.Level(xp) { - cp.OnNewLevel(s, exp.Level(xp)) + cc.onNewLevel(s, exp.Level(xp)) } } - s.Logger().Debug("periodic reduce finished", "guilds affected", i) +} + +func PeriodicSaver(ctx context.Context, s bot.Session) { + s.Logger().Debug("saving state in DB") + err := saveStateInDB(ctx) + if err != nil { + panic(err) + } } diff --git a/user/member.go b/user/member.go index 9c9ad1f..4969f8a 100644 --- a/user/member.go +++ b/user/member.go @@ -32,7 +32,7 @@ func GetCopaing(ctx context.Context, discordID string, guildID string) *CopaingC cc, err := state.Copaing(guildID, discordID) if err != nil { c := Copaing{DiscordID: discordID, GuildID: guildID} - if err := c.Load(ctx); err != nil { + if err := c.load(); err != nil { panic(err) } cc = FromCopaing(&c) @@ -40,7 +40,7 @@ func GetCopaing(ctx context.Context, discordID string, guildID string) *CopaingC return cc } -func (c *Copaing) Load(ctx context.Context) error { +func (c *Copaing) load() error { err := gokord.DB. Where("discord_id = ? and guild_id = ?", c.DiscordID, c.GuildID). Preload("CopaingXPs"). @@ -49,8 +49,6 @@ func (c *Copaing) Load(ctx context.Context) error { if err != nil { return err } - state := GetState(ctx) - _, err = state.CopaingAdd(c, 0) return err } diff --git a/user/state.go b/user/state.go index 84f2852..540d496 100644 --- a/user/state.go +++ b/user/state.go @@ -2,31 +2,46 @@ package user import ( "context" + "errors" "sync" + "time" "github.com/nyttikord/gokord/state" ) +var ErrSyncingUnsavedData = errors.New("trying to sync unsaved data") + type CopaingCached struct { - ID uint `gorm:"primarykey"` - DiscordID string `gorm:"not null"` - GuildID string `gorm:"not null"` + ID uint + DiscordID string + GuildID string XPs uint XPToAdd uint + lastSync time.Time // time.Time of the lastSync } // copaing turns a CopaingCached into a Copaing. -// This operation is heavy. -func (cc *CopaingCached) copaing(ctx context.Context) *Copaing { - c := Copaing{DiscordID: cc.DiscordID, GuildID: cc.GuildID} - if err := c.Load(ctx); err != nil { +// This operation get the copaing synced with the database. +// It doesn't: +// - save the copaing in the database, use CopaingCached.SaveInDB for that; +// - sync the copaing cached, use CopaingCached.Sync for that. +// TL;DR: don't use this method, unless you know what are you doing. +func (cc *CopaingCached) copaing() *Copaing { + c := &Copaing{DiscordID: cc.DiscordID, GuildID: cc.GuildID} + if err := c.load(); err != nil { panic(err) } - return &c + return c } func (cc *CopaingCached) Sync(ctx context.Context) error { - synced, err := GetState(ctx).CopaingAdd(cc.copaing(ctx), cc.XPToAdd) + if cc.mustSave() { + return ErrSyncingUnsavedData + } + synced := FromCopaing(cc.copaing()) + synced.XPs += cc.XPToAdd + synced.XPToAdd = cc.XPToAdd + err := synced.Save(ctx) if err != nil { return err } @@ -44,7 +59,7 @@ func (cc *CopaingCached) Save(ctx context.Context) error { } func (cc *CopaingCached) SaveInDB(ctx context.Context) error { - c := cc.copaing(ctx) + c := cc.copaing() c.CopaingXPs = append(c.CopaingXPs, CopaingXP{CopaingID: c.ID, XP: cc.XPToAdd, GuildID: c.GuildID}) err := c.Save() if err != nil { @@ -55,12 +70,33 @@ func (cc *CopaingCached) SaveInDB(ctx context.Context) error { } func (cc *CopaingCached) Delete(ctx context.Context) error { - c := cc.copaing(ctx) + c := cc.copaing() err := c.Delete() if err != nil { return err } - return GetState(ctx).CopaingRemove(c) + state := GetState(ctx) + + state.mu.Lock() + defer state.mu.Unlock() + + return state.storage.Delete(KeyCopaingCached(c)) +} + +func (cc *CopaingCached) mustSave() bool { + return cc.XPToAdd > 0 +} + +func saveStateInDB(ctx context.Context) error { + for _, v := range GetState(ctx).storage { + if v.mustSave() { + err := v.SaveInDB(ctx) + if err != nil { + return err + } + } + } + return nil } func FromCopaing(c *Copaing) *CopaingCached { @@ -116,28 +152,17 @@ func (s *State) Copaing(guildID, copaingID string) (*CopaingCached, error) { return &mC, nil } -// CopaingAdd does not call Copaing.Load! -func (s *State) CopaingAdd(c *Copaing, xpToAdd uint) (*CopaingCached, error) { - var err error - var cc *CopaingCached - if cc, err = s.Copaing(c.GuildID, c.DiscordID); err == nil { - cc.XPs = calcXP(c) + xpToAdd - } else { - cc = FromCopaing(c) - } - cc.XPToAdd = xpToAdd - - s.mu.Lock() - defer s.mu.Unlock() - - return cc, s.storage.Write(KeyCopaingCached(c), *cc) -} - -func (s *State) CopaingRemove(c *Copaing) error { - s.mu.Lock() - defer s.mu.Unlock() +func (s *State) Copaings(guild string) []CopaingCached { + s.mu.RLock() + defer s.mu.RUnlock() - return s.storage.Delete(KeyCopaingCached(c)) + var ccs []CopaingCached + for _, cc := range s.storage { + if cc.GuildID == guild { + ccs = append(ccs, cc) + } + } + return ccs } func calcXP(c *Copaing) uint { diff --git a/user/xp.go b/user/xp.go index 246b097..985b5f8 100644 --- a/user/xp.go +++ b/user/xp.go @@ -97,16 +97,14 @@ func GetBestXP(logger *slog.Logger, guildId string, n uint, d int) ([]CopaingAcc logger.Error("scanning rows", "error", err, "copaing", c.ID, "guild", c.GuildID) continue } - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { xp, err := c.GetXPForDays(logger, uint(d)) if err != nil { logger.Error("fetching xp", "error", err, "copaing", c.ID, "guild", c.GuildID) return } l = append(l, &cXP{Cxp: xp, copaing: &c}) - }() + }) } wg.Wait() slices.SortFunc(l, func(a, b *cXP) int { -- cgit v1.2.3