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.
This commit is contained in:
Houmgaor
2026-02-21 13:35:04 +01:00
parent 4fbc955774
commit 2757a5432f
4 changed files with 57 additions and 15 deletions

View File

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

View File

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

View File

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

View File

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