为所有 Lua API 文件添加完整的包级和函数级文档注释: - api_balancer: 负载均衡 API(set_current_peer, set_more_tries 等) - api_ctx: 请求上下文存储 API(ngx.ctx) - api_location: 子请求捕获 API(ngx.location.capture) - api_log: 日志输出 API(ngx.log) - api_req: 请求对象 API - api_resp: 响应对象 API - api_shared_dict: 共享字典 API - api_socket_tcp: TCP socket API - api_timer: 定时器 API - api_var: 变量 API - engine: Lua 引擎核心 - context: 请求上下文管理 - coroutine: 协程调度器 - middleware: 中间件集成 - filter_writer: 响应过滤器 - cache: Lua 脚本缓存 - shared_dict: 共享字典实现 - socket_manager: socket 连接管理 注释格式遵循 Go 官方风格,包含功能说明、参数说明和注意事项。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
369 lines
9.1 KiB
Go
369 lines
9.1 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 uint64
|
||
|
||
// 缓存未命中次数
|
||
misses 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) {
|
||
atomic.AddUint64(&c.hits, 1)
|
||
cached.AccessAt.Store(time.Now())
|
||
return cached.Proto, nil
|
||
}
|
||
|
||
atomic.AddUint64(&c.misses, 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) {
|
||
atomic.AddUint64(&c.hits, 1)
|
||
cached.AccessAt.Store(time.Now())
|
||
return cached.Proto, nil
|
||
}
|
||
|
||
atomic.AddUint64(&c.misses, 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 atomic.LoadUint64(&c.hits), atomic.LoadUint64(&c.misses), len(c.protos)
|
||
}
|
||
|
||
// HitRate 返回缓存命中率
|
||
func (c *CodeCache) HitRate() float64 {
|
||
hits := atomic.LoadUint64(&c.hits)
|
||
misses := atomic.LoadUint64(&c.misses)
|
||
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]
|
||
}
|