From 2757a5432f07a2d856d880acaf9331756862b2cc Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Sat, 21 Feb 2026 13:35:04 +0100 Subject: [PATCH] refactor(channelserver): extract QuestCache from Server struct The quest cache fields (lock, data map, expiry map) were spread across Server with manual lock management. The old read path also missed the RLock entirely, creating a data race. Encapsulating in a dedicated type fixes the race and reduces Server's field count by 2. --- server/channelserver/handlers_quest.go | 13 ++--- server/channelserver/quest_cache.go | 49 +++++++++++++++++++ server/channelserver/sys_channel_server.go | 7 +-- .../channelserver/sys_channel_server_test.go | 3 +- 4 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 server/channelserver/quest_cache.go diff --git a/server/channelserver/handlers_quest.go b/server/channelserver/handlers_quest.go index 6721ecfd5..a5670ebb5 100644 --- a/server/channelserver/handlers_quest.go +++ b/server/channelserver/handlers_quest.go @@ -205,9 +205,8 @@ func handleMsgMhfSaveFavoriteQuest(s *Session, p mhfpacket.MHFPacket) { } func loadQuestFile(s *Session, questId int) []byte { - data, exists := s.server.questCacheData[questId] - if exists && s.server.questCacheTime[questId].Add(time.Duration(s.server.erupeConfig.QuestCacheExpiry)*time.Second).After(time.Now()) { - return data + if cached, ok := s.server.questCache.Get(questId); ok { + return cached } file, err := os.ReadFile(filepath.Join(s.server.erupeConfig.BinPath, fmt.Sprintf("quests/%05dd0.bin", questId))) @@ -260,11 +259,9 @@ func loadQuestFile(s *Session, questId int) []byte { } questBody.WriteBytes(newStrings.Data()) - s.server.questCacheLock.Lock() - s.server.questCacheData[questId] = questBody.Data() - s.server.questCacheTime[questId] = time.Now() - s.server.questCacheLock.Unlock() - return questBody.Data() + result := questBody.Data() + s.server.questCache.Put(questId, result) + return result } func makeEventQuest(s *Session, rows *sql.Rows) ([]byte, error) { diff --git a/server/channelserver/quest_cache.go b/server/channelserver/quest_cache.go new file mode 100644 index 000000000..c36e781be --- /dev/null +++ b/server/channelserver/quest_cache.go @@ -0,0 +1,49 @@ +package channelserver + +import ( + "sync" + "time" +) + +// QuestCache is a thread-safe, expiring cache for parsed quest file data. +type QuestCache struct { + mu sync.RWMutex + data map[int][]byte + expiry map[int]time.Time + ttl time.Duration +} + +// NewQuestCache creates a QuestCache with the given TTL in seconds. +// A TTL of 0 disables caching (Get always misses). +func NewQuestCache(ttlSeconds int) *QuestCache { + return &QuestCache{ + data: make(map[int][]byte), + expiry: make(map[int]time.Time), + ttl: time.Duration(ttlSeconds) * time.Second, + } +} + +// Get returns cached quest data if it exists and has not expired. +func (c *QuestCache) Get(questID int) ([]byte, bool) { + if c.ttl <= 0 { + return nil, false + } + c.mu.RLock() + defer c.mu.RUnlock() + b, ok := c.data[questID] + if !ok { + return nil, false + } + if time.Now().After(c.expiry[questID]) { + return nil, false + } + return b, true +} + +// Put stores quest data in the cache with the configured TTL. +func (c *QuestCache) Put(questID int, b []byte) { + c.mu.Lock() + c.data[questID] = b + c.expiry[questID] = time.Now().Add(c.ttl) + c.mu.Unlock() +} diff --git a/server/channelserver/sys_channel_server.go b/server/channelserver/sys_channel_server.go index 3583c40ad..c15182d4e 100644 --- a/server/channelserver/sys_channel_server.go +++ b/server/channelserver/sys_channel_server.go @@ -101,9 +101,7 @@ type Server struct { raviente *Raviente - questCacheLock sync.RWMutex - questCacheData map[int][]byte - questCacheTime map[int]time.Time + questCache *QuestCache handlerTable map[network.PacketID]handlerFunc } @@ -132,8 +130,7 @@ func NewServer(config *Config) *Server { state: make([]uint32, 30), support: make([]uint32, 30), }, - questCacheData: make(map[int][]byte), - questCacheTime: make(map[int]time.Time), + questCache: NewQuestCache(config.ErupeConfig.QuestCacheExpiry), handlerTable: buildHandlerTable(), } diff --git a/server/channelserver/sys_channel_server_test.go b/server/channelserver/sys_channel_server_test.go index 244bac8c9..18f539094 100644 --- a/server/channelserver/sys_channel_server_test.go +++ b/server/channelserver/sys_channel_server_test.go @@ -58,8 +58,7 @@ func createTestServer() *Server { sessions: make(map[net.Conn]*Session), stages: make(map[string]*Stage), semaphore: make(map[string]*Semaphore), - questCacheData: make(map[int][]byte), - questCacheTime: make(map[int]time.Time), + questCache: NewQuestCache(0), erupeConfig: &cfg.Config{ DebugOptions: cfg.DebugOptions{ LogOutboundMessages: false,