Apply modern Go patterns across the codebase:
- Replace `interface{}` with `any` (Go 1.18+)
- Use `for range n` instead of `for i := 0; i < n; i++` (Go 1.22+)
- Replace `sort.Slice` with `slices.Sort` from slices package
- Simplify sync.WaitGroup patterns with errgroup where appropriate
- Add Makefile targets for modernize analyzer
Total: 84 files updated, net reduction of 79 lines
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
369 lines
9.0 KiB
Go
369 lines
9.0 KiB
Go
// Package lua 提供 Lua 脚本嵌入能力。
|
||
//
|
||
// 该文件实现 Lua 脚本字节码缓存(CodeCache),包括:
|
||
// - 内联脚本缓存:基于 SHA256 哈希去重
|
||
// - 文件脚本缓存:基于路径哈希 + 文件变更检测
|
||
// - LRU 淘汰策略:容量满时淘汰最久未访问的缓存
|
||
// - TTL 过期机制:超过生存期的缓存自动失效
|
||
// - 文件监控:文件修改时间变化时自动重新编译
|
||
//
|
||
// 注意事项:
|
||
// - 缓存读写使用 sync.RWMutex 保证并发安全
|
||
// - 统计计数使用 atomic 操作
|
||
//
|
||
// 作者:xfy
|
||
package lua
|
||
|
||
import (
|
||
"bufio"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
glua "github.com/yuin/gopher-lua"
|
||
"github.com/yuin/gopher-lua/parse"
|
||
)
|
||
|
||
// CacheKeyType 缓存键类型,区分内联脚本和文件脚本。
|
||
type CacheKeyType int
|
||
|
||
// 缓存键类型常量:内联脚本和文件脚本
|
||
const (
|
||
// CacheKeyInline 内联脚本缓存键(通过 SHA256 哈希标识)
|
||
CacheKeyInline CacheKeyType = iota
|
||
|
||
// CacheKeyFile 文件脚本缓存键(通过路径 SHA256 哈希标识)
|
||
CacheKeyFile
|
||
)
|
||
|
||
// CachedProto 缓存的编译后字节码。
|
||
type CachedProto struct {
|
||
// ModTime 文件修改时间(仅文件脚本有效)
|
||
ModTime time.Time
|
||
|
||
// CachedAt 缓存存入时间(用于 TTL 过期检测)
|
||
CachedAt time.Time
|
||
|
||
// AccessAt 最后访问时间(用于 LRU 淘汰)
|
||
AccessAt atomic.Value
|
||
|
||
// Proto 编译后的 Lua 函数原型
|
||
Proto *glua.FunctionProto
|
||
|
||
// SourcePath 源文件路径(仅文件脚本有效)
|
||
SourcePath string
|
||
|
||
// SourceType 缓存键类型
|
||
SourceType CacheKeyType
|
||
}
|
||
|
||
// CodeCache Lua 脚本字节码缓存。
|
||
//
|
||
// 支持两种缓存源:
|
||
// - 内联脚本:基于内容 SHA256 哈希去重
|
||
// - 文件脚本:基于路径哈希 + 文件变更检测
|
||
//
|
||
// 特性:
|
||
// - LRU 淘汰:容量满时淘汰最久未访问的条目
|
||
// - TTL 过期:超过生存期的缓存自动失效
|
||
// - 文件监控:文件修改时间变化时自动重新编译
|
||
// - 并发安全:使用 sync.RWMutex 保护读写
|
||
type CodeCache struct {
|
||
// protos 缓存映射:键 -> 编译后的字节码
|
||
protos map[string]*CachedProto
|
||
|
||
// order 访问顺序列表(用于 LRU 淘汰)
|
||
order []string
|
||
|
||
// 最大缓存条目数
|
||
maxSize int
|
||
|
||
// 缓存生存时间
|
||
ttl time.Duration
|
||
|
||
// 缓存命中次数
|
||
hits atomic.Uint64
|
||
|
||
// 缓存未命中次数
|
||
misses atomic.Uint64
|
||
|
||
// 读写锁
|
||
mu sync.RWMutex
|
||
|
||
// 是否启用文件变更检测
|
||
fileWatch bool
|
||
}
|
||
|
||
// NewCodeCache 创建字节码缓存实例。
|
||
//
|
||
// 参数:
|
||
// - maxSize: 最大缓存条目数
|
||
// - ttl: 缓存生存时间,零值表示永不过期
|
||
// - fileWatch: 是否启用文件变更检测
|
||
//
|
||
// 返回值:
|
||
// - *CodeCache: 初始化的缓存实例
|
||
func NewCodeCache(maxSize int, ttl time.Duration, fileWatch bool) *CodeCache {
|
||
return &CodeCache{
|
||
protos: make(map[string]*CachedProto),
|
||
order: make([]string, 0, maxSize),
|
||
maxSize: maxSize,
|
||
ttl: ttl,
|
||
fileWatch: fileWatch,
|
||
}
|
||
}
|
||
|
||
// generateInlineKey 生成内联脚本的缓存键。
|
||
//
|
||
// 使用 SHA256 哈希算法对脚本内容进行摘要,前缀为 "nhli_"。
|
||
func (c *CodeCache) generateInlineKey(src string) string {
|
||
hash := sha256.Sum256([]byte(src))
|
||
return "nhli_" + hex.EncodeToString(hash[:])
|
||
}
|
||
|
||
// generateFileKey 生成文件脚本的缓存键。
|
||
//
|
||
// 使用 SHA256 哈希算法对文件路径进行摘要,前缀为 "nhlf_"。
|
||
// 注意:键基于路径而非内容,文件变更检测由 isFileChanged 负责。
|
||
func (c *CodeCache) generateFileKey(path string) string {
|
||
hash := sha256.Sum256([]byte(path))
|
||
return "nhlf_" + hex.EncodeToString(hash[:])
|
||
}
|
||
|
||
// GetOrCompileInline 获取或编译内联脚本。
|
||
//
|
||
// 查找流程:
|
||
// 1. 基于脚本内容生成缓存键
|
||
// 2. 检查缓存是否命中且未过期
|
||
// 3. 未命中则解析并编译脚本,存入缓存
|
||
//
|
||
// 参数:
|
||
// - src: Lua 源代码字符串
|
||
//
|
||
// 返回值:
|
||
// - *glua.FunctionProto: 编译后的函数原型
|
||
// - error: 解析或编译失败时返回错误
|
||
func (c *CodeCache) GetOrCompileInline(src string) (*glua.FunctionProto, error) {
|
||
key := c.generateInlineKey(src)
|
||
|
||
c.mu.RLock()
|
||
cached, ok := c.protos[key]
|
||
c.mu.RUnlock()
|
||
|
||
if ok && !c.isExpired(cached) {
|
||
c.hits.Add(1)
|
||
cached.AccessAt.Store(time.Now())
|
||
return cached.Proto, nil
|
||
}
|
||
|
||
c.misses.Add(1)
|
||
|
||
// 编译脚本
|
||
chunk, err := parse.Parse(strings.NewReader(src), "<inline>")
|
||
if err != nil {
|
||
return nil, fmt.Errorf("parse inline script: %w", err)
|
||
}
|
||
proto, err := glua.Compile(chunk, "<inline>")
|
||
if err != nil {
|
||
return nil, fmt.Errorf("compile inline script: %w", err)
|
||
}
|
||
|
||
// 存入缓存
|
||
cached = &CachedProto{
|
||
Proto: proto,
|
||
SourceType: CacheKeyInline,
|
||
CachedAt: time.Now(),
|
||
}
|
||
cached.AccessAt.Store(time.Now())
|
||
|
||
c.mu.Lock()
|
||
c.storeLocked(key, cached)
|
||
c.mu.Unlock()
|
||
|
||
return proto, nil
|
||
}
|
||
|
||
// GetOrCompileFile 获取或编译文件脚本。
|
||
//
|
||
// 查找流程:
|
||
// 1. 基于文件路径生成缓存键
|
||
// 2. 检查缓存是否命中、未过期且文件未变更
|
||
// 3. 未命中则读取文件、解析并编译,存入缓存
|
||
//
|
||
// 参数:
|
||
// - path: Lua 脚本文件路径
|
||
//
|
||
// 返回值:
|
||
// - *glua.FunctionProto: 编译后的函数原型
|
||
// - error: 读取、解析或编译失败时返回错误
|
||
func (c *CodeCache) GetOrCompileFile(path string) (*glua.FunctionProto, error) {
|
||
key := c.generateFileKey(path)
|
||
|
||
c.mu.RLock()
|
||
cached, ok := c.protos[key]
|
||
c.mu.RUnlock()
|
||
|
||
// 检查是否需要重新加载
|
||
if ok && !c.isExpired(cached) && !c.isFileChanged(cached) {
|
||
c.hits.Add(1)
|
||
cached.AccessAt.Store(time.Now())
|
||
return cached.Proto, nil
|
||
}
|
||
|
||
c.misses.Add(1)
|
||
|
||
// 读取文件
|
||
content, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("read file %s: %w", path, err)
|
||
}
|
||
|
||
// 获取文件信息
|
||
info, err := os.Stat(path)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("stat file %s: %w", path, err)
|
||
}
|
||
|
||
// 编译脚本
|
||
reader := bufio.NewReader(strings.NewReader(string(content)))
|
||
chunk, err := parse.Parse(reader, path)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("parse file %s: %w", path, err)
|
||
}
|
||
proto, err := glua.Compile(chunk, path)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("compile file %s: %w", path, err)
|
||
}
|
||
|
||
// 存入缓存
|
||
cached = &CachedProto{
|
||
Proto: proto,
|
||
SourceType: CacheKeyFile,
|
||
SourcePath: path,
|
||
ModTime: info.ModTime(),
|
||
CachedAt: time.Now(),
|
||
}
|
||
cached.AccessAt.Store(time.Now())
|
||
|
||
c.mu.Lock()
|
||
c.storeLocked(key, cached)
|
||
c.mu.Unlock()
|
||
|
||
return proto, nil
|
||
}
|
||
|
||
// storeLocked 将缓存条目存入映射(需已持有写锁)。
|
||
//
|
||
// 如果键已存在则更新;否则先检查容量并可能触发 LRU 淘汰。
|
||
func (c *CodeCache) storeLocked(key string, cached *CachedProto) {
|
||
// 如果已存在,更新
|
||
if _, ok := c.protos[key]; ok {
|
||
c.protos[key] = cached
|
||
return
|
||
}
|
||
|
||
// LRU 淘汰
|
||
if len(c.protos) >= c.maxSize {
|
||
c.evictLocked()
|
||
}
|
||
|
||
c.protos[key] = cached
|
||
c.order = append(c.order, key)
|
||
}
|
||
|
||
// evictLocked 淘汰最久未访问的缓存条目(需已持有写锁)。
|
||
//
|
||
// 遍历 order 列表,找到 AccessAt 最早的条目并删除。
|
||
func (c *CodeCache) evictLocked() {
|
||
if len(c.order) == 0 {
|
||
return
|
||
}
|
||
|
||
// 找到最久未访问的
|
||
oldestKey := c.order[0]
|
||
oldestTime := time.Now()
|
||
|
||
for _, key := range c.order {
|
||
cached := c.protos[key]
|
||
if t, ok := cached.AccessAt.Load().(time.Time); ok && t.Before(oldestTime) {
|
||
oldestTime = t
|
||
oldestKey = key
|
||
}
|
||
}
|
||
|
||
// 删除
|
||
delete(c.protos, oldestKey)
|
||
for i, k := range c.order {
|
||
if k == oldestKey {
|
||
c.order = append(c.order[:i], c.order[i+1:]...)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// isExpired 检查缓存条目是否超过 TTL。
|
||
//
|
||
// 如果 TTL 为零或负数,永不过期。
|
||
func (c *CodeCache) isExpired(cached *CachedProto) bool {
|
||
if c.ttl <= 0 {
|
||
return false
|
||
}
|
||
return time.Since(cached.CachedAt) > c.ttl
|
||
}
|
||
|
||
// isFileChanged 检查文件脚本是否已变更。
|
||
//
|
||
// 通过比较文件的修改时间与缓存中记录的 ModTime 判断。
|
||
// 如果文件不存在或无法 stat,视为已变更(触发重新编译)。
|
||
//
|
||
// 返回值:
|
||
// - bool: true 表示文件已变更,false 表示未变更或文件监控未启用
|
||
func (c *CodeCache) isFileChanged(cached *CachedProto) bool {
|
||
if !c.fileWatch || cached.SourceType != CacheKeyFile {
|
||
return false
|
||
}
|
||
|
||
info, err := os.Stat(cached.SourcePath)
|
||
if err != nil {
|
||
return true // 文件不存在,视为变更
|
||
}
|
||
|
||
return info.ModTime().After(cached.ModTime)
|
||
}
|
||
|
||
// Stats 返回缓存统计信息。
|
||
//
|
||
// 返回值:
|
||
// - hits: 缓存命中次数
|
||
// - misses: 缓存未命中次数
|
||
// - size: 当前缓存条目数
|
||
func (c *CodeCache) Stats() (hits, misses uint64, size int) {
|
||
c.mu.RLock()
|
||
defer c.mu.RUnlock()
|
||
return c.hits.Load(), c.misses.Load(), len(c.protos)
|
||
}
|
||
|
||
// HitRate 返回缓存命中率
|
||
func (c *CodeCache) HitRate() float64 {
|
||
hits := c.hits.Load()
|
||
misses := c.misses.Load()
|
||
total := hits + misses
|
||
if total == 0 {
|
||
return 0
|
||
}
|
||
return float64(hits) / float64(total)
|
||
}
|
||
|
||
// Clear 清空缓存
|
||
func (c *CodeCache) Clear() {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
c.protos = make(map[string]*CachedProto)
|
||
c.order = c.order[:0]
|
||
}
|