lolly/internal/lua/coroutine.go
xfy 7a66e350f0 feat(lua): 添加 Lua 脚本嵌入支持
- 基于 gopher-lua 实现类似 OpenResty 的脚本嵌入能力
- LuaEngine: server 级单 LState + 请求级临时协程
- LuaContext: 请求上下文,变量存储和阶段管理
- LuaCoroutine: 沙箱隔离,Yield/Resume 循环,执行超时
- CodeCache: 字节码缓存,LRU 淘汰 + TTL 过期
- 新增 testify 用于测试断言

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 14:19:03 +08:00

189 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package lua 提供 Lua 脚本嵌入能力
package lua
import (
"context"
"fmt"
"sync/atomic"
"time"
glua "github.com/yuin/gopher-lua"
"github.com/valyala/fasthttp"
)
// Phase 处理阶段
type Phase int
const (
PhaseInit Phase = iota
PhaseRewrite
PhaseAccess
PhaseContent
PhaseLog
PhaseHeaderFilter
PhaseBodyFilter
)
func (p Phase) String() string {
switch p {
case PhaseInit:
return "init"
case PhaseRewrite:
return "rewrite"
case PhaseAccess:
return "access"
case PhaseContent:
return "content"
case PhaseLog:
return "log"
case PhaseHeaderFilter:
return "header_filter"
case PhaseBodyFilter:
return "body_filter"
default:
return "unknown"
}
}
// LuaCoroutine 请求级临时协程
// 注意:协程在 ResumeOK 后变成 dead 状态,不能复用
type LuaCoroutine struct {
// 所属引擎
Engine *LuaEngine
// 协程 LState通过 NewThread 创建)
Co *glua.LState
// 取消函数
Cancel context.CancelFunc
// 请求上下文
RequestCtx *fasthttp.RequestCtx
// 执行上下文
ExecutionContext context.Context
executionCancel context.CancelFunc
// 创建时间
CreatedAt time.Time
// 状态
Exited bool // 是否已调用 exit
// 输出缓冲
OutputBuffer []byte
}
// SetupSandbox 创建 per-request _ENV 沙箱
// 每个请求创建独立的 _ENV 表,通过元表继承全局环境
func (c *LuaCoroutine) SetupSandbox() error {
// 创建独立的 _ENV 表
env := c.Co.NewTable()
// 获取全局环境 - 使用 Engine 的主 LState 全局表
// 协程通过 NewThread 继承了父 LState 的全局环境
globals := c.Engine.L.GetGlobal("_G")
// 设置元表,使未找到的变量从全局环境读取
mt := c.Co.NewTable()
mt.RawSetString("__index", globals)
// 阻止写入全局环境(可选)
readOnlyFn := c.Co.NewFunction(func(L *glua.LState) int {
L.RaiseError("attempt to modify global table (read-only)")
return 0
})
mt.RawSetString("__newindex", readOnlyFn)
// 设置元表
c.Co.SetMetatable(env, mt)
// 将 _ENV 设置到协程
c.Co.SetGlobal("_ENV", env)
return nil
}
// Execute 在协程中执行 Lua 脚本(支持 Yield/Resume
func (c *LuaCoroutine) Execute(script string) error {
proto, err := c.Engine.codeCache.GetOrCompileInline(script)
if err != nil {
return fmt.Errorf("compile script: %w", err)
}
return c.executeProto(proto)
}
// ExecuteFile 执行文件脚本
func (c *LuaCoroutine) ExecuteFile(path string) error {
proto, err := c.Engine.codeCache.GetOrCompileFile(path)
if err != nil {
return fmt.Errorf("compile file: %w", err)
}
return c.executeProto(proto)
}
// executeProto 执行编译后的字节码,处理 yield/resume 循环
func (c *LuaCoroutine) executeProto(proto *glua.FunctionProto) error {
fn := c.Engine.L.NewFunctionFromProto(proto)
st, execErr, values := c.Engine.L.Resume(c.Co, fn)
for st == glua.ResumeYield {
results, handleErr := c.handleYield(values)
if handleErr != nil {
return fmt.Errorf("handle yield: %w", handleErr)
}
st, execErr, values = c.Engine.L.Resume(c.Co, nil, results...)
}
if st == glua.ResumeError {
atomic.AddUint64(&c.Engine.stats.ScriptsErrors, 1)
return fmt.Errorf("lua execution error: %w", execErr)
}
atomic.AddUint64(&c.Engine.stats.ScriptsExecuted, 1)
return nil
}
// handleYield 处理协程 yield
func (c *LuaCoroutine) handleYield(values []glua.LValue) ([]glua.LValue, error) {
if len(values) == 0 {
return nil, fmt.Errorf("yield without reason")
}
reason := glua.LVAsString(values[0])
switch reason {
case "sleep":
return c.handleSleep(values[1:])
default:
return nil, fmt.Errorf("unknown yield reason: %s", reason)
}
}
// handleSleep 处理 sleep yield
// 注意:此实现会阻塞当前 goroutine
func (c *LuaCoroutine) handleSleep(values []glua.LValue) ([]glua.LValue, error) {
if len(values) == 0 {
return nil, fmt.Errorf("sleep requires duration")
}
duration := float64(glua.LVAsNumber(values[0]))
d := time.Duration(duration * float64(time.Second))
timer := time.NewTimer(d)
defer timer.Stop()
select {
case <-timer.C:
// sleep 完成,返回空结果
return []glua.LValue{}, nil
case <-c.ExecutionContext.Done():
// 执行超时或取消
return nil, fmt.Errorf("sleep interrupted: %w", c.ExecutionContext.Err())
}
}
// Close 关闭协程
func (c *LuaCoroutine) Close() {
c.Engine.releaseCoroutine(c)
}